forked from EduCraft/curriculum-project-hub
feat(admin-web): add folder and project creation functionality in FolderNode and FolderTree components
This commit is contained in:
@@ -34,6 +34,10 @@ HUB_FEISHU_EVENTS_PER_MINUTE="120"
|
|||||||
# to this path and rejects any overlap before installing the unit.
|
# to this path and rejects any overlap before installing the unit.
|
||||||
HUB_PROJECT_WORKSPACE_ROOT="/var/lib/cph-hub/workspaces"
|
HUB_PROJECT_WORKSPACE_ROOT="/var/lib/cph-hub/workspaces"
|
||||||
|
|
||||||
|
# Persistent root for the content-addressed agent skill store. Required at hub
|
||||||
|
# startup unless XDG_STATE_HOME is set (then defaults to $XDG_STATE_HOME/skills).
|
||||||
|
HUB_SKILL_STORE_ROOT="/var/lib/cph-hub/state/skills"
|
||||||
|
|
||||||
# This process is pinned to exactly one Organization. Feishu credentials are
|
# This process is pinned to exactly one Organization. Feishu credentials are
|
||||||
# resolved from that Organization's encrypted ACTIVE connection.
|
# resolved from that Organization's encrypted ACTIVE connection.
|
||||||
HUB_SILO_ORGANIZATION_ID=""
|
HUB_SILO_ORGANIZATION_ID=""
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
folders,
|
folders,
|
||||||
projects,
|
projects,
|
||||||
slug,
|
slug,
|
||||||
|
onCreateFolder,
|
||||||
|
onCreateProject,
|
||||||
}: {
|
}: {
|
||||||
folder: ExplorerFolder;
|
folder: ExplorerFolder;
|
||||||
folders: ExplorerFolder[];
|
folders: ExplorerFolder[];
|
||||||
@@ -19,30 +21,58 @@
|
|||||||
binding: { chatId: string } | null;
|
binding: { chatId: string } | null;
|
||||||
}[];
|
}[];
|
||||||
slug: string;
|
slug: string;
|
||||||
|
onCreateFolder?: (parentId: string) => void;
|
||||||
|
onCreateProject?: (folderId: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let open = $state(true);
|
let open = $state(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<div class="group flex w-full items-center gap-2.5 px-3 py-2.5 text-sm transition hover:bg-surface-100">
|
||||||
type="button"
|
<button type="button" class="flex min-w-0 flex-1 items-center gap-2.5 text-left" onclick={() => (open = !open)}>
|
||||||
class="flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-sm transition hover:bg-surface-100"
|
<span class="w-3.5 text-center text-xs text-surface-600">{open ? '▾' : '▸'}</span>
|
||||||
onclick={() => (open = !open)}
|
<span class="flex h-7 w-7 items-center justify-center border border-warning-300 bg-warning-50 text-warning-800">
|
||||||
>
|
<Icon name="folder" class="h-4 w-4" />
|
||||||
<span class="w-3.5 text-center text-xs text-surface-600">{open ? '▾' : '▸'}</span>
|
</span>
|
||||||
<span class="flex h-7 w-7 items-center justify-center border border-warning-300 bg-warning-50 text-warning-800">
|
<span class="min-w-0 flex-1 truncate font-medium text-surface-900">{folder.name}</span>
|
||||||
<Icon name="folder" class="h-4 w-4" />
|
<span class="saas-badge-neutral">{folder.projectCount} 项目</span>
|
||||||
</span>
|
{#if folder.childFolderCount > 0}
|
||||||
<span class="min-w-0 flex-1 truncate font-medium text-surface-900">{folder.name}</span>
|
<span class="saas-badge-neutral">{folder.childFolderCount} 子夹</span>
|
||||||
<span class="saas-badge-neutral">{folder.projectCount} 项目</span>
|
{/if}
|
||||||
{#if folder.childFolderCount > 0}
|
</button>
|
||||||
<span class="saas-badge-neutral">{folder.childFolderCount} 子夹</span>
|
{#if onCreateFolder || onCreateProject}
|
||||||
|
<span
|
||||||
|
class="flex shrink-0 items-center gap-1 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100"
|
||||||
|
>
|
||||||
|
{#if onCreateFolder}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-6 w-6 items-center justify-center border border-surface-300 bg-surface-50 text-surface-600 transition hover:border-primary-400 hover:text-primary-700"
|
||||||
|
title="在此新建子文件夹"
|
||||||
|
aria-label="在 {folder.name} 内新建子文件夹"
|
||||||
|
onclick={() => onCreateFolder(folder.id)}
|
||||||
|
>
|
||||||
|
<Icon name="folder-plus" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if onCreateProject}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-6 w-6 items-center justify-center border border-surface-300 bg-surface-50 text-surface-600 transition hover:border-primary-400 hover:text-primary-700"
|
||||||
|
title="在此新建项目"
|
||||||
|
aria-label="在 {folder.name} 内新建项目"
|
||||||
|
onclick={() => onCreateProject(folder.id)}
|
||||||
|
>
|
||||||
|
<Icon name="file-plus" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="ml-4 border-l border-surface-300 pl-2">
|
<div class="ml-4 border-l border-surface-300 pl-2">
|
||||||
<FolderTree {folders} {projects} parentId={folder.id} {slug} />
|
<FolderTree {folders} {projects} parentId={folder.id} {slug} {onCreateFolder} {onCreateProject} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
projects,
|
projects,
|
||||||
parentId,
|
parentId,
|
||||||
slug,
|
slug,
|
||||||
|
onCreateFolder,
|
||||||
|
onCreateProject,
|
||||||
}: {
|
}: {
|
||||||
folders: ExplorerFolder[];
|
folders: ExplorerFolder[];
|
||||||
projects: {
|
projects: {
|
||||||
@@ -20,6 +22,8 @@
|
|||||||
}[];
|
}[];
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
onCreateFolder?: (parentId: string) => void;
|
||||||
|
onCreateProject?: (folderId: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let childFolders = $derived(folders.filter((f) => f.parentId === parentId));
|
let childFolders = $derived(folders.filter((f) => f.parentId === parentId));
|
||||||
@@ -44,6 +48,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each childFolders as f (f.id)}
|
{#each childFolders as f (f.id)}
|
||||||
<FolderNode folder={f} {folders} {projects} {slug} />
|
<FolderNode folder={f} {folders} {projects} {slug} {onCreateFolder} {onCreateProject} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
| 'arrow-left'
|
| 'arrow-left'
|
||||||
| 'folder'
|
| 'folder'
|
||||||
| 'file'
|
| 'file'
|
||||||
|
| 'folder-plus'
|
||||||
|
| 'file-plus'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'roles';
|
| 'roles';
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -122,6 +124,22 @@
|
|||||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
{:else if name === 'folder-plus'}
|
||||||
|
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 10.5v6m3-3H9m4.06-7.19l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else if name === 'file-plus'}
|
||||||
|
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
{:else if name === 'check'}
|
{:else if name === 'check'}
|
||||||
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
<svg class={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
|
|||||||
@@ -14,9 +14,7 @@
|
|||||||
import { toastError, toastSuccess } from '$lib/toast';
|
import { toastError, toastSuccess } from '$lib/toast';
|
||||||
|
|
||||||
const slug = $derived(page.params.slug ?? '');
|
const slug = $derived(page.params.slug ?? '');
|
||||||
const org = $derived(
|
const org = $derived(($session.me?.organizations.find((o) => o.slug === slug) as OrgMembership | undefined) ?? null);
|
||||||
($session.me?.organizations.find((o) => o.slug === slug) as OrgMembership | undefined) ?? null,
|
|
||||||
);
|
|
||||||
const isAdmin = $derived(!!org && (org.role === 'OWNER' || org.role === 'ADMIN'));
|
const isAdmin = $derived(!!org && (org.role === 'OWNER' || org.role === 'ADMIN'));
|
||||||
|
|
||||||
let data = $state<ExplorerData | null>(null);
|
let data = $state<ExplorerData | null>(null);
|
||||||
@@ -32,6 +30,18 @@
|
|||||||
let projectName = $state('');
|
let projectName = $state('');
|
||||||
let projectFolder = $state('');
|
let projectFolder = $state('');
|
||||||
|
|
||||||
|
function openFolderModal(parentId: string) {
|
||||||
|
folderName = '';
|
||||||
|
folderParent = parentId;
|
||||||
|
showFolderModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProjectModal(folderId: string) {
|
||||||
|
projectName = '';
|
||||||
|
projectFolder = folderId;
|
||||||
|
showProjectModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
@@ -113,8 +123,8 @@
|
|||||||
{:else if isAdmin && data}
|
{:else if isAdmin && data}
|
||||||
<PageHeader title="项目" description="文件夹是透明组织节点;项目是权限边界。">
|
<PageHeader title="项目" description="文件夹是透明组织节点;项目是权限边界。">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<button class="saas-btn-secondary" onclick={() => (showFolderModal = true)}>新建文件夹</button>
|
<button class="saas-btn-secondary" onclick={() => openFolderModal('')}>新建文件夹</button>
|
||||||
<button class="saas-btn-primary" onclick={() => (showProjectModal = true)}>新建项目</button>
|
<button class="saas-btn-primary" onclick={() => openProjectModal('')}>新建项目</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -122,7 +132,14 @@
|
|||||||
{#if data.projects.filter((p) => !p.folderId).length === 0 && data.folders.filter((f) => !f.parentId).length === 0}
|
{#if data.projects.filter((p) => !p.folderId).length === 0 && data.folders.filter((f) => !f.parentId).length === 0}
|
||||||
<EmptyState title="暂无项目" description="新建文件夹或项目,开始组织你的教研资产。" />
|
<EmptyState title="暂无项目" description="新建文件夹或项目,开始组织你的教研资产。" />
|
||||||
{:else}
|
{:else}
|
||||||
<FolderTree folders={data.folders} projects={data.projects} parentId={null} {slug} />
|
<FolderTree
|
||||||
|
folders={data.folders}
|
||||||
|
projects={data.projects}
|
||||||
|
parentId={null}
|
||||||
|
{slug}
|
||||||
|
onCreateFolder={openFolderModal}
|
||||||
|
onCreateProject={openProjectModal}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user