From f802962be28634e534e76e0825067c8d55b43954 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 7 Sep 2026 18:06:41 +0200 Subject: [PATCH] feat(frontend): say when a workspace holds only archived items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A workspace whose items are all archived read as empty, because the placeholder is decided by the default listing. Reaching those items then depended on the toolbar, which is why it had been left interactive while dimmed — and that let a kind toggle replace the invitation with "no items found" on a workspace that really was empty. The state is named instead. When the default listing comes back empty, one request asks whether anything archived exists, and the placeholder says which of the two it is: "Everything in this workspace is archived" with a link to show them, or the ordinary invitation. Held until that answer lands rather than drawn and swapped, since the wrong one claims the workspace is empty when it is not. The toolbar is dimmed and `inert` again, its original design: the archived case now carries its own way in. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9 --- .../src/lib/components/home/ItemsList.svelte | 47 +++++++++++++++---- .../home/WorkspaceEmptyState.svelte | 25 ++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 5983e97c38..c2ed62ff26 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -1008,6 +1008,30 @@ let hubPick = $state(undefined) let hubPickerOpen = $state(false) + /** + * Whether a workspace the default listing found empty is empty at all, or just has nothing + * unarchived — two different states that want two different things said about them. Asked + * only in that case, and once per workspace: one request for one row, never on a workspace + * with something in it. A failure answers "no archived items", which shows the ordinary + * placeholder rather than promising items that may not be there. + */ + let archivedProbe = $state<{ workspace: string; hasArchived: boolean } | undefined>(undefined) + $effect(() => { + const ws = $workspaceStore + if (!ws || !workspaceEmpty || archivedProbe?.workspace === ws) return + untrack(() => void probeArchived(ws)) + }) + async function probeArchived(workspace: string) { + try { + const res = await ScriptService.listRunnables({ workspace, showArchived: true, perPage: 1 }) + archivedProbe = { workspace, hasArchived: (res.items?.length ?? 0) > 0 } + } catch (error) { + console.error('Could not check for archived items:', error) + archivedProbe = { workspace, hasArchived: false } + } + } + let emptyStateAnswered = $derived(archivedProbe?.workspace === $workspaceStore) + // The workspace itself holds nothing — no filter is narrowing the list away. It stays // false until the first load resolves: a skeleton already means "loading", and the // empty state must not be mistaken for one. The controls it dims stay mounted, so @@ -1689,11 +1713,10 @@ > {#if !contentActive} -
+ lands; `inert` takes it out of the tab order and off the pointer meanwhile. A + workspace with nothing but archived items reaches them from its own placeholder, + so these controls are not the way there. --> +
{ @@ -1792,11 +1815,10 @@ {/if}
-
{#if workspaceEmpty && !$userStore?.operator && showEditButtons} - (hubPick = project)} /> + + {#if emptyStateAnswered} + (hubPick = project)} + onShowArchived={() => (filterValues.val = { ...filterValues.val, archived: true })} + /> + {/if} {:else} {/if} diff --git a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte index 0222ab28a1..7513c659d3 100644 --- a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte +++ b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte @@ -10,9 +10,16 @@ interface Props { /** A project was chosen here. The list owns the import dialog, and opens it on this. */ onPick: (project: HubProjectPick) => void + /** + * The workspace holds items, all of them archived. It is not empty and must not be + * told it is: the caption names what is there and offers the way to it instead. + */ + archivedOnly?: boolean + /** Switches the list to the archived view. */ + onShowArchived: () => void } - let { onPick }: Props = $props() + let { onPick, archivedOnly = false, onShowArchived }: Props = $props() // Row opacities: the list fading out of existence. Static on purpose — motion is what // makes a skeleton mean "loading", and this state means "empty". @@ -38,7 +45,7 @@
{#each rowOpacities as opacity, i (i)}
- Your scripts, flows and apps will show up here. + {#if archivedOnly} + + + Everything in this workspace is archived. + . + + {:else} + Your scripts, flows and apps will show up here. + {/if} {#if !$disableHubStore}