Update collections and attachment uploads

This commit is contained in:
rucki
2026-09-03 13:59:02 +02:00
parent 28e17a4b21
commit d97244de40
9 changed files with 337 additions and 24 deletions
+2 -3
View File
@@ -1,15 +1,14 @@
{% extends 'core/base.html' %}
{% load i18n markdown_extras %}
{% block content %}
<div class="d-md-none alert alert-info">{% trans "Collections are currently available on desktop only." %}</div>
<article class="d-none d-md-block collection-document">
<article class="collection-document">
<header class="mb-5">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<a class="btn btn-sm btn-outline-secondary mb-3" href="{% if user.is_authenticated %}{% url 'collection_list' %}{% else %}/{% endif %}">← {% if user.is_authenticated %}{% trans "Collections" %}{% else %}Start{% endif %}</a>
<h1 class="display-5 mb-3">{{ collection.title }}</h1>
<div class="d-flex flex-wrap gap-2 align-items-center">
{% if collection.mode == 'tags' %}<span class="badge text-bg-info">{% trans "Tag collection" %}</span>{% endif %}
{% if collection.mode != 'manual' %}<span class="badge text-bg-info">{{ collection.get_mode_display }}</span>{% endif %}
{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}
{% for tag in collection.filter_tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span>{% endfor %}
</div>
+10 -3
View File
@@ -1,16 +1,23 @@
{% extends 'core/base.html' %}
{% load i18n %}
{% block content %}
<div class="d-md-none alert alert-info">{% trans "Collections are currently available on desktop only." %}</div>
<div class="d-none d-md-block">
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div><h1 class="h3 mb-1">{% trans "Collections" %}</h1><p class="text-muted mb-0">{% trans "Combine existing notes into ordered documents." %}</p></div>
<a class="btn btn-dark" href="{% url 'collection_create' %}">{% trans "New collection" %}</a>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-sm {% if active_mode == 'listed' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="{% url 'collection_list' %}">{% trans "All" %}</a>
<a class="btn btn-sm {% if active_mode == 'manual' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="?mode=manual">{% trans "Manual collection" %}</a>
<a class="btn btn-sm {% if active_mode == 'tutorial' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="?mode=tutorial">{% trans "Tutorial" %}</a>
<a class="btn btn-sm {% if active_mode == 'tags' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="?mode=tags">{% trans "Tag collection" %}</a>
<a class="btn btn-sm {% if active_mode == 'chapter' %}btn-dark{% else %}btn-outline-secondary{% endif %}" href="?mode=chapter">{% trans "Chapters" %}</a>
</div>
{% if orphan_chapters %}<div class="alert alert-warning"><div class="fw-semibold mb-1">{% trans "Orphan chapters" %}</div><div class="small">{% trans "These chapters are not used by any tutorial yet:" %} {% for chapter in orphan_chapters %}<a href="{{ chapter.get_absolute_url }}">{{ chapter.title }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</div></div>{% endif %}
<div class="row g-3">
{% for entry in entries %}{% with collection=entry.collection %}
<div class="col-md-6"><a class="card item-card h-100 text-decoration-none text-dark" href="{{ collection.get_absolute_url }}"><div class="card-body">
<div class="d-flex justify-content-between gap-2"><h2 class="h5 mb-1">{{ collection.title }}</h2><div class="d-flex gap-1">{% if collection.mode == 'tags' %}<span class="badge text-bg-info">{% trans "Tag collection" %}</span>{% endif %}{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}</div></div>
<div class="d-flex justify-content-between gap-2"><h2 class="h5 mb-1">{{ collection.title }}</h2><div class="d-flex gap-1">{% if collection.mode != 'manual' %}<span class="badge text-bg-info">{{ collection.get_mode_display }}</span>{% endif %}{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}</div></div>
{% if collection.description %}<p class="text-muted mt-2 mb-2">{{ collection.description|truncatechars:140 }}</p>{% endif %}
{% if collection.mode == 'tags' %}<div class="mb-2">{% for tag in collection.filter_tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span> {% endfor %}</div>{% endif %}
<div class="small text-muted">{{ entry.section_count }} {% trans "sections" %} · {{ collection.updated_at|date:'d.m.Y H:i' }}</div>
+34 -2
View File
@@ -8,6 +8,7 @@
<a class="btn btn-sm btn-outline-secondary mb-3" href="{{ collection.get_absolute_url }}" hx-boost="false">← {% trans "View collection" %}</a>
<h1 class="display-6 mb-2">{{ collection.title }}</h1>
<div class="d-flex flex-wrap gap-2 align-items-center">
{% if collection.mode != 'manual' %}<span class="badge text-bg-info">{{ collection.get_mode_display }}</span>{% endif %}
{% if collection.team %}<span class="badge text-bg-warning">{{ collection.team.name }}</span>{% else %}<span class="badge text-bg-light">{{ collection.get_visibility_display }}</span>{% endif %}
{% for tag in collection.filter_tags.all %}<span class="badge rounded-pill text-bg-light">#{{ tag.name }}</span>{% endfor %}
</div>
@@ -35,9 +36,9 @@
</div>
{% endif %}
<div id="collection-sections" class="vstack gap-4 mb-5">
<div id="collection-sections" class="vstack gap-4 mb-5" data-reorder-url="{% url 'collection_manage' collection.id %}">
{% for section in sections %}
<section id="section-{{ section.id }}" class="card item-card"><div class="card-body">
<section id="section-{{ section.id }}" class="card item-card" draggable="true" data-section-id="{{ section.id }}" data-position="{{ forloop.counter0 }}"><div class="card-body">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<h2 class="h3 mb-0">{% if section.item %}{{ section.title|default:section.item.content|markdown_title|truncatechars:100 }}{% else %}{{ section.title|default:section.sub_collection.title|truncatechars:100 }}{% endif %}</h2>
<div class="d-flex gap-2 align-items-center">{% if section.item %}<a class="btn btn-sm btn-outline-secondary" href="{% url 'item_editor' section.item.id %}?next={% url 'collection_manage' collection.id %}" hx-boost="false">{% trans "Edit note" %}</a><a class="small text-muted" href="{{ section.item.get_absolute_url }}">#{{ section.item.id }}</a>{% else %}<a class="btn btn-sm btn-outline-secondary" href="{% url 'collection_manage' section.sub_collection.id %}" hx-boost="false">{% trans "Edit collection" %}</a><a class="small text-muted" href="{{ section.sub_collection.get_absolute_url }}">#{{ section.sub_collection.id }}</a>{% endif %}</div>
@@ -45,6 +46,8 @@
{% if section.item %}<div class="content">{{ section.item|item_markdown }}</div>
{% if section.item.comment %}<div class="content text-muted border-start ps-3 mt-3">{{ section.item.comment|markdown }}</div>{% endif %}{% else %}<div class="text-muted">{% trans "Nested collection" %}: {{ section.sub_collection.title }}</div>{% endif %}
{% if can_edit %}<div class="border-top mt-4 pt-3 d-flex align-items-center gap-2">
<span class="btn btn-sm btn-outline-secondary" title="{% trans 'Drag to reorder' %}">↕</span>
<form method="post" class="d-none move-form">{% csrf_token %}<input type="hidden" name="action" value="move_to"><input type="hidden" name="section_id" value="{{ section.id }}"><input type="hidden" name="position" value="{{ forloop.counter0 }}"></form>
<form method="post" class="d-flex gap-2 flex-grow-1" hx-on::before-request="window.collectionFocus='section-{{ section.id }}'">{% csrf_token %}<input type="hidden" name="action" value="title"><input type="hidden" name="section_id" value="{{ section.id }}"><input class="form-control form-control-sm" name="title" value="{{ section.title }}" placeholder="{% trans 'Optional section title' %}"><button class="btn btn-sm btn-outline-secondary">{% trans "Save title" %}</button></form>
<form method="post" hx-on::before-request="window.collectionFocus='section-{{ section.id }}'">{% csrf_token %}<input type="hidden" name="action" value="up"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move up' %}">↑</button></form>
<form method="post" hx-on::before-request="window.collectionFocus='section-{{ section.id }}'">{% csrf_token %}<input type="hidden" name="action" value="down"><input type="hidden" name="section_id" value="{{ section.id }}"><button class="btn btn-sm btn-outline-secondary" title="{% trans 'Move down' %}">↓</button></form>
@@ -57,6 +60,8 @@
{% if can_edit %}
<div class="card item-card"><div class="card-body">
<h2 class="h4">{% trans "Add existing note or collection" %}</h2>
<div class="mb-4"><a class="btn btn-dark" href="{% url 'new_item' %}?kind=note&collection={{ collection.id }}&next={% url 'collection_manage' collection.id %}" hx-boost="false">{% trans "Create note" %}</a></div>
<form method="post" class="input-group mb-4" hx-boost="false">{% csrf_token %}<input type="hidden" name="action" value="create_chapter"><input class="form-control" name="title" placeholder="{% trans 'New chapter title' %}"><button class="btn btn-outline-dark">{% trans "Create chapter" %}</button></form>
<form method="get" class="input-group mb-3"><input class="form-control" name="q" value="{{ q }}" placeholder="{% trans 'Search notes and collections' %}"><button class="btn btn-outline-secondary">{% trans "Search" %}</button></form>
<h3 class="h6">{% trans "Notes" %}</h3>
<div class="vstack gap-2 mb-4">
@@ -69,4 +74,31 @@
</div></div>
{% endif %}
</div>
<script>
(function(){
const list = document.getElementById('collection-sections');
if(!list) return;
let dragged = null;
list.addEventListener('dragstart', event => {
dragged = event.target.closest('[data-section-id]');
if(dragged) event.dataTransfer.effectAllowed = 'move';
});
list.addEventListener('dragover', event => {
if(!dragged) return;
const target = event.target.closest('[data-section-id]');
if(!target || target === dragged) return;
event.preventDefault();
const after = event.clientY > target.getBoundingClientRect().top + target.offsetHeight / 2;
target.parentNode.insertBefore(dragged, after ? target.nextSibling : target);
});
list.addEventListener('drop', event => {
if(!dragged) return;
event.preventDefault();
const sections = [...list.querySelectorAll('[data-section-id]')];
const form = dragged.querySelector('.move-form');
form.querySelector('[name=position]').value = sections.indexOf(dragged);
form.submit();
});
})();
</script>
{% endblock %}
+2 -1
View File
@@ -4,9 +4,10 @@
<form class="card item-card shadow-sm position-relative" method="post" enctype="multipart/form-data" x-data="quickComposer([{% for tag in tags %}'{{ tag.name|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}])">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next_url }}">
{% if collection %}<input type="hidden" name="collection" value="{{ collection.id }}">{% endif %}
<div class="card-body vstack gap-3">
<div class="d-flex justify-content-between align-items-center gap-2 sticky-top bg-white py-2" style="z-index:5">
<h1 class="h5 m-0">{% if requested_kind == 'todo' %}{% trans "Create task" %}{% elif requested_kind == 'link' %}{% trans "Create link" %}{% elif requested_kind == 'journal' %}{% trans "Create journal entry" %}{% else %}{% trans "Create note" %}{% endif %}</h1>
<h1 class="h5 m-0">{% if collection %}{% blocktrans with title=collection.title %}Create note for {{ title }}{% endblocktrans %}{% elif requested_kind == 'todo' %}{% trans "Create task" %}{% elif requested_kind == 'link' %}{% trans "Create link" %}{% elif requested_kind == 'journal' %}{% trans "Create journal entry" %}{% else %}{% trans "Create note" %}{% endif %}</h1>
<div class="d-flex gap-2"><button class="btn btn-success px-3 py-2" title="{% trans 'Save' %}">✓</button><a class="btn btn-danger px-3 py-2" href="{{ next_url }}" title="{% trans 'Cancel' %}">×</a></div>
</div>
<div class="mobile-tag-picker">