-
{name}
+
+
{name}
+ {#if actions}
+
+
+
+
e.stopPropagation()}
+ >
+ {@render actions()}
+
+ {/if}
+
{@render detail()}
diff --git a/frontend/src/lib/components/home/TreeView.svelte b/frontend/src/lib/components/home/TreeView.svelte
index 7331318727..f7b99ae5d7 100644
--- a/frontend/src/lib/components/home/TreeView.svelte
+++ b/frontend/src/lib/components/home/TreeView.svelte
@@ -3,7 +3,9 @@
import { createEventDispatcher, onDestroy, untrack, type Snippet } from 'svelte'
import ResizeTransitionWrapper from '$lib/components/common/ResizeTransitionWrapper.svelte'
- import { Folder, FolderTree, User } from 'lucide-svelte'
+ import { Eye, Folder, FolderTree, Pen, User } from 'lucide-svelte'
+ import { userStore } from '$lib/stores'
+ import Dropdown from '$lib/components/DropdownV2.svelte'
import TreeOwnerRow from './TreeOwnerRow.svelte'
import PipelineRow from '../common/table/PipelineRow.svelte'
import Item from './Item.svelte'
@@ -56,6 +58,9 @@
// Remembers which nodes the user opened by hand, so they reopen on the next visit
// while expand/collapse-all is in its collapsed state.
openMemory?: TreeOpenMemory
+ // Adds a "Manage folder" menu ("View folder" for a non-owner) to top-level folder rows.
+ // Subfolders are path segments, not folders with permissions of their own, so get none.
+ onManageFolder?: (folderName: string) => void
}
let {
@@ -75,7 +80,8 @@
ancestorHasMore = false,
indent = 0,
leaf,
- openMemory
+ openMemory,
+ onManageFolder
}: Props = $props()
const unruled = inUnruledList()
@@ -270,6 +276,29 @@
}
+{#snippet manageFolderMenu()}
+ {#if isFolder(item)}
+ {@const folderName = item.folderName}
+
+ {@const canWrite =
+ $userStore?.is_admin ||
+ $userStore?.is_super_admin ||
+ $userStore?.folders_owners.includes(folderName)}
+
onManageFolder?.(folderName)
+ }
+ ]}
+ />
+ {/if}
+{/snippet}
+
{#if isFolder(item) || isUser(item)}
{#snippet detail()}
{#if ownerCount != undefined}
@@ -331,6 +361,7 @@
depth={depth + 1}
{indent}
{leaf}
+ {onManageFolder}
/>
{/each}
{#if effectiveMax < item.items.length}
diff --git a/frontend/src/lib/components/home/TreeViewRoot.svelte b/frontend/src/lib/components/home/TreeViewRoot.svelte
index 86c1984254..d5023176b5 100644
--- a/frontend/src/lib/components/home/TreeViewRoot.svelte
+++ b/frontend/src/lib/components/home/TreeViewRoot.svelte
@@ -53,6 +53,11 @@
leaf?: Snippet<[ItemType, number]>
// See TreeView's `openMemory`.
openMemory?: TreeOpenMemory
+ // See TreeView's `onManageFolder`.
+ onManageFolder?: (folderName: string) => void
+ // Inject folders `ownerCounts` calls empty, as "0 items". Off under a kind filter,
+ // where those counts are per kind and every folder lacking that kind would show.
+ keepEmptyFolders?: boolean
}
let {
@@ -76,7 +81,9 @@
framed = true,
groupOtherUsers = false,
leaf,
- openMemory
+ openMemory,
+ onManageFolder,
+ keepEmptyFolders = false
}: Props = $props()
const unruled = inUnruledList()
@@ -127,15 +134,20 @@
.filter((g) => 'folderName' in g)
.map((g) => (g as { folderName: string }).folderName)
)
- // Once counts are in, an owner holding nothing the user can see is dropped
- // rather than injected — a workspace's folder list is mostly noise in the
- // tree otherwise. Pipeline folders are exempt: their entry is the pipeline
- // itself, whose member scripts are folded out of the runnable count.
+ // Once counts are in, a user space holding nothing the user can see is dropped
+ // rather than injected — the member list is mostly noise in the tree otherwise.
+ // Empty folders are dropped too unless `keepEmptyFolders`. Pipeline folders are
+ // exempt: their entry is the pipeline, whose scripts the count folds out.
const isEmptyOwner = (prefix: string) => ownerCounts != undefined && !ownerCounts[prefix]
const missingFolders: { folderName: string; items: [] }[] = []
for (const folderName of [...(pipelineFolders ?? []), ...allFolders]) {
if (presentFolders.has(folderName)) continue
- if (!pipelineFolders?.has(folderName) && isEmptyOwner(`f/${folderName}`)) continue
+ if (
+ !keepEmptyFolders &&
+ !pipelineFolders?.has(folderName) &&
+ isEmptyOwner(`f/${folderName}`)
+ )
+ continue
presentFolders.add(folderName)
missingFolders.push({ folderName, items: [] })
}
@@ -276,6 +288,7 @@
{showEditButton}
{leaf}
{openMemory}
+ {onManageFolder}
/>
{/snippet}
diff --git a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte
index 813cb3a265..5014e2842b 100644
--- a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte
+++ b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte
@@ -26,9 +26,17 @@
* take — the archived link stays, since reading archived items is not a write.
*/
canCreate?: boolean
+ /** See CreateActionsMenu's `onCreateFolder`. */
+ onCreateFolder?: () => void
}
- let { onPick, archivedOnly = false, onShowArchived, canCreate = true }: Props = $props()
+ let {
+ onPick,
+ archivedOnly = false,
+ onShowArchived,
+ canCreate = true,
+ onCreateFolder
+ }: 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".
@@ -136,7 +144,7 @@
or
{/if}
-
+
{#snippet trigger()}