feat(dashboard): group tree, detail page, and members tab
SvelteKit UI for Phase-2 groups, mirroring the apps/users patterns: - api.ts: Group/GroupDetail/GroupMember types + an api.groups client (list/get/create/update/reparent/delete + nested members CRUD); optional group selector wired into app create. - routes/groups: a collapsible tree overview (assembled from parent_id) with a New-group form, and a detail page with breadcrumb, subgroups/apps lists, a rename form (no slug field — slug is frozen), a reparent control, a delete button that surfaces the 409 RESTRICT message, and a Members tab reusing RoleChip/ConfirmModal/ActionMenu. - +layout.svelte: a Groups nav entry beside Apps. npm run check: 0 errors; npm run build: success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { api, ApiError, type App } from '$lib/api';
|
||||
import { api, ApiError, type App, type Group } from '$lib/api';
|
||||
import { slugify, SLUG_MAX } from '$lib/slugify';
|
||||
import { canCreateApp } from '$lib/capabilities';
|
||||
import { currentUser } from '$lib/auth';
|
||||
@@ -36,6 +36,17 @@
|
||||
let createSlug = $state('');
|
||||
let createName = $state('');
|
||||
let createDescription = $state('');
|
||||
// Optional parent group for the new app (defaults to root). Loaded
|
||||
// lazily; failure leaves the picker empty (root-only).
|
||||
let createGroup = $state('');
|
||||
let groups = $state<Group[]>([]);
|
||||
async function loadGroups() {
|
||||
try {
|
||||
groups = await api.groups.list();
|
||||
} catch {
|
||||
groups = [];
|
||||
}
|
||||
}
|
||||
// Auto-derive slug from name until the user takes manual control of
|
||||
// the slug field. Clearing the slug input releases the lock so the
|
||||
// auto-derive resumes — matches the GitLab project-create UX.
|
||||
@@ -69,6 +80,7 @@
|
||||
listError = null;
|
||||
try {
|
||||
apps = await api.apps.list();
|
||||
void loadGroups();
|
||||
if (apps && apps.length > 0) {
|
||||
void loadDlCounts(apps);
|
||||
}
|
||||
@@ -84,6 +96,7 @@
|
||||
createSlug = '';
|
||||
createName = '';
|
||||
createDescription = '';
|
||||
createGroup = '';
|
||||
createError = null;
|
||||
createHistoricalConflict = null;
|
||||
slugTouched = false;
|
||||
@@ -99,7 +112,8 @@
|
||||
slug: createSlug.trim(),
|
||||
name: createName.trim(),
|
||||
description: createDescription.trim() || null,
|
||||
force_takeover: forceTakeover || undefined
|
||||
force_takeover: forceTakeover || undefined,
|
||||
group: createGroup.trim() || undefined
|
||||
});
|
||||
showCreate = false;
|
||||
resetCreate();
|
||||
@@ -172,6 +186,15 @@
|
||||
<span>Description</span>
|
||||
<input bind:value={createDescription} placeholder="optional" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Group (optional)</span>
|
||||
<select bind:value={createGroup}>
|
||||
<option value="">— root —</option>
|
||||
{#each groups as g (g.id)}
|
||||
<option value={g.slug}>{g.name} (/{g.slug})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{#if createHistoricalConflict}
|
||||
<div class="warning">
|
||||
<strong>Slug previously redirected.</strong>
|
||||
|
||||
Reference in New Issue
Block a user