mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Add pool CRUD operations/pages
This commit is contained in:
30
client/css/pool-categories-view.styl
Normal file
30
client/css/pool-categories-view.styl
Normal file
@ -0,0 +1,30 @@
|
||||
@import colors
|
||||
|
||||
.content-wrapper.pool-categories
|
||||
width: 100%
|
||||
max-width: 45em
|
||||
table
|
||||
border-spacing: 0
|
||||
width: 100%
|
||||
tr.default td
|
||||
background: $default-pool-category-background-color
|
||||
td, th
|
||||
padding: .4em
|
||||
&.color
|
||||
input[type=text]
|
||||
width: 8em
|
||||
&.usages
|
||||
text-align: center
|
||||
&.remove, &.set-default
|
||||
white-space: pre
|
||||
th
|
||||
white-space: nowrap
|
||||
&:first-child
|
||||
padding-left: 0
|
||||
&:last-child
|
||||
padding-right: 0
|
||||
tfoot
|
||||
display: none
|
||||
form
|
||||
width: auto
|
||||
|
53
client/css/pool-input-control.styl
Normal file
53
client/css/pool-input-control.styl
Normal file
@ -0,0 +1,53 @@
|
||||
@import colors
|
||||
|
||||
div.pool-input
|
||||
position: relative
|
||||
|
||||
.main-control
|
||||
display: flex
|
||||
input
|
||||
flex: 5
|
||||
button
|
||||
flex: 1
|
||||
margin: 0 0 0 0.5em
|
||||
|
||||
|
||||
ul.compact-pools
|
||||
width: 100%
|
||||
margin: 0.5em 0 0 0
|
||||
padding: 0
|
||||
li
|
||||
margin: 0
|
||||
width: 100%
|
||||
line-height: 140%
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
transition: background-color 0.5s linear
|
||||
a
|
||||
display: inline
|
||||
a:focus
|
||||
outline: 0
|
||||
box-shadow: inset 0 0 0 2px $main-color
|
||||
&.implication
|
||||
background: $implied-pool-background-color
|
||||
color: $implied-pool-text-color
|
||||
&.new
|
||||
background: $new-pool-background-color
|
||||
color: $new-pool-text-color
|
||||
&.duplicate
|
||||
background: $duplicate-pool-background-color
|
||||
color: $duplicate-pool-text-color
|
||||
i
|
||||
padding-right: 0.4em
|
||||
|
||||
div.pool-input, ul.compact-pools
|
||||
.pool-usages, .pool-weight, .remove-pool
|
||||
color: $inactive-link-color
|
||||
unselectable()
|
||||
.pool-usages, .pool-weight
|
||||
font-size: 90%
|
||||
.pool-usages, .pool-weight
|
||||
margin-left: 0.7em
|
||||
.remove-pool
|
||||
margin-right: 0.5em
|
52
client/css/pool-list-view.styl
Normal file
52
client/css/pool-list-view.styl
Normal file
@ -0,0 +1,52 @@
|
||||
@import colors
|
||||
|
||||
.pool-list
|
||||
table
|
||||
width: 100%
|
||||
border-spacing: 0
|
||||
text-align: left
|
||||
line-height: 1.3em
|
||||
tr:hover td
|
||||
background: $top-navigation-color
|
||||
th, td
|
||||
padding: 0.1em 0.5em
|
||||
th
|
||||
white-space: nowrap
|
||||
background: $top-navigation-color
|
||||
.names
|
||||
width: 28%
|
||||
.usages
|
||||
text-align: center
|
||||
width: 8%
|
||||
.creation-time
|
||||
text-align: center
|
||||
width: 8%
|
||||
white-space: pre
|
||||
ul
|
||||
list-style-type: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
display: inline
|
||||
li
|
||||
padding: 0
|
||||
display: inline
|
||||
&:not(:last-child):after
|
||||
content: ', '
|
||||
@media (max-width: 800px)
|
||||
.implications, .suggestions
|
||||
display: none
|
||||
|
||||
.pool-list-header
|
||||
label
|
||||
display: none !important
|
||||
text-align: left
|
||||
form
|
||||
width: auto
|
||||
input[name=search-text]
|
||||
width: 25em
|
||||
@media (max-width: 1000px)
|
||||
width: 100%
|
||||
.append
|
||||
vertical-align: middle
|
||||
font-size: 0.95em
|
||||
color: $inactive-link-color
|
33
client/css/pool-view.styl
Normal file
33
client/css/pool-view.styl
Normal file
@ -0,0 +1,33 @@
|
||||
#pool
|
||||
width: 100%
|
||||
max-width: 40em
|
||||
h1
|
||||
word-break: break-all
|
||||
line-height: 130%
|
||||
margin-top: 0
|
||||
form
|
||||
width: 100%
|
||||
.pool-edit
|
||||
textarea
|
||||
height: 10em
|
||||
.pool-summary
|
||||
section
|
||||
&.description
|
||||
margin: 1.5em 0 0 0
|
||||
&.details
|
||||
vertical-align: top
|
||||
padding-right: 0.5em
|
||||
ul
|
||||
margin: 0
|
||||
padding: 0
|
||||
list-style-type: none
|
||||
li
|
||||
display: inline
|
||||
margin: 0
|
||||
padding: 0
|
||||
li:not(:last-of-type):after
|
||||
content: ', '
|
||||
ul:empty:after
|
||||
content: '(none)'
|
||||
section
|
||||
margin-bottom: 1em
|
@ -4,6 +4,7 @@
|
||||
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
|
||||
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
|
||||
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
|
||||
--><li data-name='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</a></li><!--
|
||||
--></ul><!--
|
||||
--></nav>
|
||||
|
||||
|
97
client/html/help_search_pools.tpl
Normal file
97
client/html/help_search_pools.tpl
Normal file
@ -0,0 +1,97 @@
|
||||
<p><strong>Anonymous tokens</strong></p>
|
||||
|
||||
<p>Same as <code>name</code> token.</p>
|
||||
|
||||
<p><strong>Named tokens</strong></p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>having given name (accepts wildcards)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
<td>having given category (accepts wildcards)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-date</code></td>
|
||||
<td>created at given date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-time</code></td>
|
||||
<td>alias of <code>creation-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-date</code></td>
|
||||
<td>edited at given date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-time</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-date</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-time</code></td>
|
||||
<td>alias of <code>last-edit-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>post-count</code></td>
|
||||
<td>alias of <code>usages</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Sort style tokens</strong></p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>random</code></td>
|
||||
<td>as random as it can get</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>A to Z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
<td>category (A to Z)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-date</code></td>
|
||||
<td>recently created first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>creation-time</code></td>
|
||||
<td>alias of <code>creation-date</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-date</code></td>
|
||||
<td>recently edited first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>last-edit-time</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-date</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>edit-time</code></td>
|
||||
<td>alias of <code>creation-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>post-count</code></td>
|
||||
<td>number of posts</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><strong>Special tokens</strong></p>
|
||||
|
||||
<p>None.</p>
|
18
client/html/pool.tpl
Normal file
18
client/html/pool.tpl
Normal file
@ -0,0 +1,18 @@
|
||||
<div class='content-wrapper' id='pool'>
|
||||
<h1><%- ctx.pool.first_name %></h1>
|
||||
<nav class='buttons'><!--
|
||||
--><ul><!--
|
||||
--><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
|
||||
--><% if (ctx.canEditAnything) { %><!--
|
||||
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!--
|
||||
--><% } %><!--
|
||||
--><% if (ctx.canMerge) { %><!--
|
||||
--><li data-name='merge'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'merge') %>'>Merge with…</a></li><!--
|
||||
--><% } %><!--
|
||||
--><% if (ctx.canDelete) { %><!--
|
||||
--><li data-name='delete'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'delete') %>'>Delete</a></li><!--
|
||||
--><% } %><!--
|
||||
--></ul><!--
|
||||
--></nav>
|
||||
<div class='pool-content-holder'></div>
|
||||
</div>
|
30
client/html/pool_categories.tpl
Normal file
30
client/html/pool_categories.tpl
Normal file
@ -0,0 +1,30 @@
|
||||
<div class='content-wrapper pool-categories'>
|
||||
<form>
|
||||
<h1>Pool categories</h1>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class='name'>Category name</th>
|
||||
<th class='color'>CSS color</th>
|
||||
<th class='usages'>Usages</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% if (ctx.canCreate) { %>
|
||||
<p><a href class='add'>Add new category</a></p>
|
||||
<% } %>
|
||||
|
||||
<div class='messages'></div>
|
||||
|
||||
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
|
||||
<div class='buttons'>
|
||||
<input type='submit' class='save' value='Save changes'>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
43
client/html/pool_category_row.tpl
Normal file
43
client/html/pool_category_row.tpl
Normal file
@ -0,0 +1,43 @@
|
||||
<% if (ctx.poolCategory.isDefault) { %><%
|
||||
%><tr data-category='<%- ctx.poolCategory.name %>' class='default'><%
|
||||
%><% } else { %><%
|
||||
%><tr data-category='<%- ctx.poolCategory.name %>'><%
|
||||
%><% } %>
|
||||
<td class='name'>
|
||||
<% if (ctx.canEditName) { %>
|
||||
<%= ctx.makeTextInput({value: ctx.poolCategory.name, required: true}) %>
|
||||
<% } else { %>
|
||||
<%- ctx.poolCategory.name %>
|
||||
<% } %>
|
||||
</td>
|
||||
<td class='color'>
|
||||
<% if (ctx.canEditColor) { %>
|
||||
<%= ctx.makeColorInput({value: ctx.poolCategory.color}) %>
|
||||
<% } else { %>
|
||||
<%- ctx.poolCategory.color %>
|
||||
<% } %>
|
||||
</td>
|
||||
<td class='usages'>
|
||||
<% if (ctx.poolCategory.name) { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'category:' + ctx.poolCategory.name}) %>'>
|
||||
<%- ctx.poolCategory.poolCount %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<%- ctx.poolCategory.poolCount %>
|
||||
<% } %>
|
||||
</td>
|
||||
<% if (ctx.canDelete) { %>
|
||||
<td class='remove'>
|
||||
<% if (ctx.poolCategory.poolCount) { %>
|
||||
<a class='inactive' title="Can't delete category in use">Remove</a>
|
||||
<% } else { %>
|
||||
<a href>Remove</a>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
<% if (ctx.canSetDefault) { %>
|
||||
<td class='set-default'>
|
||||
<a href>Make default</a>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
35
client/html/pool_create.tpl
Normal file
35
client/html/pool_create.tpl
Normal file
@ -0,0 +1,35 @@
|
||||
<div class='content-wrapper pool-create'>
|
||||
<form>
|
||||
<ul class='input'>
|
||||
<li class='names'>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Names',
|
||||
value: '',
|
||||
required: true,
|
||||
}) %>
|
||||
</li>
|
||||
<li class='category'>
|
||||
<%= ctx.makeSelect({
|
||||
text: 'Category',
|
||||
keyValues: ctx.categories,
|
||||
selectedKey: 'default',
|
||||
required: true,
|
||||
}) %>
|
||||
</li>
|
||||
<li class='description'>
|
||||
<%= ctx.makeTextarea({
|
||||
text: 'Description',
|
||||
value: '',
|
||||
}) %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<% if (ctx.canCreate) { %>
|
||||
<div class='messages'></div>
|
||||
|
||||
<div class='buttons'>
|
||||
<input type='submit' class='save' value='Create pool'>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
21
client/html/pool_delete.tpl
Normal file
21
client/html/pool_delete.tpl
Normal file
@ -0,0 +1,21 @@
|
||||
<div class='pool-delete'>
|
||||
<form>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
|
||||
<ul class='input'>
|
||||
<li>
|
||||
<%= ctx.makeCheckbox({
|
||||
name: 'confirm-deletion',
|
||||
text: 'I confirm that I want to delete this pool.',
|
||||
required: true,
|
||||
}) %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class='messages'></div>
|
||||
|
||||
<div class='buttons'>
|
||||
<input type='submit' value='Delete pool'/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
41
client/html/pool_edit.tpl
Normal file
41
client/html/pool_edit.tpl
Normal file
@ -0,0 +1,41 @@
|
||||
<div class='content-wrapper pool-edit'>
|
||||
<form>
|
||||
<ul class='input'>
|
||||
<li class='names'>
|
||||
<% if (ctx.canEditNames) { %>
|
||||
<%= ctx.makeTextInput({
|
||||
text: 'Names',
|
||||
value: ctx.pool.names.join(' '),
|
||||
required: true,
|
||||
}) %>
|
||||
<% } %>
|
||||
</li>
|
||||
<li class='category'>
|
||||
<% if (ctx.canEditCategory) { %>
|
||||
<%= ctx.makeSelect({
|
||||
text: 'Category',
|
||||
keyValues: ctx.categories,
|
||||
selectedKey: ctx.pool.category,
|
||||
required: true,
|
||||
}) %>
|
||||
<% } %>
|
||||
</li>
|
||||
<li class='description'>
|
||||
<% if (ctx.canEditDescription) { %>
|
||||
<%= ctx.makeTextarea({
|
||||
text: 'Description',
|
||||
value: ctx.pool.description,
|
||||
}) %>
|
||||
<% } %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<% if (ctx.canEditAnything) { %>
|
||||
<div class='messages'></div>
|
||||
|
||||
<div class='buttons'>
|
||||
<input type='submit' class='save' value='Save changes'>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
0
client/html/pool_input.tpl
Normal file
0
client/html/pool_input.tpl
Normal file
22
client/html/pool_merge.tpl
Normal file
22
client/html/pool_merge.tpl
Normal file
@ -0,0 +1,22 @@
|
||||
<div class='pool-merge'>
|
||||
<form>
|
||||
<ul class='input'>
|
||||
<li class='target'>
|
||||
<%= ctx.makeTextInput({name: 'target-pool', required: true, text: 'Target pool', pattern: ctx.poolNamePattern}) %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<p>Posts between the two pools will be combined.
|
||||
Category needs to be handled manually.</p>
|
||||
|
||||
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class='messages'></div>
|
||||
|
||||
<div class='buttons'>
|
||||
<input type='submit' value='Merge pool'/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
23
client/html/pool_summary.tpl
Normal file
23
client/html/pool_summary.tpl
Normal file
@ -0,0 +1,23 @@
|
||||
<div class='content-wrapper pool-summary'>
|
||||
<section class='details'>
|
||||
<section>
|
||||
Category:
|
||||
<span class='<%= ctx.makeCssName(ctx.pool.category, 'pool') %>'><%- ctx.pool.category %></span>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
Aliases:<br/>
|
||||
<ul><!--
|
||||
--><% for (let name of ctx.pool.names.slice(1)) { %><!--
|
||||
--><li><%= ctx.makePoolLink(ctx.pool, false, false, name) %></li><!--
|
||||
--><% } %><!--
|
||||
--></ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class='description'>
|
||||
<hr/>
|
||||
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.pool.names[0])}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
</section>
|
||||
</div>
|
22
client/html/pools_header.tpl
Normal file
22
client/html/pools_header.tpl
Normal file
@ -0,0 +1,22 @@
|
||||
<div class='pool-list-header'>
|
||||
<form class='horizontal'>
|
||||
<ul class='input'>
|
||||
<li>
|
||||
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class='buttons'>
|
||||
<input type='submit' value='Search'/>
|
||||
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Syntax help</a>
|
||||
|
||||
<% if (ctx.canCreate) { %>
|
||||
<a class='append' href='<%- ctx.formatClientLink('pool', 'create') %>'>Add new pool</a>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPoolCategories) { %>
|
||||
<a class='append' href='<%- ctx.formatClientLink('pool-categories') %>'>Pool categories</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
48
client/html/pools_page.tpl
Normal file
48
client/html/pools_page.tpl
Normal file
@ -0,0 +1,48 @@
|
||||
<div class='pool-list table-wrap'>
|
||||
<% if (ctx.response.results.length) { %>
|
||||
<table>
|
||||
<thead>
|
||||
<th class='names'>
|
||||
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='post-count'>
|
||||
<% if (ctx.parameters.query == 'sort:post-count') { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post Count</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post Count</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='creation-time'>
|
||||
<% if (ctx.parameters.query == 'sort:creation-time') { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
|
||||
<% } %>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let pool of ctx.response.results) { %>
|
||||
<tr>
|
||||
<td class='names'>
|
||||
<ul>
|
||||
<% for (let name of pool.names) { %>
|
||||
<li><%= ctx.makePoolLink(pool, false, false, name) %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</td>
|
||||
<td class='post-count'>
|
||||
<a href='<%- ctx.formatClientLink('pools', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
|
||||
</td>
|
||||
<td class='creation-time'>
|
||||
<%= ctx.makeRelativeTime(pool.creationTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
@ -3,35 +3,35 @@
|
||||
<table>
|
||||
<thead>
|
||||
<th class='names'>
|
||||
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
|
||||
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:name'}) %>'>Tag name(s)</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='implications'>
|
||||
<% if (ctx.query == 'sort:implication-count') { %>
|
||||
<% if (ctx.parameters.query == 'sort:implication-count') { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:implication-count'}) %>'>Implications</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='suggestions'>
|
||||
<% if (ctx.query == 'sort:suggestion-count') { %>
|
||||
<% if (ctx.parameters.query == 'sort:suggestion-count') { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:suggestion-count'}) %>'>Suggestions</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='usages'>
|
||||
<% if (ctx.query == 'sort:usages') { %>
|
||||
<% if (ctx.parameters.query == 'sort:usages') { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:usages'}) %>'>Usages</a>
|
||||
<% } %>
|
||||
</th>
|
||||
<th class='creation-time'>
|
||||
<% if (ctx.query == 'sort:creation-time') { %>
|
||||
<% if (ctx.parameters.query == 'sort:creation-time') { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
|
||||
<% } else { %>
|
||||
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>
|
||||
|
@ -84,6 +84,10 @@ class Api extends events.EventTarget {
|
||||
return remoteConfig.tagNameRegex;
|
||||
}
|
||||
|
||||
getPoolNameRegex() {
|
||||
return remoteConfig.poolNameRegex;
|
||||
}
|
||||
|
||||
getPasswordRegex() {
|
||||
return remoteConfig.passwordRegex;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const tags = require('../tags.js');
|
||||
const pools = require('../pools.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const LoginView = require('../views/login_view.js');
|
||||
@ -27,6 +28,7 @@ class LoginController {
|
||||
ctx.controller.showSuccess('Logged in');
|
||||
// reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous
|
||||
tags.refreshCategoryColorMap();
|
||||
pools.refreshCategoryColorMap();
|
||||
}, error => {
|
||||
this._loginView.showError(error.message);
|
||||
this._loginView.enableForm();
|
||||
|
57
client/js/controllers/pool_categories_controller.js
Normal file
57
client/js/controllers/pool_categories_controller.js
Normal file
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const pools = require('../pools.js');
|
||||
const PoolCategoryList = require('../models/pool_category_list.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const PoolCategoriesView = require('../views/pool_categories_view.js');
|
||||
const EmptyView = require('../views/empty_view.js');
|
||||
|
||||
class PoolCategoriesController {
|
||||
constructor() {
|
||||
if (!api.hasPrivilege('poolCategories:list')) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(
|
||||
'You don\'t have privileges to view pool categories.');
|
||||
return;
|
||||
}
|
||||
|
||||
topNavigation.activate('pools');
|
||||
topNavigation.setTitle('Listing pools');
|
||||
PoolCategoryList.get().then(response => {
|
||||
this._poolCategories = response.results;
|
||||
this._view = new PoolCategoriesView({
|
||||
poolCategories: this._poolCategories,
|
||||
canEditName: api.hasPrivilege('poolCategories:edit:name'),
|
||||
canEditColor: api.hasPrivilege('poolCategories:edit:color'),
|
||||
canDelete: api.hasPrivilege('poolCategories:delete'),
|
||||
canCreate: api.hasPrivilege('poolCategories:create'),
|
||||
canSetDefault: api.hasPrivilege('poolCategories:setDefault'),
|
||||
});
|
||||
this._view.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
this._poolCategories.save()
|
||||
.then(() => {
|
||||
pools.refreshCategoryColorMap();
|
||||
this._view.enableForm();
|
||||
this._view.showSuccess('Changes saved.');
|
||||
}, error => {
|
||||
this._view.enableForm();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router => {
|
||||
router.enter(['pool-categories'], (ctx, next) => {
|
||||
ctx.controller = new PoolCategoriesController(ctx, next);
|
||||
});
|
||||
};
|
141
client/js/controllers/pool_controller.js
Normal file
141
client/js/controllers/pool_controller.js
Normal file
@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const Pool = require('../models/pool.js');
|
||||
const PoolCategoryList = require('../models/pool_category_list.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const PoolView = require('../views/pool_view.js');
|
||||
const EmptyView = require('../views/empty_view.js');
|
||||
|
||||
class PoolController {
|
||||
constructor(ctx, section) {
|
||||
if (!api.hasPrivilege('pools:view')) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError('You don\'t have privileges to view pools.');
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
PoolCategoryList.get(),
|
||||
Pool.get(ctx.parameters.id)
|
||||
]).then(responses => {
|
||||
const [poolCategoriesResponse, pool] = responses;
|
||||
|
||||
topNavigation.activate('pools');
|
||||
topNavigation.setTitle('Pool #' + pool.names[0]);
|
||||
|
||||
this._name = ctx.parameters.name;
|
||||
pool.addEventListener('change', e => this._evtSaved(e, section));
|
||||
|
||||
const categories = {};
|
||||
for (let category of poolCategoriesResponse.results) {
|
||||
categories[category.name] = category.name;
|
||||
}
|
||||
|
||||
this._view = new PoolView({
|
||||
pool: pool,
|
||||
section: section,
|
||||
canEditAnything: api.hasPrivilege('pools:edit'),
|
||||
canEditNames: api.hasPrivilege('pools:edit:names'),
|
||||
canEditCategory: api.hasPrivilege('pools:edit:category'),
|
||||
canEditDescription: api.hasPrivilege('pools:edit:description'),
|
||||
canMerge: api.hasPrivilege('pools:merge'),
|
||||
canDelete: api.hasPrivilege('pools:delete'),
|
||||
categories: categories,
|
||||
escapeColons: uri.escapeColons,
|
||||
});
|
||||
|
||||
this._view.addEventListener('change', e => this._evtChange(e));
|
||||
this._view.addEventListener('submit', e => this._evtUpdate(e));
|
||||
this._view.addEventListener('merge', e => this._evtMerge(e));
|
||||
this._view.addEventListener('delete', e => this._evtDelete(e));
|
||||
},
|
||||
error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
_evtChange(e) {
|
||||
misc.enableExitConfirmation();
|
||||
}
|
||||
|
||||
_evtSaved(e, section) {
|
||||
misc.disableExitConfirmation();
|
||||
if (this._name !== e.detail.pool.names[0]) {
|
||||
router.replace(
|
||||
uri.formatClientLink('pool', e.detail.pool.id, section),
|
||||
null, false);
|
||||
}
|
||||
}
|
||||
|
||||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
e.detail.pool.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
e.detail.pool.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
e.detail.pool.description = e.detail.description;
|
||||
}
|
||||
e.detail.pool.save().then(() => {
|
||||
this._view.showSuccess('Pool saved.');
|
||||
this._view.enableForm();
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
|
||||
_evtMerge(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
e.detail.pool
|
||||
.merge(e.detail.targetPoolName, e.detail.addAlias)
|
||||
.then(() => {
|
||||
this._view.showSuccess('Pool merged.');
|
||||
this._view.enableForm();
|
||||
router.replace(
|
||||
uri.formatClientLink(
|
||||
'pool', e.detail.targetPoolName, 'merge'),
|
||||
null, false);
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
|
||||
_evtDelete(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
e.detail.pool.delete()
|
||||
.then(() => {
|
||||
const ctx = router.show(uri.formatClientLink('pools'));
|
||||
ctx.controller.showSuccess('Pool deleted.');
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router => {
|
||||
router.enter(['pool', ':id', 'edit'], (ctx, next) => {
|
||||
ctx.controller = new PoolController(ctx, 'edit');
|
||||
});
|
||||
router.enter(['pool', ':id', 'merge'], (ctx, next) => {
|
||||
ctx.controller = new PoolController(ctx, 'merge');
|
||||
});
|
||||
router.enter(['pool', ':id', 'delete'], (ctx, next) => {
|
||||
ctx.controller = new PoolController(ctx, 'delete');
|
||||
});
|
||||
router.enter(['pool', ':id'], (ctx, next) => {
|
||||
ctx.controller = new PoolController(ctx, 'summary');
|
||||
});
|
||||
};
|
63
client/js/controllers/pool_create_controller.js
Normal file
63
client/js/controllers/pool_create_controller.js
Normal file
@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const PoolCategoryList = require('../models/pool_category_list.js');
|
||||
const PoolCreateView = require('../views/pool_create_view.js');
|
||||
const EmptyView = require('../views/empty_view.js');
|
||||
|
||||
class PoolCreateController {
|
||||
constructor(ctx) {
|
||||
if (!api.hasPrivilege('pools:create')) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError('You don\'t have privileges to create pools.');
|
||||
return;
|
||||
}
|
||||
|
||||
PoolCategoryList.get()
|
||||
.then(poolCategoriesResponse => {
|
||||
const categories = {};
|
||||
for (let category of poolCategoriesResponse.results) {
|
||||
categories[category.name] = category.name;
|
||||
}
|
||||
|
||||
this._view = new PoolCreateView({
|
||||
canCreate: api.hasPrivilege('pools:create'),
|
||||
categories: categories,
|
||||
escapeColons: uri.escapeColons,
|
||||
});
|
||||
|
||||
this._view.addEventListener('submit', e => this._evtCreate(e));
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
_evtCreate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
e.detail.pool.save()
|
||||
.then(() => {
|
||||
this._view.clearMessages();
|
||||
misc.disableExitConfirmation();
|
||||
const ctx = router.show(uri.formatClientLink('pools'));
|
||||
ctx.controller.showSuccess('Pool created.');
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
|
||||
_evtChange(e) {
|
||||
misc.enableExitConfirmation();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router => {
|
||||
router.enter(['pool', 'create'], (ctx, next) => {
|
||||
ctx.controller = new PoolCreateController(ctx, 'create');
|
||||
});
|
||||
};
|
105
client/js/controllers/pool_list_controller.js
Normal file
105
client/js/controllers/pool_list_controller.js
Normal file
@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('../router.js');
|
||||
const api = require('../api.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const PoolList = require('../models/pool_list.js');
|
||||
const topNavigation = require('../models/top_navigation.js');
|
||||
const PageController = require('../controllers/page_controller.js');
|
||||
const PoolsHeaderView = require('../views/pools_header_view.js');
|
||||
const PoolsPageView = require('../views/pools_page_view.js');
|
||||
const EmptyView = require('../views/empty_view.js');
|
||||
|
||||
const fields = [
|
||||
'id',
|
||||
'names',
|
||||
/* 'suggestions',
|
||||
* 'implications', */
|
||||
'creationTime',
|
||||
'postCount',
|
||||
'category'];
|
||||
|
||||
class PoolListController {
|
||||
constructor(ctx) {
|
||||
if (!api.hasPrivilege('pools:list')) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError('You don\'t have privileges to view pools.');
|
||||
return;
|
||||
}
|
||||
|
||||
topNavigation.activate('pools');
|
||||
topNavigation.setTitle('Listing pools');
|
||||
|
||||
this._ctx = ctx;
|
||||
this._pageController = new PageController();
|
||||
|
||||
this._headerView = new PoolsHeaderView({
|
||||
hostNode: this._pageController.view.pageHeaderHolderNode,
|
||||
parameters: ctx.parameters,
|
||||
canCreate: api.hasPrivilege('pools:create'),
|
||||
canEditPoolCategories: api.hasPrivilege('poolCategories:edit'),
|
||||
});
|
||||
this._headerView.addEventListener(
|
||||
'submit', e => this._evtSubmit(e),
|
||||
'navigate', e => this._evtNavigate(e));
|
||||
|
||||
this._syncPageController();
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
this._pageController.showSuccess(message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this._pageController.showError(message);
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
e.detail.pool.save()
|
||||
.then(() => {
|
||||
this._installView(e.detail.pool, 'edit');
|
||||
this._view.showSuccess('Pool created.');
|
||||
router.replace(
|
||||
uri.formatClientLink(
|
||||
'pool', e.detail.pool.id, 'edit'),
|
||||
null, false);
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
|
||||
_evtNavigate(e) {
|
||||
router.showNoDispatch(
|
||||
uri.formatClientLink('pools', e.detail.parameters));
|
||||
Object.assign(this._ctx.parameters, e.detail.parameters);
|
||||
this._syncPageController();
|
||||
}
|
||||
|
||||
_syncPageController() {
|
||||
this._pageController.run({
|
||||
parameters: this._ctx.parameters,
|
||||
defaultLimit: 50,
|
||||
getClientUrlForPage: (offset, limit) => {
|
||||
const parameters = Object.assign(
|
||||
{}, this._ctx.parameters, {offset: offset, limit: limit});
|
||||
return uri.formatClientLink('pools', parameters);
|
||||
},
|
||||
requestPage: (offset, limit) => {
|
||||
return PoolList.search(
|
||||
this._ctx.parameters.query, offset, limit, fields);
|
||||
},
|
||||
pageRenderer: pageCtx => {
|
||||
return new PoolsPageView(pageCtx);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router => {
|
||||
router.enter(
|
||||
['pools'],
|
||||
(ctx, next) => { ctx.controller = new PoolListController(ctx); });
|
||||
};
|
57
client/js/controls/pool_auto_complete_control.js
Normal file
57
client/js/controls/pool_auto_complete_control.js
Normal file
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
const misc = require('../util/misc.js');
|
||||
const PoolList = require('../models/pool_list.js');
|
||||
const AutoCompleteControl = require('./auto_complete_control.js');
|
||||
|
||||
function _poolListToMatches(pools, options) {
|
||||
return [...pools].sort((pool1, pool2) => {
|
||||
return pool2.postCount - pool1.postCount;
|
||||
}).map(pool => {
|
||||
let cssName = misc.makeCssName(pool.category, 'pool');
|
||||
// TODO
|
||||
if (options.isPooledWith(pool.id)) {
|
||||
cssName += ' disabled';
|
||||
}
|
||||
const caption = (
|
||||
'<span class="' + cssName + '">'
|
||||
+ misc.escapeHtml(pool.names[0] + ' (' + pool.postCount + ')')
|
||||
+ '</span>');
|
||||
return {
|
||||
caption: caption,
|
||||
value: pool,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
class PoolAutoCompleteControl extends AutoCompleteControl {
|
||||
constructor(input, options) {
|
||||
const minLengthForPartialSearch = 3;
|
||||
|
||||
options = Object.assign({
|
||||
isPooledWith: poolId => false,
|
||||
}, options);
|
||||
|
||||
options.getMatches = text => {
|
||||
const term = misc.escapeSearchTerm(text);
|
||||
const query = (
|
||||
text.length < minLengthForPartialSearch
|
||||
? term + '*'
|
||||
: '*' + term + '*') + ' sort:post-count';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
PoolList.search(
|
||||
query, 0, this._options.maxResults,
|
||||
['id', 'names', 'category', 'postCount'])
|
||||
.then(
|
||||
response => resolve(
|
||||
_poolListToMatches(response.results, this._options)),
|
||||
reject);
|
||||
});
|
||||
};
|
||||
|
||||
super(input, options);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PoolAutoCompleteControl;
|
@ -27,6 +27,7 @@ router.enter(
|
||||
});
|
||||
|
||||
const tags = require('./tags.js');
|
||||
const pools = require('./pools.js');
|
||||
const api = require('./api.js');
|
||||
|
||||
api.fetchConfig().then(() => {
|
||||
@ -45,6 +46,10 @@ api.fetchConfig().then(() => {
|
||||
controllers.push(require('./controllers/tag_controller.js'));
|
||||
controllers.push(require('./controllers/tag_list_controller.js'));
|
||||
controllers.push(require('./controllers/tag_categories_controller.js'));
|
||||
controllers.push(require('./controllers/pool_create_controller.js'));
|
||||
controllers.push(require('./controllers/pool_controller.js'));
|
||||
controllers.push(require('./controllers/pool_list_controller.js'));
|
||||
controllers.push(require('./controllers/pool_categories_controller.js'));
|
||||
controllers.push(require('./controllers/settings_controller.js'));
|
||||
controllers.push(require('./controllers/user_controller.js'));
|
||||
controllers.push(require('./controllers/user_list_controller.js'));
|
||||
@ -61,6 +66,7 @@ api.fetchConfig().then(() => {
|
||||
}).then(() => {
|
||||
api.loginFromCookies().then(() => {
|
||||
tags.refreshCategoryColorMap();
|
||||
pools.refreshCategoryColorMap();
|
||||
router.start();
|
||||
}, error => {
|
||||
if (window.location.href.indexOf('login') !== -1) {
|
||||
|
155
client/js/models/pool.js
Normal file
155
client/js/models/pool.js
Normal file
@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
|
||||
class Pool extends events.EventTarget {
|
||||
constructor() {
|
||||
// const PoolList = require('./pool_list.js');
|
||||
|
||||
super();
|
||||
this._orig = {};
|
||||
|
||||
for (let obj of [this, this._orig]) {
|
||||
// TODO
|
||||
// obj._suggestions = new PoolList();
|
||||
// obj._implications = new PoolList();
|
||||
}
|
||||
|
||||
this._updateFromResponse({});
|
||||
}
|
||||
|
||||
get id() { return this._id; }
|
||||
get names() { return this._names; }
|
||||
get category() { return this._category; }
|
||||
get description() { return this._description; }
|
||||
/* get suggestions() { return this._suggestions; }
|
||||
* get implications() { return this._implications; } */
|
||||
get postCount() { return this._postCount; }
|
||||
get creationTime() { return this._creationTime; }
|
||||
get lastEditTime() { return this._lastEditTime; }
|
||||
|
||||
set names(value) { this._names = value; }
|
||||
set category(value) { this._category = value; }
|
||||
set description(value) { this._description = value; }
|
||||
|
||||
static fromResponse(response) {
|
||||
const ret = new Pool();
|
||||
ret._updateFromResponse(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static get(id) {
|
||||
return api.get(uri.formatApiLink('pool', id))
|
||||
.then(response => {
|
||||
return Promise.resolve(Pool.fromResponse(response));
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
const detail = {version: this._version};
|
||||
|
||||
// send only changed fields to avoid user privilege violation
|
||||
if (misc.arraysDiffer(this._names, this._orig._names, true)) {
|
||||
detail.names = this._names;
|
||||
}
|
||||
if (this._category !== this._orig._category) {
|
||||
detail.category = this._category;
|
||||
}
|
||||
if (this._description !== this._orig._description) {
|
||||
detail.description = this._description;
|
||||
}
|
||||
// TODO
|
||||
// if (misc.arraysDiffer(this._implications, this._orig._implications)) {
|
||||
// detail.implications = this._implications.map(
|
||||
// relation => relation.names[0]);
|
||||
// }
|
||||
// if (misc.arraysDiffer(this._suggestions, this._orig._suggestions)) {
|
||||
// detail.suggestions = this._suggestions.map(
|
||||
// relation => relation.names[0]);
|
||||
// }
|
||||
|
||||
let promise = this._id ?
|
||||
api.put(uri.formatApiLink('pool', this._id), detail) :
|
||||
api.post(uri.formatApiLink('pools'), detail);
|
||||
return promise
|
||||
.then(response => {
|
||||
this._updateFromResponse(response);
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
pool: this,
|
||||
},
|
||||
}));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
merge(targetId, addAlias) {
|
||||
return api.get(uri.formatApiLink('pool', targetId))
|
||||
.then(response => {
|
||||
return api.post(uri.formatApiLink('pool-merge'), {
|
||||
removeVersion: this._version,
|
||||
remove: this._id,
|
||||
mergeToVersion: response.version,
|
||||
mergeTo: targetId,
|
||||
});
|
||||
}).then(response => {
|
||||
if (!addAlias) {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
return api.put(uri.formatApiLink('pool', targetId), {
|
||||
version: response.version,
|
||||
names: response.names.concat(this._names),
|
||||
});
|
||||
}).then(response => {
|
||||
this._updateFromResponse(response);
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
pool: this,
|
||||
},
|
||||
}));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
return api.delete(
|
||||
uri.formatApiLink('pool', this._id),
|
||||
{version: this._version})
|
||||
.then(response => {
|
||||
this.dispatchEvent(new CustomEvent('delete', {
|
||||
detail: {
|
||||
pool: this,
|
||||
},
|
||||
}));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
const map = {
|
||||
_id: response.id,
|
||||
_version: response.version,
|
||||
_origName: response.names ? response.names[0] : null,
|
||||
_names: response.names,
|
||||
_category: response.category,
|
||||
_description: response.description,
|
||||
_creationTime: response.creationTime,
|
||||
_lastEditTime: response.lastEditTime,
|
||||
_postCount: response.usages || 0,
|
||||
};
|
||||
|
||||
for (let obj of [this, this._orig]) {
|
||||
// TODO
|
||||
// obj._suggestions.sync(response.suggestions);
|
||||
// obj._implications.sync(response.implications);
|
||||
}
|
||||
|
||||
Object.assign(this, map);
|
||||
Object.assign(this._orig, map);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Pool;
|
90
client/js/models/pool_category.js
Normal file
90
client/js/models/pool_category.js
Normal file
@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const events = require('../events.js');
|
||||
|
||||
class PoolCategory extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._name = '';
|
||||
this._color = '#000000';
|
||||
this._poolCount = 0;
|
||||
this._isDefault = false;
|
||||
this._origName = null;
|
||||
this._origColor = null;
|
||||
}
|
||||
|
||||
get name() { return this._name; }
|
||||
get color() { return this._color; }
|
||||
get poolCount() { return this._poolCount; }
|
||||
get isDefault() { return this._isDefault; }
|
||||
get isTransient() { return !this._origName; }
|
||||
|
||||
set name(value) { this._name = value; }
|
||||
set color(value) { this._color = value; }
|
||||
|
||||
static fromResponse(response) {
|
||||
const ret = new PoolCategory();
|
||||
ret._updateFromResponse(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
save() {
|
||||
const detail = {version: this._version};
|
||||
|
||||
if (this.name !== this._origName) {
|
||||
detail.name = this.name;
|
||||
}
|
||||
if (this.color !== this._origColor) {
|
||||
detail.color = this.color;
|
||||
}
|
||||
|
||||
if (!Object.keys(detail).length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let promise = this._origName ?
|
||||
api.put(
|
||||
uri.formatApiLink('pool-category', this._origName),
|
||||
detail) :
|
||||
api.post(uri.formatApiLink('pool-categories'), detail);
|
||||
|
||||
return promise
|
||||
.then(response => {
|
||||
this._updateFromResponse(response);
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
detail: {
|
||||
poolCategory: this,
|
||||
},
|
||||
}));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
return api.delete(
|
||||
uri.formatApiLink('pool-category', this._origName),
|
||||
{version: this._version})
|
||||
.then(response => {
|
||||
this.dispatchEvent(new CustomEvent('delete', {
|
||||
detail: {
|
||||
poolCategory: this,
|
||||
},
|
||||
}));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
this._version = response.version;
|
||||
this._name = response.name;
|
||||
this._color = response.color;
|
||||
this._isDefault = response.default;
|
||||
this._poolCount = response.usages;
|
||||
this._origName = this.name;
|
||||
this._origColor = this.color;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolCategory;
|
82
client/js/models/pool_category_list.js
Normal file
82
client/js/models/pool_category_list.js
Normal file
@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const AbstractList = require('./abstract_list.js');
|
||||
const PoolCategory = require('./pool_category.js');
|
||||
|
||||
class PoolCategoryList extends AbstractList {
|
||||
constructor() {
|
||||
super();
|
||||
this._defaultCategory = null;
|
||||
this._origDefaultCategory = null;
|
||||
this._deletedCategories = [];
|
||||
this.addEventListener('remove', e => this._evtCategoryDeleted(e));
|
||||
}
|
||||
|
||||
static fromResponse(response) {
|
||||
const ret = super.fromResponse(response);
|
||||
ret._defaultCategory = null;
|
||||
for (let poolCategory of ret) {
|
||||
if (poolCategory.isDefault) {
|
||||
ret._defaultCategory = poolCategory;
|
||||
}
|
||||
}
|
||||
ret._origDefaultCategory = ret._defaultCategory;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static get() {
|
||||
return api.get(uri.formatApiLink('pool-categories'))
|
||||
.then(response => {
|
||||
return Promise.resolve(Object.assign(
|
||||
{},
|
||||
response,
|
||||
{results: PoolCategoryList.fromResponse(response.results)}));
|
||||
});
|
||||
}
|
||||
|
||||
get defaultCategory() {
|
||||
return this._defaultCategory;
|
||||
}
|
||||
|
||||
set defaultCategory(poolCategory) {
|
||||
this._defaultCategory = poolCategory;
|
||||
}
|
||||
|
||||
save() {
|
||||
let promises = [];
|
||||
for (let poolCategory of this) {
|
||||
promises.push(poolCategory.save());
|
||||
}
|
||||
for (let poolCategory of this._deletedCategories) {
|
||||
promises.push(poolCategory.delete());
|
||||
}
|
||||
|
||||
if (this._defaultCategory !== this._origDefaultCategory) {
|
||||
promises.push(
|
||||
api.put(
|
||||
uri.formatApiLink(
|
||||
'pool-category',
|
||||
this._defaultCategory.name,
|
||||
'default')));
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(response => {
|
||||
this._deletedCategories = [];
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
_evtCategoryDeleted(e) {
|
||||
if (!e.detail.poolCategory.isTransient) {
|
||||
this._deletedCategories.push(e.detail.poolCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PoolCategoryList._itemClass = PoolCategory;
|
||||
PoolCategoryList._itemName = 'poolCategory';
|
||||
|
||||
module.exports = PoolCategoryList;
|
30
client/js/models/pool_list.js
Normal file
30
client/js/models/pool_list.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const AbstractList = require('./abstract_list.js');
|
||||
const Pool = require('./pool.js');
|
||||
|
||||
class PoolList extends AbstractList {
|
||||
static search(text, offset, limit, fields) {
|
||||
return api.get(
|
||||
uri.formatApiLink(
|
||||
'pools', {
|
||||
query: text,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fields: fields.join(','),
|
||||
}))
|
||||
.then(response => {
|
||||
return Promise.resolve(Object.assign(
|
||||
{},
|
||||
response,
|
||||
{results: PoolList.fromResponse(response.results)}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PoolList._itemClass = Pool;
|
||||
PoolList._itemName = 'pool';
|
||||
|
||||
module.exports = PoolList;
|
@ -81,6 +81,7 @@ function _makeTopNavigation() {
|
||||
ret.add('upload', new TopNavigationItem('U', 'Upload', 'upload'));
|
||||
ret.add('comments', new TopNavigationItem('C', 'Comments', 'comments'));
|
||||
ret.add('tags', new TopNavigationItem('T', 'Tags', 'tags'));
|
||||
ret.add('pools', new TopNavigationItem('O', 'Pools', 'pools'));
|
||||
ret.add('users', new TopNavigationItem('S', 'Users', 'users'));
|
||||
ret.add('account', new TopNavigationItem('A', 'Account', 'user/{me}'));
|
||||
ret.add('register', new TopNavigationItem('R', 'Register', 'register'));
|
||||
|
26
client/js/pools.js
Normal file
26
client/js/pools.js
Normal file
@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const misc = require('./util/misc.js');
|
||||
const PoolCategoryList = require('./models/pool_category_list.js');
|
||||
|
||||
let _stylesheet = null;
|
||||
|
||||
function refreshCategoryColorMap() {
|
||||
return PoolCategoryList.get().then(response => {
|
||||
if (_stylesheet) {
|
||||
document.head.removeChild(_stylesheet);
|
||||
}
|
||||
_stylesheet = document.createElement('style');
|
||||
document.head.appendChild(_stylesheet);
|
||||
for (let category of response.results) {
|
||||
const ruleName = misc.makeCssName(category.name, 'pool');
|
||||
_stylesheet.sheet.insertRule(
|
||||
`.${ruleName} { color: ${category.color} }`,
|
||||
_stylesheet.sheet.cssRules.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refreshCategoryColorMap: refreshCategoryColorMap,
|
||||
};
|
@ -163,6 +163,11 @@ function escapeHtml(unsafe) {
|
||||
}
|
||||
|
||||
function arraysDiffer(source1, source2, orderImportant) {
|
||||
if ((source1 instanceof Array && source2 === undefined)
|
||||
|| (source1 === undefined && source2 instanceof Array)) {
|
||||
return true
|
||||
}
|
||||
|
||||
source1 = [...source1];
|
||||
source2 = [...source2];
|
||||
if (orderImportant === true) {
|
||||
|
@ -221,6 +221,29 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
misc.escapeHtml(text));
|
||||
}
|
||||
|
||||
function makePoolLink(pool, includeHash, includeCount, name) {
|
||||
const category = pool.category;
|
||||
let text = name ? name : pool.names[0];
|
||||
if (includeHash === true) {
|
||||
text = '#' + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += ' (' + pool.postCount + ')';
|
||||
}
|
||||
return api.hasPrivilege('pools:view') ?
|
||||
makeElement(
|
||||
'a',
|
||||
{
|
||||
href: uri.formatClientLink('pool', pool.id),
|
||||
class: misc.makeCssName(category, 'pool'),
|
||||
},
|
||||
misc.escapeHtml(text)) :
|
||||
makeElement(
|
||||
'span',
|
||||
{class: misc.makeCssName(category, 'pool')},
|
||||
misc.escapeHtml(text));
|
||||
}
|
||||
|
||||
function makeUserLink(user) {
|
||||
let text = makeThumbnail(user ? user.avatarUrl : null);
|
||||
text += user && user.name ? misc.escapeHtml(user.name) : 'Anonymous';
|
||||
@ -400,6 +423,7 @@ function getTemplate(templatePath) {
|
||||
makeDateInput: makeDateInput,
|
||||
makePostLink: makePostLink,
|
||||
makeTagLink: makeTagLink,
|
||||
makePoolLink: makePoolLink,
|
||||
makeUserLink: makeUserLink,
|
||||
makeFlexboxAlign: makeFlexboxAlign,
|
||||
makeAccessKey: makeAccessKey,
|
||||
@ -529,6 +553,7 @@ module.exports = {
|
||||
decorateValidator: decorateValidator,
|
||||
makeTagLink: makeTagLink,
|
||||
makePostLink: makePostLink,
|
||||
makePoolLink: makePoolLink,
|
||||
makeCheckbox: makeCheckbox,
|
||||
makeRadio: makeRadio,
|
||||
syncScrollPosition: syncScrollPosition,
|
||||
|
@ -17,6 +17,7 @@ const subsectionTemplates = {
|
||||
'posts': views.getTemplate('help-search-posts'),
|
||||
'users': views.getTemplate('help-search-users'),
|
||||
'tags': views.getTemplate('help-search-tags'),
|
||||
'pools': views.getTemplate('help-search-pools'),
|
||||
},
|
||||
};
|
||||
|
||||
|
166
client/js/views/pool_categories_view.js
Normal file
166
client/js/views/pool_categories_view.js
Normal file
@ -0,0 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const PoolCategory = require('../models/pool_category.js');
|
||||
|
||||
const template = views.getTemplate('pool-categories');
|
||||
const rowTemplate = views.getTemplate('pool-category-row');
|
||||
|
||||
class PoolCategoriesView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
this._ctx = ctx;
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
views.syncScrollPosition();
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
const categoriesToAdd = Array.from(ctx.poolCategories);
|
||||
categoriesToAdd.sort((a, b) => {
|
||||
if (b.isDefault) {
|
||||
return 1;
|
||||
} else if (a.isDefault) {
|
||||
return -1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
for (let poolCategory of categoriesToAdd) {
|
||||
this._addPoolCategoryRowNode(poolCategory);
|
||||
}
|
||||
|
||||
if (this._addLinkNode) {
|
||||
this._addLinkNode.addEventListener(
|
||||
'click', e => this._evtAddButtonClick(e));
|
||||
}
|
||||
|
||||
ctx.poolCategories.addEventListener(
|
||||
'add', e => this._evtPoolCategoryAdded(e));
|
||||
|
||||
ctx.poolCategories.addEventListener(
|
||||
'remove', e => this._evtPoolCategoryDeleted(e));
|
||||
|
||||
this._formNode.addEventListener(
|
||||
'submit', e => this._evtSaveButtonClick(e, ctx));
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _tableBodyNode() {
|
||||
return this._hostNode.querySelector('tbody');
|
||||
}
|
||||
|
||||
get _addLinkNode() {
|
||||
return this._hostNode.querySelector('a.add');
|
||||
}
|
||||
|
||||
_addPoolCategoryRowNode(poolCategory) {
|
||||
const rowNode = rowTemplate(
|
||||
Object.assign(
|
||||
{}, this._ctx, {poolCategory: poolCategory}));
|
||||
|
||||
const nameInput = rowNode.querySelector('.name input');
|
||||
if (nameInput) {
|
||||
nameInput.addEventListener(
|
||||
'change', e => this._evtNameChange(e, rowNode));
|
||||
}
|
||||
|
||||
const colorInput = rowNode.querySelector('.color input');
|
||||
if (colorInput) {
|
||||
colorInput.addEventListener(
|
||||
'change', e => this._evtColorChange(e, rowNode));
|
||||
}
|
||||
|
||||
const removeLinkNode = rowNode.querySelector('.remove a');
|
||||
if (removeLinkNode) {
|
||||
removeLinkNode.addEventListener(
|
||||
'click', e => this._evtDeleteButtonClick(e, rowNode));
|
||||
}
|
||||
|
||||
const defaultLinkNode = rowNode.querySelector('.set-default a');
|
||||
if (defaultLinkNode) {
|
||||
defaultLinkNode.addEventListener(
|
||||
'click', e => this._evtSetDefaultButtonClick(e, rowNode));
|
||||
}
|
||||
|
||||
this._tableBodyNode.appendChild(rowNode);
|
||||
|
||||
rowNode._poolCategory = poolCategory;
|
||||
poolCategory._rowNode = rowNode;
|
||||
}
|
||||
|
||||
_removePoolCategoryRowNode(poolCategory) {
|
||||
const rowNode = poolCategory._rowNode;
|
||||
rowNode.parentNode.removeChild(rowNode);
|
||||
}
|
||||
|
||||
_evtPoolCategoryAdded(e) {
|
||||
this._addPoolCategoryRowNode(e.detail.poolCategory);
|
||||
}
|
||||
|
||||
_evtPoolCategoryDeleted(e) {
|
||||
this._removePoolCategoryRowNode(e.detail.poolCategory);
|
||||
}
|
||||
|
||||
_evtAddButtonClick(e) {
|
||||
e.preventDefault();
|
||||
this._ctx.poolCategories.add(new PoolCategory());
|
||||
}
|
||||
|
||||
_evtNameChange(e, rowNode) {
|
||||
rowNode._poolCategory.name = e.target.value;
|
||||
}
|
||||
|
||||
_evtColorChange(e, rowNode) {
|
||||
e.target.value = e.target.value.toLowerCase();
|
||||
rowNode._poolCategory.color = e.target.value;
|
||||
}
|
||||
|
||||
_evtDeleteButtonClick(e, rowNode, link) {
|
||||
e.preventDefault();
|
||||
if (e.target.classList.contains('inactive')) {
|
||||
return;
|
||||
}
|
||||
this._ctx.poolCategories.remove(rowNode._poolCategory);
|
||||
}
|
||||
|
||||
_evtSetDefaultButtonClick(e, rowNode) {
|
||||
e.preventDefault();
|
||||
this._ctx.poolCategories.defaultCategory = rowNode._poolCategory;
|
||||
const oldRowNode = rowNode.parentNode.querySelector('tr.default');
|
||||
if (oldRowNode) {
|
||||
oldRowNode.classList.remove('default');
|
||||
}
|
||||
rowNode.classList.add('default');
|
||||
}
|
||||
|
||||
_evtSaveButtonClick(e, ctx) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolCategoriesView;
|
108
client/js/views/pool_create_view.js
Normal file
108
client/js/views/pool_create_view.js
Normal file
@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const api = require('../api.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const Pool = require('../models/pool.js')
|
||||
|
||||
const template = views.getTemplate('pool-create');
|
||||
|
||||
class PoolCreateView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
if (this._namesFieldNode) {
|
||||
this._namesFieldNode.addEventListener(
|
||||
'input', e => this._evtNameInput(e));
|
||||
}
|
||||
|
||||
for (let node of this._formNode.querySelectorAll(
|
||||
'input, select, textarea')) {
|
||||
node.addEventListener(
|
||||
'change', e => {
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
}
|
||||
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
_evtNameInput(e) {
|
||||
const regex = new RegExp(api.getPoolNameRegex());
|
||||
const list = misc.splitByWhitespace(this._namesFieldNode.value);
|
||||
|
||||
if (!list.length) {
|
||||
this._namesFieldNode.setCustomValidity(
|
||||
'Pools must have at least one name.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of list) {
|
||||
if (!regex.test(item)) {
|
||||
this._namesFieldNode.setCustomValidity(
|
||||
`Pool name "${item}" contains invalid symbols.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._namesFieldNode.setCustomValidity('');
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
let pool = new Pool()
|
||||
pool.names = misc.splitByWhitespace(this._namesFieldNode.value);
|
||||
pool.category = this._categoryFieldNode.value;
|
||||
pool.description = this._descriptionFieldNode.value;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
pool: pool,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _namesFieldNode() {
|
||||
return this._formNode.querySelector('.names input');
|
||||
}
|
||||
|
||||
get _categoryFieldNode() {
|
||||
return this._formNode.querySelector('.category select');
|
||||
}
|
||||
|
||||
get _descriptionFieldNode() {
|
||||
return this._formNode.querySelector('.description textarea');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolCreateView;
|
53
client/js/views/pool_delete_view.js
Normal file
53
client/js/views/pool_delete_view.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('pool-delete');
|
||||
|
||||
class PoolDeleteView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._hostNode = ctx.hostNode;
|
||||
this._pool = ctx.pool;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
views.decorateValidator(this._formNode);
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
pool: this._pool,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolDeleteView;
|
115
client/js/views/pool_edit_view.js
Normal file
115
client/js/views/pool_edit_view.js
Normal file
@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const api = require('../api.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('pool-edit');
|
||||
|
||||
class PoolEditView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._pool = ctx.pool;
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
if (this._namesFieldNode) {
|
||||
this._namesFieldNode.addEventListener(
|
||||
'input', e => this._evtNameInput(e));
|
||||
}
|
||||
|
||||
for (let node of this._formNode.querySelectorAll(
|
||||
'input, select, textarea')) {
|
||||
node.addEventListener(
|
||||
'change', e => {
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
}
|
||||
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
_evtNameInput(e) {
|
||||
const regex = new RegExp(api.getPoolNameRegex());
|
||||
const list = misc.splitByWhitespace(this._namesFieldNode.value);
|
||||
|
||||
if (!list.length) {
|
||||
this._namesFieldNode.setCustomValidity(
|
||||
'Pools must have at least one name.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of list) {
|
||||
if (!regex.test(item)) {
|
||||
this._namesFieldNode.setCustomValidity(
|
||||
`Pool name "${item}" contains invalid symbols.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._namesFieldNode.setCustomValidity('');
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
pool: this._pool,
|
||||
|
||||
names: this._namesFieldNode ?
|
||||
misc.splitByWhitespace(this._namesFieldNode.value) :
|
||||
undefined,
|
||||
|
||||
category: this._categoryFieldNode ?
|
||||
this._categoryFieldNode.value :
|
||||
undefined,
|
||||
|
||||
description: this._descriptionFieldNode ?
|
||||
this._descriptionFieldNode.value :
|
||||
undefined,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _namesFieldNode() {
|
||||
return this._formNode.querySelector('.names input');
|
||||
}
|
||||
|
||||
get _categoryFieldNode() {
|
||||
return this._formNode.querySelector('.category select');
|
||||
}
|
||||
|
||||
get _descriptionFieldNode() {
|
||||
return this._formNode.querySelector('.description textarea');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolEditView;
|
77
client/js/views/pool_merge_view.js
Normal file
77
client/js/views/pool_merge_view.js
Normal file
@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const api = require('../api.js');
|
||||
const views = require('../util/views.js');
|
||||
const PoolAutoCompleteControl =
|
||||
require('../controls/pool_auto_complete_control.js');
|
||||
|
||||
const template = views.getTemplate('pool-merge');
|
||||
|
||||
class PoolMergeView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._pool = ctx.pool;
|
||||
this._hostNode = ctx.hostNode;
|
||||
ctx.poolNamePattern = api.getPoolNameRegex();
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
views.decorateValidator(this._formNode);
|
||||
if (this._targetPoolFieldNode) {
|
||||
this._autoCompleteControl = new PoolAutoCompleteControl(
|
||||
this._targetPoolFieldNode,
|
||||
{
|
||||
confirm: pool =>
|
||||
this._autoCompleteControl.replaceSelectedText(
|
||||
pool.names[0], false),
|
||||
});
|
||||
}
|
||||
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
pool: this._pool,
|
||||
targetPoolName: this._targetPoolFieldNode.value
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _targetPoolFieldNode() {
|
||||
return this._formNode.querySelector('input[name=target-pool]');
|
||||
}
|
||||
|
||||
get _addAliasCheckboxNode() {
|
||||
return this._formNode.querySelector('input[name=alias]');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolMergeView;
|
23
client/js/views/pool_summary_view.js
Normal file
23
client/js/views/pool_summary_view.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('pool-summary');
|
||||
|
||||
class PoolSummaryView {
|
||||
constructor(ctx) {
|
||||
this._pool = ctx.pool;
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolSummaryView;
|
106
client/js/views/pool_view.js
Normal file
106
client/js/views/pool_view.js
Normal file
@ -0,0 +1,106 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const PoolSummaryView = require('./pool_summary_view.js');
|
||||
const PoolEditView = require('./pool_edit_view.js');
|
||||
const PoolMergeView = require('./pool_merge_view.js');
|
||||
const PoolDeleteView = require('./pool_delete_view.js');
|
||||
const EmptyView = require('../views/empty_view.js');
|
||||
|
||||
const template = views.getTemplate('pool');
|
||||
|
||||
class PoolView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._ctx = ctx;
|
||||
ctx.pool.addEventListener('change', e => this._evtChange(e));
|
||||
ctx.section = ctx.section || 'summary';
|
||||
ctx.getPrettyPoolName = misc.getPrettyPoolName;
|
||||
|
||||
this._hostNode = document.getElementById('content-holder');
|
||||
this._install();
|
||||
}
|
||||
|
||||
_install() {
|
||||
const ctx = this._ctx;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
for (let item of this._hostNode.querySelectorAll('[data-name]')) {
|
||||
item.classList.toggle(
|
||||
'active', item.getAttribute('data-name') === ctx.section);
|
||||
if (item.getAttribute('data-name') === ctx.section) {
|
||||
item.parentNode.scrollLeft =
|
||||
item.getBoundingClientRect().left -
|
||||
item.parentNode.getBoundingClientRect().left
|
||||
}
|
||||
}
|
||||
|
||||
ctx.hostNode = this._hostNode.querySelector('.pool-content-holder');
|
||||
if (ctx.section === 'edit') {
|
||||
if (!this._ctx.canEditAnything) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(
|
||||
'You don\'t have privileges to edit pools.');
|
||||
} else {
|
||||
this._view = new PoolEditView(ctx);
|
||||
events.proxyEvent(this._view, this, 'submit');
|
||||
}
|
||||
|
||||
} else if (ctx.section === 'merge') {
|
||||
if (!this._ctx.canMerge) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(
|
||||
'You don\'t have privileges to merge pools.');
|
||||
} else {
|
||||
this._view = new PoolMergeView(ctx);
|
||||
events.proxyEvent(this._view, this, 'submit', 'merge');
|
||||
}
|
||||
|
||||
} else if (ctx.section === 'delete') {
|
||||
if (!this._ctx.canDelete) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(
|
||||
'You don\'t have privileges to delete pools.');
|
||||
} else {
|
||||
this._view = new PoolDeleteView(ctx);
|
||||
events.proxyEvent(this._view, this, 'submit', 'delete');
|
||||
}
|
||||
|
||||
} else {
|
||||
this._view = new PoolSummaryView(ctx);
|
||||
}
|
||||
|
||||
events.proxyEvent(this._view, this, 'change');
|
||||
views.syncScrollPosition();
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
this._view.clearMessages();
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
this._view.enableForm();
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
this._view.disableForm();
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
this._view.showSuccess(message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this._view.showError(message);
|
||||
}
|
||||
|
||||
_evtChange(e) {
|
||||
this._ctx.pool = e.detail.pool;
|
||||
this._install(this._ctx);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolView;
|
52
client/js/views/pools_header_view.js
Normal file
52
client/js/views/pools_header_view.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const search = require('../util/search.js');
|
||||
const views = require('../util/views.js');
|
||||
const PoolAutoCompleteControl =
|
||||
require('../controls/pool_auto_complete_control.js');
|
||||
|
||||
const template = views.getTemplate('pools-header');
|
||||
|
||||
class PoolsHeaderView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
||||
this._hostNode = ctx.hostNode;
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
|
||||
if (this._queryInputNode) {
|
||||
this._autoCompleteControl = new PoolAutoCompleteControl(
|
||||
this._queryInputNode,
|
||||
{
|
||||
confirm: pool =>
|
||||
this._autoCompleteControl.replaceSelectedText(
|
||||
misc.escapeSearchTerm(pool.names[0]), true),
|
||||
});
|
||||
}
|
||||
|
||||
search.searchInputNodeFocusHelper(this._queryInputNode);
|
||||
|
||||
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector('form');
|
||||
}
|
||||
|
||||
get _queryInputNode() {
|
||||
return this._hostNode.querySelector('[name=search-text]');
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
e.preventDefault();
|
||||
this._queryInputNode.blur();
|
||||
this.dispatchEvent(new CustomEvent('navigate', {detail: {parameters: {
|
||||
query: this._queryInputNode.value,
|
||||
page: 1,
|
||||
}}}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolsHeaderView;
|
13
client/js/views/pools_page_view.js
Normal file
13
client/js/views/pools_page_view.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const views = require('../util/views.js');
|
||||
|
||||
const template = views.getTemplate('pools-page');
|
||||
|
||||
class PoolsPageView {
|
||||
constructor(ctx) {
|
||||
views.replaceContent(ctx.hostNode, template(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolsPageView;
|
@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --no-vendor-js;c1=$c2;sleep 1;done"
|
||||
"watch": "c1=\"\";while :;do c2=$(find html js css img -type f -and -not -iname '*autogen*'|sort|xargs cat|md5sum);[[ $c1 != $c2 ]]&&npm run build -- --debug --no-vendor-js;c1=$c2;sleep 1;done"
|
||||
},
|
||||
"dependencies": {
|
||||
"font-awesome": "^4.7.0",
|
||||
|
Reference in New Issue
Block a user