feat(groups): omdøb med dobbeltklik, og slet der hvor man kan se hvad man sletter
Release / Build and publish image (push) Failing after 9m59s
Release / Build and publish image (push) Failing after 9m59s
De to ord "Omdøb" og "Slet" lå oven på gruppens navn, hver gang musen var i nærheden. De er væk fra sidebjælken. Omdøbning starter nu ved dobbeltklik på rækken — eller F2, hvis man har den i fokus og ikke vil bruge musen. Sletning er flyttet til gruppens egen side, hvor projekterne, den ikke tager med sig, står på skærmen ovenover knappen. Dobbeltklik på et navn, der også er et link, er ikke gratis. Første forsøg lod det første klik navigere, og så åbnede siden mens omdøbningsformularen kom frem — farveprikkerne flyttede sig ud under markøren, hvilket testen fangede som "element is not stable". Navigationen venter derfor to tiendedele på et muligt andet klik. Det er under grænsen for et dobbeltklik og over grænsen for at mærke det. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bd5e86dd03
commit
c151d6c98c
@@ -363,7 +363,9 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
// Colour, which is chosen while renaming rather than from the row. Both are
|
||||
// edits to the same thing, and a heading carrying four controls crowded the
|
||||
// name it exists to show.
|
||||
await sidebar.getByRole('button', { name: 'Omdøb' }).click();
|
||||
// Double-click on the row opens it; the two words that used to sit on top of
|
||||
// the name are gone from the sidebar entirely.
|
||||
await sidebar.getByRole('link', { name: 'Arbejde', exact: true }).dblclick();
|
||||
await sidebar.getByRole('button', { name: 'Petrol' }).click();
|
||||
// Still open: picking a colour must not close the form under the pointer, which
|
||||
// is what a plain blur-to-cancel does in the two browsers that do not focus a
|
||||
@@ -407,7 +409,7 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
|
||||
// Renaming writes one row rather than one per project, which is the reason a
|
||||
// group is a table and not a string repeated on every project.
|
||||
await sidebar.getByRole('button', { name: 'Omdøb' }).click();
|
||||
await sidebar.getByRole('link', { name: 'Arbejde', exact: true }).dblclick();
|
||||
const name = sidebar.getByLabel('Gruppens navn');
|
||||
await name.fill('Kontoret');
|
||||
await name.press('Enter');
|
||||
@@ -416,8 +418,11 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
||||
// Deleting the heading must not take the project filed under it. It is
|
||||
// ungrouped here, which is the case that would look identical either way if
|
||||
// the assertion were only "the group is gone".
|
||||
// Deleting is on the group's own page now, where the projects it will not take
|
||||
// with it are on the screen above the button.
|
||||
await sidebar.getByRole('link', { name: 'Kontoret', exact: true }).click();
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
await sidebar.getByRole('button', { name: 'Slet' }).click();
|
||||
await page.getByRole('button', { name: 'Slet' }).click();
|
||||
await expect(sidebar.getByRole('link', { name: 'Kontoret', exact: true })).toBeHidden();
|
||||
await expect(sidebar.getByRole('link', { name: 'Regnskab' })).toBeVisible();
|
||||
|
||||
|
||||
@@ -105,17 +105,18 @@
|
||||
await app.renameGroup(group.id, name);
|
||||
}
|
||||
|
||||
async function removeGroup(group) {
|
||||
const count = inGroup(group.id).length;
|
||||
// Says what happens to the projects, because "slet gruppen" reads like it
|
||||
// might take them with it. It does not: only the heading goes.
|
||||
const kept =
|
||||
count === 1 ? t('sidebar.deleteGroupOne') : t('sidebar.deleteGroupMany', { n: count });
|
||||
const question = t('sidebar.deleteGroup', { name: group.name });
|
||||
if (!confirm(count ? `${question} ${kept}` : question)) return;
|
||||
await app.deleteGroup(group.id);
|
||||
/** Double-click, or F2 with the row focused. The colour is chosen in the same
|
||||
* form, because it is the same edit to the same thing. */
|
||||
/** Cleared by a second click, which means the double was a rename. */
|
||||
let navTimer;
|
||||
|
||||
function startRename(group) {
|
||||
clearTimeout(navTimer);
|
||||
renamingGroup = group.id;
|
||||
groupName = group.name;
|
||||
}
|
||||
|
||||
|
||||
// --- resizing -------------------------------------------------------------------
|
||||
|
||||
let resizing = $state(false);
|
||||
@@ -547,6 +548,13 @@
|
||||
overId = null;
|
||||
}}
|
||||
ondrop={(e) => onGroupDrop(e, group)}
|
||||
ondblclick={() => startRename(group)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'F2') {
|
||||
e.preventDefault();
|
||||
startRename(group);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if renamingGroup === group.id}
|
||||
<!-- Renaming is where the colour is chosen too.
|
||||
@@ -618,7 +626,24 @@
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="fold">
|
||||
<a href="/gruppe/{group.id}" onclick={onnavigate}>
|
||||
<!-- Navigation waits two tenths of a second for a second click. A
|
||||
link cannot know a double is coming without pausing, and the
|
||||
alternative was worse than the pause: the first click opened the
|
||||
page while the rename form was appearing, and the colour swatches
|
||||
moved out from under the pointer. Two tenths is under the double
|
||||
click threshold and over the threshold of noticing. -->
|
||||
<a
|
||||
href="/gruppe/{group.id}"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
clearTimeout(navTimer);
|
||||
if (e.detail > 1) return;
|
||||
navTimer = setTimeout(() => {
|
||||
onnavigate();
|
||||
goto(`/gruppe/${group.id}`);
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
class="dot group-dot"
|
||||
style="background: {colorVar(group.color)}"
|
||||
@@ -628,14 +653,6 @@
|
||||
</a>
|
||||
</h2>
|
||||
<span class="count">{inside.length}</span>
|
||||
<button
|
||||
class="group-action"
|
||||
onclick={() => {
|
||||
renamingGroup = group.id;
|
||||
groupName = group.name;
|
||||
}}>{t('sidebar.rename')}</button
|
||||
>
|
||||
<button class="group-action remove" onclick={() => removeGroup(group)}>{t('sidebar.delete')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1013,43 +1030,6 @@
|
||||
|
||||
/* Hidden until the heading is hovered, like the section actions on a project
|
||||
page: two buttons permanently beside a label stop it reading as a label. */
|
||||
/* Hidden but still in the flow, these two reserved their own width at all
|
||||
times — enough that ARBEJDE was drawn as "ARBE". They are lifted out of the
|
||||
flow and sit over the right end of the row, so the name has the whole line
|
||||
until somebody actually reaches for them. */
|
||||
.group-action {
|
||||
position: absolute;
|
||||
right: var(--s2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-faint);
|
||||
padding: 0 var(--s1);
|
||||
flex: none;
|
||||
opacity: 0;
|
||||
background: var(--surface);
|
||||
transition: opacity var(--fast) var(--ease);
|
||||
}
|
||||
|
||||
.group-action.remove {
|
||||
right: var(--s2);
|
||||
}
|
||||
|
||||
.group-action:not(.remove) {
|
||||
right: calc(var(--s2) + 3.2em);
|
||||
}
|
||||
|
||||
.folder-head:hover .group-action,
|
||||
.group-action:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.group-action:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.group-action.remove:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.folder-head .count {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* projects answers is what is left.
|
||||
*/
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api, humanMessage } from '$lib/api.js';
|
||||
import { app, projectName } from '$lib/stores.svelte.js';
|
||||
import { colorVar } from '$lib/colors.js';
|
||||
@@ -29,6 +30,18 @@
|
||||
load(id);
|
||||
});
|
||||
|
||||
/** Says what happens to the projects, because "delete the group" reads like it
|
||||
* might take them with it. It does not: only the heading goes. */
|
||||
async function removeGroup() {
|
||||
const count = projects.length;
|
||||
const kept =
|
||||
count === 1 ? t('sidebar.deleteGroupOne') : t('sidebar.deleteGroupMany', { n: count });
|
||||
const question = t('sidebar.deleteGroup', { name: group.name });
|
||||
if (!confirm(count ? `${question} ${kept}` : question)) return;
|
||||
await app.deleteGroup(group.id);
|
||||
goto('/');
|
||||
}
|
||||
|
||||
async function load(groupId) {
|
||||
if (!groupId) return;
|
||||
status = 'loading';
|
||||
@@ -208,6 +221,14 @@
|
||||
<span>{uploading ? t('group.uploading') : t('group.addFile')}</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<!-- Deleting lives here rather than in the sidebar, where two words sat on
|
||||
top of the group's name and where you could press them without having
|
||||
seen what the group holds. Here the projects are on the screen above
|
||||
it, which is the whole argument for the button being here. -->
|
||||
<section class="danger">
|
||||
<button class="remove" onclick={removeGroup}>{t('sidebar.delete')}</button>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -230,6 +251,22 @@
|
||||
gap: var(--s3);
|
||||
}
|
||||
|
||||
.danger {
|
||||
margin-top: var(--s4);
|
||||
padding-top: var(--s4);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.danger .remove {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--ink-faint);
|
||||
padding: var(--s2) 0;
|
||||
}
|
||||
|
||||
.danger .remove:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-2xl);
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user