Add pool input box in post details
This commit is contained in:
parent
e6bf102bc0
commit
8795279a73
11 changed files with 337 additions and 6 deletions
|
@ -0,0 +1,8 @@
|
|||
<div class='pool-input'>
|
||||
<div class='main-control'>
|
||||
<input type='text' placeholder='type to add…'/>
|
||||
<!-- <button>Add</button> -->
|
||||
</div>
|
||||
|
||||
<ul class='compact-pools'></ul>
|
||||
</div>
|
|
@ -87,6 +87,12 @@
|
|||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPoolPosts) { %>
|
||||
<section class='pools'>
|
||||
<%= ctx.makeTextInput({}) %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPostContent) { %>
|
||||
<section class='post-content'>
|
||||
<label>Content</label>
|
||||
|
|
230
client/js/controls/pool_input_control.js
Normal file
230
client/js/controls/pool_input_control.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
'use strict';
|
||||
|
||||
const api = require('../api.js');
|
||||
const pools = require('../pools.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const uri = require('../util/uri.js');
|
||||
const Pool = require('../models/pool.js');
|
||||
const settings = require('../models/settings.js');
|
||||
const events = require('../events.js');
|
||||
const views = require('../util/views.js');
|
||||
const PoolAutoCompleteControl = require('./pool_auto_complete_control.js');
|
||||
|
||||
const KEY_SPACE = 32;
|
||||
const KEY_RETURN = 13;
|
||||
|
||||
const SOURCE_INIT = 'init';
|
||||
const SOURCE_IMPLICATION = 'implication';
|
||||
const SOURCE_USER_INPUT = 'user-input';
|
||||
const SOURCE_CLIPBOARD = 'clipboard';
|
||||
|
||||
const template = views.getTemplate('pool-input');
|
||||
|
||||
function _fadeOutListItemNodeStatus(listItemNode) {
|
||||
if (listItemNode.classList.length) {
|
||||
if (listItemNode.fadeTimeout) {
|
||||
window.clearTimeout(listItemNode.fadeTimeout);
|
||||
}
|
||||
listItemNode.fadeTimeout = window.setTimeout(() => {
|
||||
while (listItemNode.classList.length) {
|
||||
listItemNode.classList.remove(
|
||||
listItemNode.classList.item(0));
|
||||
}
|
||||
listItemNode.fadeTimeout = null;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
class PoolInputControl extends events.EventTarget {
|
||||
constructor(hostNode, poolList) {
|
||||
super();
|
||||
this.pools = poolList;
|
||||
this._hostNode = hostNode;
|
||||
this._poolToListItemNode = new Map();
|
||||
|
||||
// dom
|
||||
const editAreaNode = template();
|
||||
this._editAreaNode = editAreaNode;
|
||||
this._poolInputNode = editAreaNode.querySelector('input');
|
||||
this._poolListNode = editAreaNode.querySelector('ul.compact-pools');
|
||||
|
||||
this._autoCompleteControl = new PoolAutoCompleteControl(
|
||||
this._poolInputNode, {
|
||||
getTextToFind: () => {
|
||||
return this._poolInputNode.value;
|
||||
},
|
||||
confirm: pool => {
|
||||
this._poolInputNode.value = '';
|
||||
this.addPool(pool, SOURCE_USER_INPUT);
|
||||
},
|
||||
delete: pool => {
|
||||
this._poolInputNode.value = '';
|
||||
this.deletePool(pool);
|
||||
},
|
||||
verticalShift: -2
|
||||
});
|
||||
|
||||
// dom events
|
||||
this._poolInputNode.addEventListener(
|
||||
'keydown', e => this._evtInputKeyDown(e));
|
||||
|
||||
// show
|
||||
this._hostNode.style.display = 'none';
|
||||
this._hostNode.parentNode.insertBefore(
|
||||
this._editAreaNode, hostNode.nextSibling);
|
||||
|
||||
// add existing pools
|
||||
for (let pool of [...this.pools]) {
|
||||
const listItemNode = this._createListItemNode(pool);
|
||||
this._poolListNode.appendChild(listItemNode);
|
||||
}
|
||||
}
|
||||
|
||||
addPoolByText(text, source) {
|
||||
for (let poolName of text.split(/\s+/).filter(word => word).reverse()) {
|
||||
this.addPoolByName(poolName, source);
|
||||
}
|
||||
}
|
||||
|
||||
addPoolByName(name, source) {
|
||||
name = name.trim();
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
return Pool.get(name).then(pool => {
|
||||
return this.addPool(pool, source);
|
||||
}, () => {
|
||||
const pool = new Pool();
|
||||
pool.names = [name];
|
||||
pool.category = null;
|
||||
return this.addPool(pool, source);
|
||||
});
|
||||
}
|
||||
|
||||
addPool(pool, source) {
|
||||
if (source != SOURCE_INIT && this.pools.hasPoolId(pool.id)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.pools.add(pool, false)
|
||||
|
||||
const listItemNode = this._createListItemNode(pool);
|
||||
if (!pool.category) {
|
||||
listItemNode.classList.add('new');
|
||||
}
|
||||
this._poolListNode.prependChild(listItemNode);
|
||||
_fadeOutListItemNodeStatus(listItemNode);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('add', {
|
||||
detail: {pool: pool, source: source},
|
||||
}));
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
deletePool(pool) {
|
||||
if (!this.pools.hasPoolId(pool.id)) {
|
||||
return;
|
||||
}
|
||||
this.pools.removeById(pool.id);
|
||||
this._hideAutoComplete();
|
||||
|
||||
this._deleteListItemNode(pool);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('remove', {
|
||||
detail: {pool: pool},
|
||||
}));
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
}
|
||||
|
||||
_evtAddPoolButtonClick(e) {
|
||||
// TODO
|
||||
// e.preventDefault();
|
||||
// this.addPoolByName(this._poolInputNode.value, SOURCE_USER_INPUT);
|
||||
// this._poolInputNode.value = '';
|
||||
}
|
||||
|
||||
_evtInputKeyDown(e) {
|
||||
// TODO
|
||||
if (e.which == KEY_RETURN || e.which == KEY_SPACE) {
|
||||
e.preventDefault();
|
||||
// this._hideAutoComplete();
|
||||
// this.addPoolByText(this._poolInputNode.value, SOURCE_USER_INPUT);
|
||||
// this._poolInputNode.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
_createListItemNode(pool) {
|
||||
const className = pool.category ?
|
||||
misc.makeCssName(pool.category, 'pool') :
|
||||
null;
|
||||
|
||||
const poolLinkNode = document.createElement('a');
|
||||
if (className) {
|
||||
poolLinkNode.classList.add(className);
|
||||
}
|
||||
poolLinkNode.setAttribute(
|
||||
'href', uri.formatClientLink('pool', pool.names[0]));
|
||||
|
||||
const poolIconNode = document.createElement('i');
|
||||
poolIconNode.classList.add('fa');
|
||||
poolIconNode.classList.add('fa-pool');
|
||||
poolLinkNode.appendChild(poolIconNode);
|
||||
|
||||
const searchLinkNode = document.createElement('a');
|
||||
if (className) {
|
||||
searchLinkNode.classList.add(className);
|
||||
}
|
||||
searchLinkNode.setAttribute(
|
||||
'href', uri.formatClientLink(
|
||||
'posts', {query: uri.escapeColons(pool.names[0])}));
|
||||
searchLinkNode.textContent = pool.names[0] + ' ';
|
||||
searchLinkNode.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const usagesNode = document.createElement('span');
|
||||
usagesNode.classList.add('pool-usages');
|
||||
usagesNode.setAttribute('data-pseudo-content', pool.postCount);
|
||||
|
||||
const removalLinkNode = document.createElement('a');
|
||||
removalLinkNode.classList.add('remove-pool');
|
||||
removalLinkNode.setAttribute('href', '');
|
||||
removalLinkNode.setAttribute('data-pseudo-content', '×');
|
||||
removalLinkNode.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
this.deletePool(pool);
|
||||
});
|
||||
|
||||
const listItemNode = document.createElement('li');
|
||||
listItemNode.appendChild(removalLinkNode);
|
||||
listItemNode.appendChild(poolLinkNode);
|
||||
listItemNode.appendChild(searchLinkNode);
|
||||
listItemNode.appendChild(usagesNode);
|
||||
for (let name of pool.names) {
|
||||
this._poolToListItemNode.set(name, listItemNode);
|
||||
}
|
||||
return listItemNode;
|
||||
}
|
||||
|
||||
_deleteListItemNode(pool) {
|
||||
const listItemNode = this._getListItemNode(pool);
|
||||
if (listItemNode) {
|
||||
listItemNode.parentNode.removeChild(listItemNode);
|
||||
}
|
||||
for (let name of pool.names) {
|
||||
this._poolToListItemNode.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
_getListItemNode(pool) {
|
||||
return this._poolToListItemNode.get(pool.names[0]);
|
||||
}
|
||||
|
||||
_hideAutoComplete() {
|
||||
this._autoCompleteControl.hide();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolInputControl;
|
|
@ -7,6 +7,7 @@ const views = require('../util/views.js');
|
|||
const Note = require('../models/note.js');
|
||||
const Point = require('../models/point.js');
|
||||
const TagInputControl = require('./tag_input_control.js');
|
||||
const PoolInputControl = require('./pool_input_control.js');
|
||||
const ExpanderControl = require('../controls/expander_control.js');
|
||||
const FileDropperControl = require('../controls/file_dropper_control.js');
|
||||
|
||||
|
@ -38,6 +39,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
canEditPostContent: api.hasPrivilege('posts:edit:content'),
|
||||
canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'),
|
||||
canEditPostSource: api.hasPrivilege('posts:edit:source'),
|
||||
canEditPoolPosts: api.hasPrivilege('pools:edit:posts'),
|
||||
canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'),
|
||||
canDeletePosts: api.hasPrivilege('posts:delete'),
|
||||
canFeaturePosts: api.hasPrivilege('posts:feature'),
|
||||
|
@ -56,6 +58,10 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
'post-notes',
|
||||
'Notes',
|
||||
this._hostNode.querySelectorAll('.notes'));
|
||||
this._poolsExpander = new ExpanderControl(
|
||||
'post-pools',
|
||||
`Pools (${this._post.pools.length})`,
|
||||
this._hostNode.querySelectorAll('.pools'));
|
||||
new ExpanderControl(
|
||||
'post-content',
|
||||
'Content',
|
||||
|
@ -76,6 +82,11 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
this._tagInputNode, post.tags);
|
||||
}
|
||||
|
||||
if (this._poolInputNode) {
|
||||
this._poolControl = new PoolInputControl(
|
||||
this._poolInputNode, post.pools);
|
||||
}
|
||||
|
||||
if (this._contentInputNode) {
|
||||
this._contentFileDropper = new FileDropperControl(
|
||||
this._contentInputNode, {
|
||||
|
@ -170,6 +181,9 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
this._post.notes.addEventListener(eventType, e => {
|
||||
this._syncExpanderTitles();
|
||||
});
|
||||
this._post.pools.addEventListener(eventType, e => {
|
||||
this._syncExpanderTitles();
|
||||
});
|
||||
}
|
||||
|
||||
this._tagControl.addEventListener(
|
||||
|
@ -182,11 +196,18 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
this._noteTextareaNode.addEventListener(
|
||||
'change', e => this._evtNoteTextChangeRequest(e));
|
||||
}
|
||||
|
||||
this._poolControl.addEventListener(
|
||||
'change', e => {
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
this._syncExpanderTitles();
|
||||
});
|
||||
}
|
||||
|
||||
_syncExpanderTitles() {
|
||||
this._notesExpander.title = `Notes (${this._post.notes.length})`;
|
||||
this._tagsExpander.title = `Tags (${this._post.tags.length})`;
|
||||
this._poolsExpander.title = `Pools (${this._post.pools.length})`;
|
||||
}
|
||||
|
||||
_evtPostContentChange(e) {
|
||||
|
@ -338,6 +359,10 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
misc.splitByWhitespace(this._tagInputNode.value) :
|
||||
undefined,
|
||||
|
||||
pools: this._poolInputNode ?
|
||||
misc.splitByWhitespace(this._poolInputNode.value) :
|
||||
undefined,
|
||||
|
||||
relations: this._relationsInputNode ?
|
||||
misc.splitByWhitespace(this._relationsInputNode.value)
|
||||
.map(x => parseInt(x)) :
|
||||
|
@ -374,6 +399,10 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
return this._formNode.querySelector('.tags input');
|
||||
}
|
||||
|
||||
get _poolInputNode() {
|
||||
return this._formNode.querySelector('.pools input');
|
||||
}
|
||||
|
||||
get _loopVideoInputNode() {
|
||||
return this._formNode.querySelector('.flags input[name=loop]');
|
||||
}
|
||||
|
|
|
@ -22,6 +22,23 @@ class PoolList extends AbstractList {
|
|||
{results: PoolList.fromResponse(response.results)}));
|
||||
});
|
||||
}
|
||||
|
||||
hasPoolId(poolId) {
|
||||
for (let pool of this._list) {
|
||||
if (pool.id === poolId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeById(poolId) {
|
||||
for (let pool of this._list) {
|
||||
if (pool.id === poolId) {
|
||||
this.remove(pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PoolList._itemClass = Pool;
|
||||
|
|
|
@ -7,6 +7,7 @@ const events = require('../events.js');
|
|||
const TagList = require('./tag_list.js');
|
||||
const NoteList = require('./note_list.js');
|
||||
const CommentList = require('./comment_list.js');
|
||||
const PoolList = require('./pool_list.js');
|
||||
const misc = require('../util/misc.js');
|
||||
|
||||
class Post extends events.EventTarget {
|
||||
|
@ -18,6 +19,7 @@ class Post extends events.EventTarget {
|
|||
obj._tags = new TagList();
|
||||
obj._notes = new NoteList();
|
||||
obj._comments = new CommentList();
|
||||
obj._pools = new PoolList();
|
||||
}
|
||||
|
||||
this._updateFromResponse({});
|
||||
|
@ -46,6 +48,7 @@ class Post extends events.EventTarget {
|
|||
get notes() { return this._notes; }
|
||||
get comments() { return this._comments; }
|
||||
get relations() { return this._relations; }
|
||||
get pools() { return this._pools; }
|
||||
|
||||
get score() { return this._score; }
|
||||
get commentCount() { return this._commentCount; }
|
||||
|
@ -128,6 +131,7 @@ class Post extends events.EventTarget {
|
|||
if (this._source !== this._orig._source) {
|
||||
detail.source = this._source;
|
||||
}
|
||||
// TODO pools
|
||||
|
||||
let apiPromise = this._id ?
|
||||
api.put(uri.formatApiLink('post', this.id), detail, files) :
|
||||
|
@ -304,6 +308,7 @@ class Post extends events.EventTarget {
|
|||
obj._tags.sync(response.tags);
|
||||
obj._notes.sync(response.notes);
|
||||
obj._comments.sync(response.comments);
|
||||
obj._pools.sync(response.pools);
|
||||
}
|
||||
|
||||
Object.assign(this, map());
|
||||
|
|
|
@ -34,3 +34,22 @@ def _bump_query_count() -> None:
|
|||
|
||||
|
||||
sa.event.listen(_engine, 'after_execute', lambda *args: _bump_query_count())
|
||||
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("myapp.sqltime")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def before_cursor_execute(conn, cursor, statement,
|
||||
parameters, context, executemany):
|
||||
conn.info.setdefault('query_start_time', []).append(time.time())
|
||||
logger.info("Start Query: %s" % statement)
|
||||
|
||||
def after_cursor_execute(conn, cursor, statement,
|
||||
parameters, context, executemany):
|
||||
total = time.time() - conn.info['query_start_time'].pop(-1)
|
||||
logger.info("Total Time: %f" % total)
|
||||
|
||||
sa.event.listen(_engine, "before_cursor_execute", before_cursor_execute)
|
||||
sa.event.listen(_engine, "after_cursor_execute", after_cursor_execute)
|
||||
|
|
|
@ -5,7 +5,7 @@ from datetime import datetime
|
|||
import sqlalchemy as sa
|
||||
from szurubooru import config, db, model, errors, rest
|
||||
from szurubooru.func import (
|
||||
users, scores, comments, tags, util,
|
||||
users, scores, comments, tags, pools, util,
|
||||
mime, images, files, image_hash, serialization, snapshots)
|
||||
|
||||
|
||||
|
@ -176,6 +176,7 @@ class PostSerializer(serialization.BaseSerializer):
|
|||
'hasCustomThumbnail': self.serialize_has_custom_thumbnail,
|
||||
'notes': self.serialize_notes,
|
||||
'comments': self.serialize_comments,
|
||||
'pools': self.serialize_pools,
|
||||
}
|
||||
|
||||
def serialize_id(self) -> Any:
|
||||
|
@ -299,6 +300,14 @@ class PostSerializer(serialization.BaseSerializer):
|
|||
self.post.comments,
|
||||
key=lambda comment: comment.creation_time)]
|
||||
|
||||
def serialize_pools(self) -> Any:
|
||||
return [
|
||||
pools.serialize_pool(pool)
|
||||
for pool in sorted(
|
||||
self.post.pools,
|
||||
key=lambda pool: pool.creation_time)]
|
||||
|
||||
|
||||
|
||||
def serialize_post(
|
||||
post: Optional[model.Post],
|
||||
|
|
|
@ -48,7 +48,7 @@ def upgrade():
|
|||
op.create_table(
|
||||
'pool_post',
|
||||
sa.Column('pool_id', sa.Integer(), nullable=False),
|
||||
sa.Column('post_id', sa.Integer(), nullable=False),
|
||||
sa.Column('post_id', sa.Integer(), nullable=False, index=True),
|
||||
sa.Column('ord', sa.Integer(), nullable=False, index=True),
|
||||
sa.ForeignKeyConstraint(['pool_id'], ['pool.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ondelete='CASCADE'),
|
||||
|
|
|
@ -2,7 +2,6 @@ import sqlalchemy as sa
|
|||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from szurubooru.model.base import Base
|
||||
import szurubooru.model as model
|
||||
|
||||
|
||||
class PoolName(Base):
|
||||
|
@ -43,9 +42,9 @@ class PoolPost(Base):
|
|||
order = sa.Column('ord', sa.Integer, nullable=False, index=True)
|
||||
|
||||
pool = sa.orm.relationship('Pool', back_populates='_posts')
|
||||
post = sa.orm.relationship('Post')
|
||||
post = sa.orm.relationship('Post', back_populates='_pools')
|
||||
|
||||
def __init__(self, post: model.Post) -> None:
|
||||
def __init__(self, post) -> None:
|
||||
self.post_id = post.post_id
|
||||
|
||||
class Pool(Base):
|
||||
|
|
|
@ -2,7 +2,10 @@ from typing import List
|
|||
import sqlalchemy as sa
|
||||
from szurubooru.model.base import Base
|
||||
from szurubooru.model.comment import Comment
|
||||
from szurubooru.model.pool import PoolPost
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
|
||||
|
||||
class PostFeature(Base):
|
||||
|
@ -224,6 +227,12 @@ class Post(Base):
|
|||
notes = sa.orm.relationship(
|
||||
'PostNote', cascade='all, delete-orphan', lazy='joined')
|
||||
comments = sa.orm.relationship('Comment', cascade='all, delete-orphan')
|
||||
_pools = sa.orm.relationship(
|
||||
'PoolPost',
|
||||
lazy='select',
|
||||
order_by='PoolPost.order',
|
||||
back_populates='post')
|
||||
pools = association_proxy('_pools', 'pool')
|
||||
|
||||
# dynamic columns
|
||||
tag_count = sa.orm.column_property(
|
||||
|
|
Loading…
Reference in a new issue