add bookmark collection editor
This commit is contained in:
@@ -511,6 +511,12 @@ class BookmarkCollections(Model):
|
|||||||
"""
|
"""
|
||||||
res = db.fetch_one(q, user_id)
|
res = db.fetch_one(q, user_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_user(cls, user_id):
|
||||||
|
q = """SELECT * FROM bookmark_collections WHERE user_id = ? ORDER BY sort_order ASC"""
|
||||||
|
res = db.query(q, user_id)
|
||||||
|
return [cls.from_data(row) for row in res]
|
||||||
|
|
||||||
def has_posts(self):
|
def has_posts(self):
|
||||||
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e'
|
q = 'SELECT EXISTS(SELECT 1 FROM bookmarked_posts WHERE collection_id = ?) as e'
|
||||||
res = db.fetch_one(q, self.id)['e']
|
res = db.fetch_one(q, self.id)['e']
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def get_bookmark_dropdown():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return 'error', 400
|
return 'error', 400
|
||||||
is_thread = concept_kind == 'thread'
|
is_thread = concept_kind == 'thread'
|
||||||
collections = BookmarkCollections.findall({'user_id': user.id})
|
collections = BookmarkCollections.get_for_user(user.id)
|
||||||
in_collection = None
|
in_collection = None
|
||||||
note = ''
|
note = ''
|
||||||
for collection in collections:
|
for collection in collections:
|
||||||
|
|||||||
@@ -448,6 +448,69 @@ def bookmarks(username):
|
|||||||
username = username.lower()
|
username = username.lower()
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/<username>/bookmarks/collections/')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def bookmark_collections(username):
|
||||||
|
user = get_active_user()
|
||||||
|
collections = BookmarkCollections.get_for_user(user.id)
|
||||||
|
return render_template('users/manage_collections.html', collections=collections)
|
||||||
|
|
||||||
|
@bp.post('/<username>/bookmarks/collections/')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def edit_bookmark_collections(username):
|
||||||
|
user = get_active_user()
|
||||||
|
ids = request.form.getlist('id[]')
|
||||||
|
names = request.form.getlist('name[]')
|
||||||
|
if len(ids) == 0 or len(ids) != len(names):
|
||||||
|
abort(400)
|
||||||
|
deleted_ids = filter(lambda x: x.strip(), request.form.get('deleted_ids', '').split(';'))
|
||||||
|
try:
|
||||||
|
deleted_ids = map(lambda x: int(x), deleted_ids)
|
||||||
|
except ValueError:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
with db.transaction():
|
||||||
|
for new_order, id in enumerate(ids):
|
||||||
|
new_name = names[new_order]
|
||||||
|
if id == 'new':
|
||||||
|
bc = BookmarkCollections.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'is_default': False,
|
||||||
|
'name': new_name,
|
||||||
|
'sort_order': new_order,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
id = int(id)
|
||||||
|
bc = BookmarkCollections.find({'id': id})
|
||||||
|
if not bc:
|
||||||
|
continue
|
||||||
|
if bc.user_id != user.id:
|
||||||
|
continue
|
||||||
|
if bc.is_default:
|
||||||
|
new_order = 0
|
||||||
|
elif new_order == 0:
|
||||||
|
new_order = 1
|
||||||
|
bc.update({
|
||||||
|
'name': new_name,
|
||||||
|
'sort_order': new_order,
|
||||||
|
})
|
||||||
|
|
||||||
|
for deleted_id in deleted_ids:
|
||||||
|
bc = BookmarkCollections.find({'id': deleted_id})
|
||||||
|
if not bc:
|
||||||
|
continue
|
||||||
|
if bc.user_id != user.id:
|
||||||
|
continue
|
||||||
|
if bc.is_default:
|
||||||
|
continue
|
||||||
|
bc.delete()
|
||||||
|
|
||||||
|
return redirect(url_for('.bookmark_collections', username=username))
|
||||||
|
|
||||||
@bp.get('/<username>/delete-confirm/')
|
@bp.get('/<username>/delete-confirm/')
|
||||||
@login_required
|
@login_required
|
||||||
@redirect_to_own
|
@redirect_to_own
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
||||||
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'tertiary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'secondary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||||
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
||||||
<div class="sortable-item-inner">{{ caller() }}</div>
|
<div class="sortable-item-inner">{{ caller() }}</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
44
app/templates/users/manage_collections.html
Normal file
44
app/templates/users/manage_collections.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader, sortable_list, sortable_list_item -%}
|
||||||
|
{%- macro collection_item(name='', can_delete=true, id=-1, thread_count=0, post_count=0) -%}
|
||||||
|
<input name="name[]" type="text" autocomplete="off" value="{{name}}" required maxlength=60 placeholder="Collection name">
|
||||||
|
<input type="hidden" name="id[]" value="{{ 'new' if id == -1 else id}}" autocomplete="off">
|
||||||
|
<span>{{thread_count}} {{'thread' | pluralize(num=thread_count)}}, {{post_count}} {{'post' | pluralize(num=post_count)}} </span>
|
||||||
|
{%- if not can_delete -%}
|
||||||
|
<i>Default collection</i>
|
||||||
|
{%- else -%}
|
||||||
|
<button type="button" class="critical" data-s="deleteCollection">Delete</button>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endmacro -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}managing bookmark collections{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
<bitty-8 data-connect="/static/js/bits/collections-editor.js"></bitty-8>
|
||||||
|
{%- set sh -%}
|
||||||
|
<span class="js-only" data-r="enhance">
|
||||||
|
Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.
|
||||||
|
</span>
|
||||||
|
<div data-r="enhanceHide">This page requires JS enabled to work correctly.</div>
|
||||||
|
{%- endset -%}
|
||||||
|
{%- call() subheader('Manage bookmark collections', sh) -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions js-only" data-r="enhance">
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<button data-s="addCollection">Add new collection</button>
|
||||||
|
<input type="submit" class="alt" value="Save collections" form="collections-form">
|
||||||
|
</fieldset>
|
||||||
|
{%- endcall -%}
|
||||||
|
<form class="plank" method="POST" id="collections-form">
|
||||||
|
<input type="hidden" autocomplete="off" name="deleted_ids" value="" data-r="countDeletedCollection">
|
||||||
|
{%- call() sortable_list(attr={'data-r': 'addCollection'}) -%}
|
||||||
|
{%- for collection in collections -%}
|
||||||
|
{%- call() sortable_list_item(key='bc', immovable=collection.is_default == 1, attr={'data-r': 'deleteCollection', 'data-id': collection.id}) -%}
|
||||||
|
{{ collection_item(name=collection.name, can_delete=collection.is_default != 1, thread_count=collection.get_threads_count(), post_count=collection.get_posts_count(), id=collection.id) }}
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endcall -%}
|
||||||
|
</form>
|
||||||
|
<script type="text/html" data-template="collectionItem">
|
||||||
|
{%- call() sortable_list_item(key='bc', attr={'data-r': 'deleteCollection', 'data-id': 'new'}) -%}
|
||||||
|
{{- collection_item() -}}
|
||||||
|
{%- endcall -%}
|
||||||
|
</script>
|
||||||
|
{%- endblock -%}
|
||||||
@@ -297,6 +297,25 @@ a.site-title {
|
|||||||
margin-top: var(--medium-padding);
|
margin-top: var(--medium-padding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.primary-bg {
|
||||||
|
--main-color: var(--bg-color-primary);
|
||||||
|
background-color: var(--bg-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary-bg {
|
||||||
|
--main-color: var(--bg-color-secondary);
|
||||||
|
--rotation: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tertiary-bg {
|
||||||
|
--main-color: var(--bg-color-tertiary);
|
||||||
|
--rotation: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.contrast-bg {
|
||||||
|
--main-color: var(--bg-color-contrast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@@ -368,25 +387,6 @@ ul.horizontal, ol.horizontal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-bg {
|
|
||||||
--main-color: var(--bg-color-primary);
|
|
||||||
background-color: var(--bg-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-bg {
|
|
||||||
--main-color: var(--bg-color-secondary);
|
|
||||||
--rotation: 0deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tertiary-bg {
|
|
||||||
--main-color: var(--bg-color-tertiary);
|
|
||||||
--rotation: 0deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contrast-bg {
|
|
||||||
--main-color: var(--bg-color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.motd {
|
.motd {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--big-padding);
|
gap: var(--big-padding);
|
||||||
|
|||||||
18
data/static/js/bits/collections-editor.js
Normal file
18
data/static/js/bits/collections-editor.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const b = {}
|
||||||
|
|
||||||
|
export function addCollection(ev, sender, el) {
|
||||||
|
el.innerHTML += b.templates.collectionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCollection(ev, sender, el) {
|
||||||
|
if (!el.contains(sender)) return;
|
||||||
|
b.send({ 'id': el.prop('id') }, 'countDeletedCollection');
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countDeletedCollection(payload, _, el) {
|
||||||
|
if (payload.id === 'new') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.value += `${payload.id};`
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export const b = {
|
export const b = {
|
||||||
init: 'enhance',
|
init: 'enhance enhanceHide',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enhance(_, __, el) {
|
export function enhance(_, __, el) {
|
||||||
@@ -18,3 +18,11 @@ export function enhance(_, __, el) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function enhanceHide(_, __, el) {
|
||||||
|
if (el === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@
|
|||||||
if (listItems.has(node)) return;
|
if (listItems.has(node)) return;
|
||||||
|
|
||||||
const dragger = node.querySelector('.dragger');
|
const dragger = node.querySelector('.dragger');
|
||||||
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, item) });
|
dragger.addEventListener('dragstart', e => { sortableItemDragStart(e, node) });
|
||||||
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, item) });
|
dragger.addEventListener('dragend', e => { sortableItemDragEnd(e, node) });
|
||||||
node.addEventListener('dragover', e => { sortableItemDragOver(e, node) });
|
node.addEventListener('dragover', e => { sortableItemDragOver(e, node) });
|
||||||
listItems.add(node);
|
listItems.add(node);
|
||||||
listItemsHandled.set(list, listItems);
|
listItemsHandled.set(list, listItems);
|
||||||
|
|||||||
Reference in New Issue
Block a user