feat(projects): add, rename and delete sections
The board has drawn a column per section since it was written and there was no way to make one. Sections could only be created by the Todoist importer or a template — so an ordinary project could never have a heading in it at all. Add, rename and delete, in the list view where the headings already are. The controls appear on hover rather than sitting beside the heading permanently: a heading with two buttons always next to it stops reading as a heading. Deleting says how many tasks are in the section and that they will survive it, because the API keeps them and drops their section — which is the right behaviour and not what "delete section" sounds like. The list is re-read afterwards, since those tasks move to the unsectioned group and nothing else would tell the view they had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Kristian Wind
co-authored by
Claude Opus 5
parent
9056c44c9a
commit
3b521b463c
@@ -699,12 +699,6 @@
|
|||||||
gap: var(--s3);
|
gap: var(--s3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeats {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
color: var(--ink-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -101,6 +101,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- sections -------------------------------------------------------------------
|
||||||
|
|
||||||
|
let addingSection = $state(false);
|
||||||
|
let renamingSection = $state(null);
|
||||||
|
let sectionName = $state('');
|
||||||
|
|
||||||
|
async function addSection(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const name = sectionName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
addingSection = false;
|
||||||
|
sectionName = '';
|
||||||
|
try {
|
||||||
|
sections = [...sections, await api.createSection(project.id, name)];
|
||||||
|
} catch (e) {
|
||||||
|
app.toast(humanMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameSection(event, section) {
|
||||||
|
event.preventDefault();
|
||||||
|
const name = sectionName.trim();
|
||||||
|
renamingSection = null;
|
||||||
|
if (!name || name === section.name) return;
|
||||||
|
|
||||||
|
const previous = sections;
|
||||||
|
sections = sections.map((s) => (s.id === section.id ? { ...s, name } : s));
|
||||||
|
try {
|
||||||
|
await api.updateSection(section.id, { name });
|
||||||
|
} catch (e) {
|
||||||
|
sections = previous;
|
||||||
|
app.toast(humanMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSection(section) {
|
||||||
|
const count = open.filter((t) => t.section_id === section.id).length;
|
||||||
|
const warning = count
|
||||||
|
? `Slet sektionen "${section.name}"? De ${count} opgaver bliver, men mister deres sektion.`
|
||||||
|
: `Slet sektionen "${section.name}"?`;
|
||||||
|
if (!confirm(warning)) return;
|
||||||
|
|
||||||
|
const previous = sections;
|
||||||
|
sections = sections.filter((s) => s.id !== section.id);
|
||||||
|
try {
|
||||||
|
await api.deleteSection(section.id);
|
||||||
|
// The tasks are not deleted with it; they come back unsectioned, and the
|
||||||
|
// list has to be re-read to show them in the right place.
|
||||||
|
await app.loadTasks({ project_id: project.id });
|
||||||
|
} catch (e) {
|
||||||
|
sections = previous;
|
||||||
|
app.toast(humanMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function invite(event) {
|
async function invite(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
inviteError = '';
|
inviteError = '';
|
||||||
@@ -249,7 +304,34 @@
|
|||||||
{#each sections as section (section.id)}
|
{#each sections as section (section.id)}
|
||||||
{@const tasks = open.filter((t) => t.section_id === section.id)}
|
{@const tasks = open.filter((t) => t.section_id === section.id)}
|
||||||
<section>
|
<section>
|
||||||
<h2>{section.name}</h2>
|
<div class="section-head">
|
||||||
|
{#if renamingSection === section.id}
|
||||||
|
<form onsubmit={(e) => renameSection(e, section)}>
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<input
|
||||||
|
bind:value={sectionName}
|
||||||
|
autofocus
|
||||||
|
aria-label="Sektionens navn"
|
||||||
|
onblur={() => (renamingSection = null)}
|
||||||
|
onkeydown={(e) => e.key === 'Escape' && (renamingSection = null)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<h2>{section.name}</h2>
|
||||||
|
{#if canEdit}
|
||||||
|
<button
|
||||||
|
class="section-action"
|
||||||
|
onclick={() => {
|
||||||
|
renamingSection = section.id;
|
||||||
|
sectionName = section.name;
|
||||||
|
}}>Omdøb</button
|
||||||
|
>
|
||||||
|
<button class="section-action remove" onclick={() => removeSection(section)}>
|
||||||
|
Slet
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<TaskList {tasks} projectId={project.id} sectionId={section.id} {canEdit} />
|
<TaskList {tasks} projectId={project.id} sectionId={section.id} {canEdit} />
|
||||||
{#if !tasks.length}
|
{#if !tasks.length}
|
||||||
<p class="empty">Tom</p>
|
<p class="empty">Tom</p>
|
||||||
@@ -257,7 +339,35 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if !open.length}
|
{#if canEdit}
|
||||||
|
<section class="add-section">
|
||||||
|
{#if addingSection}
|
||||||
|
<form onsubmit={addSection}>
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<input
|
||||||
|
bind:value={sectionName}
|
||||||
|
autofocus
|
||||||
|
placeholder="Sektionens navn"
|
||||||
|
aria-label="Ny sektion"
|
||||||
|
onblur={() => !sectionName.trim() && (addingSection = false)}
|
||||||
|
onkeydown={(e) => e.key === 'Escape' && (addingSection = false)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="add"
|
||||||
|
onclick={() => {
|
||||||
|
addingSection = true;
|
||||||
|
sectionName = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Tilføj sektion
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !open.length && !sections.length}
|
||||||
<p class="clear">
|
<p class="clear">
|
||||||
<span class="rune" aria-hidden="true">ᚹ</span>
|
<span class="rune" aria-hidden="true">ᚹ</span>
|
||||||
Ingenting her endnu.
|
Ingenting her endnu.
|
||||||
@@ -548,6 +658,79 @@
|
|||||||
margin-bottom: var(--s2);
|
margin-bottom: var(--s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--s3);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
margin-bottom: var(--s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head h2 {
|
||||||
|
border-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden until the section is hovered or something in it has focus: a heading
|
||||||
|
with two buttons permanently beside it stops reading as a heading. */
|
||||||
|
.section-action {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-faint);
|
||||||
|
padding: 0 var(--s1);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--fast) var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head:hover .section-action,
|
||||||
|
.section-action:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action.remove:hover {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head form {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head input,
|
||||||
|
.add-section input {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--s1) var(--s2);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head input:focus,
|
||||||
|
.add-section input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section {
|
||||||
|
margin-top: var(--s5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section .add {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--ink-faint);
|
||||||
|
padding: var(--s2);
|
||||||
|
transition: color var(--fast) var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section .add:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.empty,
|
.empty,
|
||||||
.readonly {
|
.readonly {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user