spine-framework 0.3.125 → 0.3.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.framework/cli/commands/agents.ts +35 -37
- package/.framework/cli/commands/auth.ts +35 -34
- package/.framework/cli/commands/create-app.ts +104 -40
- package/.framework/cli/commands/dev.ts +104 -69
- package/.framework/cli/commands/doctor.ts +169 -97
- package/.framework/cli/commands/generate.ts +82 -160
- package/.framework/cli/commands/init.ts +72 -34
- package/.framework/cli/commands/install-app.ts +260 -125
- package/.framework/cli/commands/items.ts +33 -44
- package/.framework/cli/commands/migrate.ts +122 -40
- package/.framework/cli/commands/migrations.ts +33 -40
- package/.framework/cli/commands/pipelines.ts +31 -39
- package/.framework/cli/commands/status.ts +47 -23
- package/.framework/cli/commands/system.ts +47 -37
- package/.framework/cli/commands/test.ts +112 -68
- package/.framework/cli/commands/validate-app.ts +215 -121
- package/.framework/cli/context.ts +112 -130
- package/.framework/cli/env-loader.ts +47 -18
- package/.framework/cli/index.ts +81 -42
- package/.framework/docs/runtime-types.md +1024 -0
- package/.framework/functions/_shared/agent-runner.ts +1296 -961
- package/.framework/functions/_shared/app-manifest.ts +82 -68
- package/.framework/functions/_shared/audit.ts +135 -83
- package/.framework/functions/_shared/{cil-state.ts → cil.ts} +147 -54
- package/.framework/functions/_shared/db.ts +80 -126
- package/.framework/functions/_shared/hash-api-key.ts +63 -0
- package/.framework/functions/_shared/index.ts +222 -347
- package/.framework/functions/_shared/kb.ts +319 -0
- package/.framework/functions/_shared/llm.ts +92 -0
- package/.framework/functions/_shared/logging.ts +288 -0
- package/.framework/functions/_shared/middleware.ts +329 -365
- package/.framework/functions/_shared/outcomes.ts +36 -0
- package/.framework/functions/_shared/permissions.ts +359 -643
- package/.framework/functions/_shared/pipeline-runner.ts +243 -316
- package/.framework/functions/_shared/principal.ts +376 -468
- package/.framework/functions/_shared/resolve-credentials.ts +102 -0
- package/.framework/functions/_shared/resolve-ids.ts +162 -14
- package/.framework/functions/_shared/retry-deliveries.ts +104 -0
- package/.framework/functions/_shared/schema-utils.ts +611 -373
- package/.framework/functions/_shared/testing.ts +207 -137
- package/.framework/functions/_shared/tracing.ts +276 -0
- package/.framework/functions/_shared/trigger-engine.ts +159 -211
- package/.framework/functions/_shared/webhook-registration.ts +95 -81
- package/.framework/functions/_shared/webhook-registry.ts +153 -75
- package/.framework/functions/account-nodes.ts +31 -53
- package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
- package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
- package/.framework/functions/adapters/openai-outbound.ts +40 -0
- package/.framework/functions/admin-data.ts +687 -533
- package/.framework/functions/admin-funnel.ts +31 -20
- package/.framework/functions/admin-install-funnel.ts +47 -28
- package/.framework/functions/admin-signals.ts +47 -29
- package/.framework/functions/ai-agents.ts +122 -147
- package/.framework/functions/ai-interactions.ts +180 -90
- package/.framework/functions/api-keys.ts +250 -192
- package/.framework/functions/apps.ts +203 -197
- package/.framework/functions/auth.ts +98 -91
- package/.framework/functions/cil-agent-ingest.ts +773 -0
- package/.framework/functions/cil-agent.ts +592 -0
- package/.framework/functions/cil-callmap.ts +1227 -0
- package/.framework/functions/cil-config.ts +583 -0
- package/.framework/functions/cil-context.ts +445 -0
- package/.framework/functions/cil-doc.ts +1752 -0
- package/.framework/functions/cil-embeddings.ts +1202 -0
- package/.framework/functions/cil-graph.ts +585 -0
- package/.framework/functions/cil-ingest.ts +2416 -0
- package/.framework/functions/cil-pipeline.ts +318 -0
- package/.framework/functions/cil-project.ts +499 -0
- package/.framework/functions/cil-validate.ts +469 -0
- package/.framework/functions/debug-auth.ts +73 -53
- package/.framework/functions/deliveries.ts +117 -0
- package/.framework/functions/embeddings.ts +528 -516
- package/.framework/functions/eval-harness.ts +158 -106
- package/.framework/functions/integration-outbound.ts +311 -0
- package/.framework/functions/integration-routes.ts +240 -466
- package/.framework/functions/integrations.ts +132 -140
- package/.framework/functions/invites.ts +401 -201
- package/.framework/functions/item-progress.ts +113 -53
- package/.framework/functions/kb.ts +28 -0
- package/.framework/functions/logs.ts +239 -222
- package/.framework/functions/observability.ts +308 -259
- package/.framework/functions/pipeline-executions.ts +179 -235
- package/.framework/functions/pipelines.ts +163 -205
- package/.framework/functions/prompt-configs.ts +109 -156
- package/.framework/functions/roles.ts +97 -147
- package/.framework/functions/system-cron.ts +132 -218
- package/.framework/functions/system.ts +112 -52
- package/.framework/functions/tests.ts +32 -28
- package/.framework/functions/timers.ts +162 -210
- package/.framework/functions/triggers.ts +282 -346
- package/.framework/functions/types.ts +283 -400
- package/.framework/migrations/001_core_schema.sql +630 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
- package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
- package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
- package/.framework/migrations/005_core_observability.sql +598 -0
- package/.framework/migrations/006_security_hardening_rls.sql +97 -0
- package/.framework/migrations/007_kb_article_list.sql +237 -0
- package/.framework/migrations/008_items_target.sql +8 -0
- package/.framework/migrations/009_roles_slug_unique.sql +11 -0
- package/.framework/public/spine.config.json +91 -0
- package/.framework/scripts/app-install-cli.ts +112 -98
- package/.framework/src/App.tsx +67 -20
- package/.framework/src/apps/admin/index.tsx +97 -27
- package/.framework/src/components/AppWrapper.tsx +50 -11
- package/.framework/src/components/CustomAppLoader.tsx +131 -22
- package/.framework/src/components/admin/AdminListPage.tsx +75 -8
- package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
- package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
- package/.framework/src/components/admin/PageInstructions.tsx +49 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
- package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
- package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
- package/.framework/src/components/app-sidebar.tsx +50 -3
- package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
- package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
- package/.framework/src/components/kb/KBArticleList.tsx +557 -0
- package/.framework/src/components/layout/AppShell.tsx +54 -16
- package/.framework/src/components/layout/Header.tsx +50 -38
- package/.framework/src/components/layout/Layout.tsx +48 -32
- package/.framework/src/components/layout/Sidebar.tsx +167 -16
- package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
- package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
- package/.framework/src/components/runtime/DataFilters.tsx +78 -1
- package/.framework/src/components/runtime/DataHeader.tsx +32 -29
- package/.framework/src/components/runtime/DataListPage.tsx +161 -2
- package/.framework/src/components/runtime/DataStats.tsx +48 -1
- package/.framework/src/components/runtime/DataTable.tsx +153 -1
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
- package/.framework/src/components/runtime/index.ts +16 -9
- package/.framework/src/components/search-form.tsx +33 -1
- package/.framework/src/components/shared/AgentView.tsx +48 -51
- package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
- package/.framework/src/components/shared/InviteManager.tsx +113 -52
- package/.framework/src/components/shared/MemberList.tsx +182 -0
- package/.framework/src/components/shared/SchemaFields.tsx +96 -78
- package/.framework/src/components/ui/DataTable.tsx +86 -56
- package/.framework/src/components/ui/Form.tsx +188 -109
- package/.framework/src/components/ui/ItemCard.tsx +86 -58
- package/.framework/src/components/ui/ItemListView.tsx +88 -61
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
- package/.framework/src/components/ui/Modal.tsx +81 -4
- package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
- package/.framework/src/components/ui/accordion.tsx +97 -5
- package/.framework/src/components/ui/alert-dialog.tsx +225 -13
- package/.framework/src/components/ui/alert.tsx +113 -6
- package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
- package/.framework/src/components/ui/avatar.tsx +129 -7
- package/.framework/src/components/ui/badge.tsx +65 -3
- package/.framework/src/components/ui/breadcrumb.tsx +145 -8
- package/.framework/src/components/ui/button-group.tsx +81 -4
- package/.framework/src/components/ui/button.tsx +49 -2
- package/.framework/src/components/ui/calendar.tsx +65 -3
- package/.framework/src/components/ui/card.tsx +145 -8
- package/.framework/src/components/ui/carousel.tsx +129 -7
- package/.framework/src/components/ui/chart.tsx +161 -10
- package/.framework/src/components/ui/checkbox.tsx +49 -2
- package/.framework/src/components/ui/collapsible.tsx +81 -5
- package/.framework/src/components/ui/combobox.tsx +305 -20
- package/.framework/src/components/ui/command.tsx +177 -11
- package/.framework/src/components/ui/context-menu.tsx +273 -17
- package/.framework/src/components/ui/dialog.tsx +209 -27
- package/.framework/src/components/ui/direction.tsx +64 -4
- package/.framework/src/components/ui/drawer.tsx +193 -12
- package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
- package/.framework/src/components/ui/empty.tsx +145 -9
- package/.framework/src/components/ui/field.tsx +209 -14
- package/.framework/src/components/ui/hover-card.tsx +81 -4
- package/.framework/src/components/ui/input-group.tsx +162 -10
- package/.framework/src/components/ui/input-otp.tsx +98 -5
- package/.framework/src/components/ui/input.tsx +49 -2
- package/.framework/src/components/ui/item.tsx +225 -13
- package/.framework/src/components/ui/kbd.tsx +65 -3
- package/.framework/src/components/ui/label.tsx +49 -2
- package/.framework/src/components/ui/menubar.tsx +289 -17
- package/.framework/src/components/ui/native-select.tsx +97 -5
- package/.framework/src/components/ui/navigation-menu.tsx +177 -10
- package/.framework/src/components/ui/pagination.tsx +161 -9
- package/.framework/src/components/ui/popover.tsx +145 -8
- package/.framework/src/components/ui/progress.tsx +49 -2
- package/.framework/src/components/ui/radio-group.tsx +65 -3
- package/.framework/src/components/ui/resizable.tsx +82 -4
- package/.framework/src/components/ui/scroll-area.tsx +65 -3
- package/.framework/src/components/ui/select.tsx +193 -11
- package/.framework/src/components/ui/separator.tsx +49 -2
- package/.framework/src/components/ui/sheet.tsx +193 -11
- package/.framework/src/components/ui/sidebar.tsx +481 -38
- package/.framework/src/components/ui/skeleton.tsx +49 -2
- package/.framework/src/components/ui/slider.tsx +49 -2
- package/.framework/src/components/ui/sonner.tsx +49 -2
- package/.framework/src/components/ui/spinner.tsx +49 -2
- package/.framework/src/components/ui/switch.tsx +49 -2
- package/.framework/src/components/ui/table-primitives.tsx +160 -33
- package/.framework/src/components/ui/table.tsx +161 -9
- package/.framework/src/components/ui/tabs.tsx +113 -6
- package/.framework/src/components/ui/textarea.tsx +49 -2
- package/.framework/src/components/ui/toggle-group.tsx +81 -4
- package/.framework/src/components/ui/toggle.tsx +65 -3
- package/.framework/src/components/ui/tooltip.tsx +97 -5
- package/.framework/src/contexts/AppContext.tsx +129 -45
- package/.framework/src/contexts/AuthContext.tsx +173 -145
- package/.framework/src/hooks/use-mobile.ts +48 -2
- package/.framework/src/hooks/useApi.ts +208 -159
- package/.framework/src/hooks/useAppPath.ts +32 -23
- package/.framework/src/hooks/useApps.ts +51 -20
- package/.framework/src/hooks/useEntityList.ts +32 -78
- package/.framework/src/hooks/useEntityRecord.ts +35 -96
- package/.framework/src/hooks/useForm.ts +32 -92
- package/.framework/src/hooks/useListSchema.ts +49 -78
- package/.framework/src/hooks/useSchemaRecord.ts +114 -105
- package/.framework/src/lib/api.ts +192 -115
- package/.framework/src/lib/supabase.ts +98 -64
- package/.framework/src/lib/utils.ts +287 -151
- package/.framework/src/main.tsx +49 -3
- package/.framework/src/pages/DashboardPage.tsx +52 -29
- package/.framework/src/pages/NotFoundPage.tsx +32 -10
- package/.framework/src/pages/SetupPage.tsx +32 -12
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
- package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
- package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
- package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
- package/.framework/src/pages/admin/AppsPage.tsx +49 -2
- package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
- package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
- package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
- package/.framework/src/pages/admin/LogsPage.tsx +422 -75
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
- package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
- package/.framework/src/pages/admin/RolesPage.tsx +171 -18
- package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
- package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
- package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
- package/.framework/src/pages/admin/TimersPage.tsx +48 -20
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
- package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
- package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
- package/.framework/src/pages/admin/TypesPage.tsx +48 -20
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
- package/.framework/src/pages/auth/LoginPage.tsx +50 -23
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
- package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
- package/.framework/src/types/auth.ts +128 -71
- package/.framework/src/types/types.ts +373 -157
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
- package/docs/README.md +9 -130
- package/netlify.toml +3 -3
- package/package.json +13 -25
- package/scripts/kb-ingest-docs.ts +203 -0
- package/spine.config.json +65 -4
- package/.framework/migrations/001_schema.sql +0 -425
- package/.framework/migrations/002_rls_and_functions.sql +0 -763
- package/.framework/migrations/004_enhancements.sql +0 -43
- package/.framework/migrations/005_auth_user_trigger.sql +0 -105
- package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
- package/.framework/migrations/016_invites.sql +0 -27
- package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
- package/.framework/migrations/018_apps_required_roles.sql +0 -17
- package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
- package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
- package/.framework/migrations/021_app_domains.sql +0 -112
- package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
- package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
- package/.framework/migrations/024_messages_account_id.sql +0 -9
- package/.framework/migrations/025_retype_threads_messages.sql +0 -240
- package/.framework/migrations/026_fix_rls_policies.sql +0 -47
- package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
- package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
- package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
- package/.framework/migrations/030_messages_updated_by.sql +0 -6
- package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
- package/docs/APPS.md +0 -232
- package/docs/ARCHITECTURE.md +0 -246
- package/docs/INSTALL.md +0 -155
- package/docs/design-schema-spec.md +0 -1837
- package/docs/dev-guides/01-create-an-app.md +0 -183
- package/docs/dev-guides/02-create-a-function.md +0 -178
- package/docs/dev-guides/03-create-a-component.md +0 -178
- package/docs/dev-guides/04-webhook-handlers.md +0 -129
- package/docs/dev-guides/05-testing.md +0 -168
- package/docs/dev-guides/README.md +0 -40
- package/docs/npm-packages-plan.md +0 -288
- package/docs/spine-core--api-reference.md +0 -822
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { apiFetch } from '../../lib/api'
|
|
3
|
+
import { formatDateTime } from '../../lib/utils'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { Button } from '../ui/button'
|
|
6
|
+
import { Input } from '../ui/input'
|
|
7
|
+
import { Badge } from '../ui/badge'
|
|
8
|
+
import { Skeleton } from '../ui/skeleton'
|
|
9
|
+
import { Plus, Search, Edit, BookOpen, GripVertical, X } from 'lucide-react'
|
|
10
|
+
|
|
11
|
+
export interface KBArticleListProps {
|
|
12
|
+
typeSlugs: string[]
|
|
13
|
+
typeName?: string
|
|
14
|
+
onSelect?: (record: KBRecord) => void
|
|
15
|
+
onNew?: () => void
|
|
16
|
+
onEdit?: (record: KBRecord) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface KBRecord {
|
|
20
|
+
id: string
|
|
21
|
+
title: string
|
|
22
|
+
description?: string
|
|
23
|
+
status: string
|
|
24
|
+
type_id: string
|
|
25
|
+
type_slug?: string
|
|
26
|
+
created_at: string
|
|
27
|
+
data: Record<string, any>
|
|
28
|
+
[key: string]: any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TreeNode extends KBRecord {
|
|
32
|
+
children: TreeNode[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNullId(a: string | null | undefined): a is null | undefined {
|
|
36
|
+
return a === null || a === undefined || a === ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sameId(a: string | null | undefined, b: string | null | undefined): boolean {
|
|
40
|
+
return a === b || (isNullId(a) && isNullId(b))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sameParent(record: KBRecord, parentId: string | undefined): boolean {
|
|
44
|
+
return sameId(record.data?.parent_id as string | undefined, parentId)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sortByOrder(a: KBRecord, b: KBRecord): number {
|
|
48
|
+
return (a.data?.order ?? Number.MAX_SAFE_INTEGER) - (b.data?.order ?? Number.MAX_SAFE_INTEGER)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function KBArticleList({ typeSlugs, typeName, onSelect, onNew, onEdit }: KBArticleListProps) {
|
|
52
|
+
const [typeRecords, setTypeRecords] = useState<any[]>([])
|
|
53
|
+
const [articles, setArticles] = useState<KBRecord[]>([])
|
|
54
|
+
const [loading, setLoading] = useState(true)
|
|
55
|
+
const [error, setError] = useState<string | null>(null)
|
|
56
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
57
|
+
const [searchResultIds, setSearchResultIds] = useState<Set<string> | null>(null)
|
|
58
|
+
const [searching, setSearching] = useState(false)
|
|
59
|
+
const [statusFilter, setStatusFilter] = useState<string>('all')
|
|
60
|
+
const [typeFilter, setTypeFilter] = useState<string>('all')
|
|
61
|
+
const [dragging, setDragging] = useState<string | null>(null)
|
|
62
|
+
|
|
63
|
+
const typeById = useMemo(() => {
|
|
64
|
+
const map: Record<string, any> = {}
|
|
65
|
+
typeRecords.forEach((t) => { map[t.id] = t })
|
|
66
|
+
return map
|
|
67
|
+
}, [typeRecords])
|
|
68
|
+
|
|
69
|
+
const typeSlugsSet = useMemo(() => new Set(typeSlugs), [typeSlugs])
|
|
70
|
+
|
|
71
|
+
const statusOptions = useMemo(() => {
|
|
72
|
+
const opts = new Set<string>()
|
|
73
|
+
typeRecords.forEach((t) => {
|
|
74
|
+
const options = t.design_schema?.fields?.status?.options || ['draft', 'published', 'archived']
|
|
75
|
+
options.forEach((o: string) => opts.add(o))
|
|
76
|
+
})
|
|
77
|
+
return ['all', ...Array.from(opts)]
|
|
78
|
+
}, [typeRecords])
|
|
79
|
+
|
|
80
|
+
const statusLabel = (s: string) => s === 'all' ? 'All' : (s?.charAt(0).toUpperCase() + s?.slice(1))
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let cancelled = false
|
|
84
|
+
setLoading(true)
|
|
85
|
+
apiFetch('/api/types?action=list')
|
|
86
|
+
.then((r) => r.json())
|
|
87
|
+
.then((j) => {
|
|
88
|
+
if (cancelled) return
|
|
89
|
+
const found = (j.data || []).filter((t: any) => typeSlugsSet.has(t.slug))
|
|
90
|
+
setTypeRecords(found)
|
|
91
|
+
})
|
|
92
|
+
.catch(() => setTypeRecords([]))
|
|
93
|
+
return () => { cancelled = true }
|
|
94
|
+
}, [typeSlugsSet])
|
|
95
|
+
|
|
96
|
+
const loadTree = useCallback(async () => {
|
|
97
|
+
if (typeRecords.length === 0) return
|
|
98
|
+
setLoading(true)
|
|
99
|
+
setError(null)
|
|
100
|
+
try {
|
|
101
|
+
const loadCache = async (title: string): Promise<Record<string, any[]>> => {
|
|
102
|
+
const res = await apiFetch(
|
|
103
|
+
`/api/admin-data?action=list&entity=items&type_slug=kb-article-list&title=${encodeURIComponent(title)}&limit=1&offset=0`
|
|
104
|
+
)
|
|
105
|
+
const json = await res.json()
|
|
106
|
+
const records = Array.isArray(json) ? json : (json.data || [])
|
|
107
|
+
const record = records[0]
|
|
108
|
+
return record?.data?.trees || {}
|
|
109
|
+
}
|
|
110
|
+
const [globalTrees, accountTrees] = await Promise.all([
|
|
111
|
+
loadCache('KB Article List - Global'),
|
|
112
|
+
loadCache('KB Article List - Account'),
|
|
113
|
+
])
|
|
114
|
+
const allTrees = { ...globalTrees, ...accountTrees }
|
|
115
|
+
const flat = typeSlugs.flatMap((slug) => allTrees[slug] || [])
|
|
116
|
+
setArticles(flat.map((r: any) => ({
|
|
117
|
+
...r,
|
|
118
|
+
type_slug: r.type_slug || typeById[r.type_id]?.slug,
|
|
119
|
+
})))
|
|
120
|
+
} catch (e: any) {
|
|
121
|
+
setError(e.message || 'Failed to load articles')
|
|
122
|
+
setArticles([])
|
|
123
|
+
} finally {
|
|
124
|
+
setLoading(false)
|
|
125
|
+
}
|
|
126
|
+
}, [typeRecords, typeSlugs, typeById])
|
|
127
|
+
|
|
128
|
+
const handleSearch = useCallback(async (query: string) => {
|
|
129
|
+
if (typeRecords.length === 0 || query.trim().length < 2) {
|
|
130
|
+
setSearchResultIds(null)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
setSearching(true)
|
|
134
|
+
setError(null)
|
|
135
|
+
try {
|
|
136
|
+
const res = await apiFetch('/api/kb?action=search', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
query: query.trim(),
|
|
141
|
+
type_slugs: typeSlugs,
|
|
142
|
+
sources: ['kb'],
|
|
143
|
+
limit: 100,
|
|
144
|
+
similarity_threshold: 0.4,
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
const json = await res.json()
|
|
148
|
+
console.log('[KBArticleList] search json:', json)
|
|
149
|
+
const results = json.data?.results || json.results || []
|
|
150
|
+
const ids = new Set<string>(results.map((r: any) => r.id).filter(Boolean))
|
|
151
|
+
console.log('[KBArticleList] search results:', results.length, 'ids:', ids.size)
|
|
152
|
+
setSearchResultIds(ids.size > 0 ? ids : new Set())
|
|
153
|
+
} catch (e: any) {
|
|
154
|
+
setError(e.message || 'Search failed')
|
|
155
|
+
setSearchResultIds(null)
|
|
156
|
+
} finally {
|
|
157
|
+
setSearching(false)
|
|
158
|
+
}
|
|
159
|
+
}, [typeRecords, typeSlugs])
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
loadTree()
|
|
163
|
+
}, [loadTree])
|
|
164
|
+
|
|
165
|
+
const hasActiveSearch = searchResultIds !== null
|
|
166
|
+
|
|
167
|
+
const { visibleArticles, roots, byId } = useMemo(() => {
|
|
168
|
+
let out = articles
|
|
169
|
+
if (statusFilter !== 'all') out = out.filter((a) => a.status === statusFilter)
|
|
170
|
+
if (typeFilter !== 'all') out = out.filter((a) => a.type_slug === typeFilter || a.data?.kb_type === typeFilter)
|
|
171
|
+
|
|
172
|
+
const fullById = new Map<string, KBRecord>()
|
|
173
|
+
for (const a of articles) fullById.set(a.id, a)
|
|
174
|
+
|
|
175
|
+
if (searchResultIds) {
|
|
176
|
+
const ancestorSet = new Set<string>(searchResultIds)
|
|
177
|
+
for (const id of searchResultIds) {
|
|
178
|
+
let current = fullById.get(id)
|
|
179
|
+
while (current?.data?.parent_id) {
|
|
180
|
+
const parentId = current.data.parent_id as string
|
|
181
|
+
if (ancestorSet.has(parentId)) break
|
|
182
|
+
ancestorSet.add(parentId)
|
|
183
|
+
current = fullById.get(parentId)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
out = out.filter((a) => ancestorSet.has(a.id))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const nodes: TreeNode[] = out.map((a) => ({ ...a, children: [] }))
|
|
190
|
+
const byId = new Map<string, TreeNode>()
|
|
191
|
+
const idSet = new Set<string>()
|
|
192
|
+
for (const n of nodes) {
|
|
193
|
+
byId.set(n.id, n)
|
|
194
|
+
idSet.add(n.id)
|
|
195
|
+
}
|
|
196
|
+
const roots: TreeNode[] = []
|
|
197
|
+
for (const n of nodes) {
|
|
198
|
+
const parentId = n.data?.parent_id as string | null | undefined
|
|
199
|
+
if (parentId && idSet.has(parentId)) {
|
|
200
|
+
const parent = byId.get(parentId)
|
|
201
|
+
if (parent) parent.children.push(n)
|
|
202
|
+
} else {
|
|
203
|
+
roots.push(n)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const node of byId.values()) {
|
|
207
|
+
node.children = node.children.sort(sortByOrder)
|
|
208
|
+
}
|
|
209
|
+
return { visibleArticles: out, roots: roots.sort(sortByOrder), byId }
|
|
210
|
+
}, [articles, statusFilter, typeFilter, searchResultIds])
|
|
211
|
+
|
|
212
|
+
const isTreeMode = useMemo(
|
|
213
|
+
() => statusFilter === 'all' && typeFilter === 'all',
|
|
214
|
+
[statusFilter, typeFilter]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const handleDrop = async (itemId: string, targetParentId: string | undefined, insertIndex: number) => {
|
|
218
|
+
const item = articles.find((a) => a.id === itemId)
|
|
219
|
+
if (!item) return
|
|
220
|
+
setDragging(null)
|
|
221
|
+
|
|
222
|
+
const oldParentId = item.data?.parent_id as string | undefined
|
|
223
|
+
const isSameGroup = sameId(oldParentId, targetParentId)
|
|
224
|
+
let effectiveIndex = insertIndex
|
|
225
|
+
|
|
226
|
+
if (isSameGroup) {
|
|
227
|
+
const fullTargetGroup = [...articles]
|
|
228
|
+
.filter((a) => sameParent(a, targetParentId))
|
|
229
|
+
.sort(sortByOrder)
|
|
230
|
+
const originalIndex = fullTargetGroup.findIndex((a) => a.id === itemId)
|
|
231
|
+
if (originalIndex !== -1 && insertIndex > originalIndex) {
|
|
232
|
+
effectiveIndex = insertIndex - 1
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const targetChildren = articles
|
|
237
|
+
.filter((a) => sameParent(a, targetParentId) && a.id !== itemId)
|
|
238
|
+
.sort(sortByOrder)
|
|
239
|
+
const newGroup = [...targetChildren]
|
|
240
|
+
newGroup.splice(effectiveIndex, 0, item)
|
|
241
|
+
|
|
242
|
+
const updates: { id: string; type_slug: string; data: any }[] = []
|
|
243
|
+
|
|
244
|
+
newGroup.forEach((a, order) => {
|
|
245
|
+
const nextData = { ...a.data, order }
|
|
246
|
+
if (a.id === itemId) {
|
|
247
|
+
nextData.parent_id = targetParentId ?? null
|
|
248
|
+
}
|
|
249
|
+
const parentChanged = a.id === itemId && !sameId(a.data?.parent_id as string | undefined, targetParentId)
|
|
250
|
+
if (a.data?.order !== order || parentChanged) {
|
|
251
|
+
updates.push({
|
|
252
|
+
id: a.id,
|
|
253
|
+
type_slug: a.type_slug || typeById[a.type_id]?.slug || 'kb-spine-public',
|
|
254
|
+
data: nextData,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (!isSameGroup) {
|
|
260
|
+
const sourceChildren = articles
|
|
261
|
+
.filter((a) => sameParent(a, oldParentId) && a.id !== itemId)
|
|
262
|
+
.sort(sortByOrder)
|
|
263
|
+
sourceChildren.forEach((a, order) => {
|
|
264
|
+
if (a.data?.order !== order) {
|
|
265
|
+
updates.push({
|
|
266
|
+
id: a.id,
|
|
267
|
+
type_slug: a.type_slug || typeById[a.type_id]?.slug || 'kb-spine-public',
|
|
268
|
+
data: { ...a.data, order },
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (updates.length === 0) return
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await Promise.all(
|
|
278
|
+
updates.map((u) =>
|
|
279
|
+
apiFetch(`/api/admin-data?entity=items&id=${u.id}`, {
|
|
280
|
+
method: 'PATCH',
|
|
281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ type_slug: u.type_slug, data: u.data }),
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
await loadTree()
|
|
287
|
+
} catch (e: any) {
|
|
288
|
+
setError(e.message || 'Failed to update order')
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const DropIndicator = () => (
|
|
293
|
+
<div className="h-0.5 bg-primary rounded-full my-1" />
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
function getDropIndex(e: React.DragEvent, container: HTMLElement | null): number {
|
|
297
|
+
if (!container) return 0
|
|
298
|
+
const itemEls = Array.from(container.children).filter((c) => c.getAttribute('data-item') === 'true')
|
|
299
|
+
if (itemEls.length === 0) return 0
|
|
300
|
+
const containerRect = container.getBoundingClientRect()
|
|
301
|
+
const mouseY = e.clientY - containerRect.top
|
|
302
|
+
for (let i = 0; i < itemEls.length; i++) {
|
|
303
|
+
const rect = itemEls[i].getBoundingClientRect()
|
|
304
|
+
const centerY = rect.top + rect.height / 2 - containerRect.top
|
|
305
|
+
if (mouseY < centerY) return i
|
|
306
|
+
}
|
|
307
|
+
return itemEls.length
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function SortableDropZone({
|
|
311
|
+
parentId,
|
|
312
|
+
nodes,
|
|
313
|
+
depth,
|
|
314
|
+
}: {
|
|
315
|
+
parentId: string | undefined
|
|
316
|
+
nodes: TreeNode[]
|
|
317
|
+
depth: number
|
|
318
|
+
}): JSX.Element {
|
|
319
|
+
const [over, setOver] = useState(false)
|
|
320
|
+
const [insertIndex, setInsertIndex] = useState<number | null>(null)
|
|
321
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
322
|
+
|
|
323
|
+
const computeIndex = useCallback(
|
|
324
|
+
(e: React.DragEvent) => getDropIndex(e, listRef.current),
|
|
325
|
+
[]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div
|
|
330
|
+
ref={listRef}
|
|
331
|
+
className={cn(
|
|
332
|
+
'space-y-0.5',
|
|
333
|
+
depth > 0 ? 'pl-4 border-l border-border/50' : '',
|
|
334
|
+
over && 'rounded-md bg-primary/5',
|
|
335
|
+
depth > 0 && nodes.length === 0 ? 'min-h-[1.5rem]' : ''
|
|
336
|
+
)}
|
|
337
|
+
onDragEnter={(e) => {
|
|
338
|
+
e.preventDefault()
|
|
339
|
+
e.stopPropagation()
|
|
340
|
+
if (!e.relatedTarget || !listRef.current?.contains(e.relatedTarget as Node)) {
|
|
341
|
+
setOver(true)
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
onDragLeave={(e) => {
|
|
345
|
+
e.preventDefault()
|
|
346
|
+
e.stopPropagation()
|
|
347
|
+
if (!e.relatedTarget || !listRef.current?.contains(e.relatedTarget as Node)) {
|
|
348
|
+
setOver(false)
|
|
349
|
+
setInsertIndex(null)
|
|
350
|
+
}
|
|
351
|
+
}}
|
|
352
|
+
onDragOver={(e) => {
|
|
353
|
+
e.preventDefault()
|
|
354
|
+
e.stopPropagation()
|
|
355
|
+
e.dataTransfer.dropEffect = 'move'
|
|
356
|
+
setInsertIndex(computeIndex(e))
|
|
357
|
+
}}
|
|
358
|
+
onDrop={(e) => {
|
|
359
|
+
e.preventDefault()
|
|
360
|
+
e.stopPropagation()
|
|
361
|
+
setOver(false)
|
|
362
|
+
const id = e.dataTransfer.getData('text/plain')
|
|
363
|
+
const index = computeIndex(e)
|
|
364
|
+
setInsertIndex(null)
|
|
365
|
+
if (id) handleDrop(id, parentId, index)
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{nodes.length === 0 && over && (
|
|
369
|
+
<div className="text-xs text-muted-foreground py-2 px-3 border border-dashed border-border/60 rounded-md text-center">
|
|
370
|
+
Drop here as child
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
{nodes.map((node, i) => (
|
|
374
|
+
<Fragment key={node.id}>
|
|
375
|
+
{insertIndex === i && <DropIndicator />}
|
|
376
|
+
<div
|
|
377
|
+
data-item="true"
|
|
378
|
+
draggable={isTreeMode && !hasActiveSearch}
|
|
379
|
+
onDragStart={(e) => {
|
|
380
|
+
setDragging(node.id)
|
|
381
|
+
e.dataTransfer.setData('text/plain', node.id)
|
|
382
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
383
|
+
}}
|
|
384
|
+
onDragEnd={() => setDragging(null)}
|
|
385
|
+
onDragOver={(e) => {
|
|
386
|
+
e.preventDefault()
|
|
387
|
+
e.dataTransfer.dropEffect = 'move'
|
|
388
|
+
}}
|
|
389
|
+
onDrop={(e) => {
|
|
390
|
+
e.preventDefault()
|
|
391
|
+
e.stopPropagation()
|
|
392
|
+
const id = e.dataTransfer.getData('text/plain')
|
|
393
|
+
if (id) handleDrop(id, node.id, node.children.length)
|
|
394
|
+
}}
|
|
395
|
+
className={cn(
|
|
396
|
+
'group flex items-center gap-2 rounded-md py-1.5 px-2 transition-colors select-none',
|
|
397
|
+
dragging === node.id ? 'opacity-50' : 'hover:bg-accent/50',
|
|
398
|
+
isTreeMode ? 'cursor-grab active:cursor-grabbing' : ''
|
|
399
|
+
)}
|
|
400
|
+
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
401
|
+
>
|
|
402
|
+
{isTreeMode && (
|
|
403
|
+
<GripVertical size={14} className="shrink-0 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
|
404
|
+
)}
|
|
405
|
+
<span className="flex-1 text-left text-sm truncate">
|
|
406
|
+
{node.title}
|
|
407
|
+
</span>
|
|
408
|
+
<Badge variant="outline" className="text-[0.625rem] shrink-0">
|
|
409
|
+
{node.type_slug || '—'}
|
|
410
|
+
</Badge>
|
|
411
|
+
{onEdit && (
|
|
412
|
+
<Button
|
|
413
|
+
variant="ghost"
|
|
414
|
+
size="sm"
|
|
415
|
+
className="h-6 w-6 p-0 shrink-0"
|
|
416
|
+
onClick={() => onEdit(node)}
|
|
417
|
+
>
|
|
418
|
+
<Edit size={14} />
|
|
419
|
+
</Button>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
<SortableDropZone parentId={node.id} nodes={node.children} depth={depth + 1} />
|
|
423
|
+
</Fragment>
|
|
424
|
+
))}
|
|
425
|
+
{insertIndex === nodes.length && <DropIndicator />}
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const renderFlatRow = (record: KBRecord) => (
|
|
431
|
+
<div
|
|
432
|
+
key={record.id}
|
|
433
|
+
className="flex items-center gap-3 py-2 px-2 hover:bg-accent/50 rounded-md"
|
|
434
|
+
>
|
|
435
|
+
<button onClick={() => onSelect?.(record)} className="flex-1 text-left text-sm truncate">
|
|
436
|
+
{record.title}
|
|
437
|
+
</button>
|
|
438
|
+
<Badge variant={record.status === 'published' ? 'default' : 'secondary'} className="text-[0.625rem]">
|
|
439
|
+
{record.status}
|
|
440
|
+
</Badge>
|
|
441
|
+
<Badge variant="outline" className="text-[0.625rem]">
|
|
442
|
+
{record.type_slug || '—'}
|
|
443
|
+
</Badge>
|
|
444
|
+
<span className="text-xs text-muted-foreground hidden sm:inline">
|
|
445
|
+
{formatDateTime(record.created_at)}
|
|
446
|
+
</span>
|
|
447
|
+
{onEdit && (
|
|
448
|
+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => onEdit(record)}>
|
|
449
|
+
<Edit size={14} />
|
|
450
|
+
</Button>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<div className="p-6 h-full overflow-y-auto space-y-4">
|
|
457
|
+
<div className="flex items-center justify-between">
|
|
458
|
+
<h1 className="text-2xl font-semibold flex items-center gap-2">
|
|
459
|
+
<BookOpen className="w-6 h-6" />
|
|
460
|
+
{typeName || 'Knowledge Base'}
|
|
461
|
+
</h1>
|
|
462
|
+
{onNew && (
|
|
463
|
+
<Button onClick={onNew}>
|
|
464
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
465
|
+
New Article
|
|
466
|
+
</Button>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
471
|
+
<div className="relative flex-1 max-w-md flex items-center gap-2">
|
|
472
|
+
<div className="relative flex-1">
|
|
473
|
+
<Search className="absolute left-2.5 top-2 h-4 w-4 text-muted-foreground" />
|
|
474
|
+
<Input
|
|
475
|
+
placeholder="Search articles…"
|
|
476
|
+
value={searchQuery}
|
|
477
|
+
onChange={(e) => { setSearchQuery(e.target.value) }}
|
|
478
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch(searchQuery) }}
|
|
479
|
+
className="pl-8"
|
|
480
|
+
/>
|
|
481
|
+
{searchQuery && (
|
|
482
|
+
<button
|
|
483
|
+
onClick={() => { setSearchQuery(''); setSearchResultIds(null) }}
|
|
484
|
+
className="absolute right-2.5 top-2 text-muted-foreground hover:text-foreground"
|
|
485
|
+
>
|
|
486
|
+
<X size={14} />
|
|
487
|
+
</button>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
<Button
|
|
491
|
+
size="sm"
|
|
492
|
+
onClick={() => { handleSearch(searchQuery) }}
|
|
493
|
+
disabled={searching || searchQuery.trim().length < 2}
|
|
494
|
+
>
|
|
495
|
+
{searching ? 'Searching…' : 'Search'}
|
|
496
|
+
</Button>
|
|
497
|
+
</div>
|
|
498
|
+
{statusOptions.map((s: string) => (
|
|
499
|
+
<Button
|
|
500
|
+
key={s}
|
|
501
|
+
variant={statusFilter === s ? 'default' : 'outline'}
|
|
502
|
+
size="sm"
|
|
503
|
+
onClick={() => { setStatusFilter(s) }}
|
|
504
|
+
>
|
|
505
|
+
{statusLabel(s)}
|
|
506
|
+
</Button>
|
|
507
|
+
))}
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
511
|
+
<span className="text-sm text-muted-foreground">Type:</span>
|
|
512
|
+
<Button
|
|
513
|
+
variant={typeFilter === 'all' ? 'default' : 'outline'}
|
|
514
|
+
size="sm"
|
|
515
|
+
onClick={() => { setTypeFilter('all') }}
|
|
516
|
+
>
|
|
517
|
+
All
|
|
518
|
+
</Button>
|
|
519
|
+
{typeRecords.map((t) => (
|
|
520
|
+
<Button
|
|
521
|
+
key={t.slug}
|
|
522
|
+
variant={typeFilter === t.slug ? 'default' : 'outline'}
|
|
523
|
+
size="sm"
|
|
524
|
+
onClick={() => { setTypeFilter(t.slug) }}
|
|
525
|
+
>
|
|
526
|
+
{t.name || t.slug}
|
|
527
|
+
</Button>
|
|
528
|
+
))}
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
{error && (
|
|
532
|
+
<div className="p-3 text-sm text-destructive bg-destructive/10 border border-destructive/20 rounded-md">
|
|
533
|
+
{error}
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
{loading ? (
|
|
538
|
+
<div className="p-4 space-y-2">
|
|
539
|
+
{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-8 w-full" />)}
|
|
540
|
+
</div>
|
|
541
|
+
) : visibleArticles.length === 0 ? (
|
|
542
|
+
<div className="p-8 text-center text-muted-foreground border border-dashed rounded-md">
|
|
543
|
+
<BookOpen className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
544
|
+
<p className="text-sm">No articles found.</p>
|
|
545
|
+
</div>
|
|
546
|
+
) : isTreeMode ? (
|
|
547
|
+
<div className="space-y-1">
|
|
548
|
+
<SortableDropZone parentId={undefined} nodes={roots} depth={0} />
|
|
549
|
+
</div>
|
|
550
|
+
) : (
|
|
551
|
+
<div className="space-y-1 border-t border-border/50 pt-2">
|
|
552
|
+
{visibleArticles.map(renderFlatRow)}
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module src/components/layout/AppShell
|
|
3
|
-
* @audience installer
|
|
4
|
-
* @layer frontend-component
|
|
5
|
-
* @stability stable
|
|
6
|
-
*
|
|
7
|
-
* Application shell with Sidebar-05 layout for professional SaaS look.
|
|
8
|
-
* Wraps page content with collapsible sidebar navigation.
|
|
9
|
-
*/
|
|
10
1
|
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// CIL[BEGIN chunk_id="appshell_moduleheader_1783929861" type="module_header" identity="imports" parent="none"]
|
|
5
|
+
// CIL[DOC-MACRO purpose="Import React, router, and UI components for app shell layout"]
|
|
6
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide dependencies for AppShell component"]
|
|
7
|
+
// CIL[DOC-CONTEXT module="src/components/layout/AppShell" layer="frontend-component" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/src/components/ui/dialog.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\"\\\"]"]
|
|
8
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="React, Link, Sidebar components, Separator, Breadcrumb components" errors="none" side_effects="none"]
|
|
9
|
+
// CIL[DOC-MICRO logic_summary="Imports React, router Link, shadcn sidebar, separator and breadcrumb components" notable_decisions="[]"]
|
|
10
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
11
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
12
|
+
// CIL[DOC-AUDIENCE audience="developer"]
|
|
13
|
+
// CIL[DOC-VECTOR type="context" content="Layer: frontend-component. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, custom/apps/portal/pages/ContentPage.tsx, .framework/src/components/ui/dialog.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, \."]
|
|
14
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
15
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
16
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
17
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
18
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
11
19
|
import * as React from "react"
|
|
12
20
|
import { Link } from 'react-router-dom'
|
|
13
21
|
import { SidebarProvider, SidebarInset, SidebarTrigger } from "../ui/sidebar"
|
|
@@ -20,26 +28,56 @@ import {
|
|
|
20
28
|
BreadcrumbPage,
|
|
21
29
|
BreadcrumbSeparator,
|
|
22
30
|
} from "../ui/breadcrumb"
|
|
23
|
-
|
|
31
|
+
// CIL[END chunk_id="appshell_moduleheader_1783929861"]
|
|
32
|
+
// CIL[BEGIN chunk_id="appshell_appshellprops_1783929861" type="interface" identity="AppShellProps" parent="none"]
|
|
33
|
+
// CIL[DOC-MACRO purpose="Define props interface for AppShell component with sidebar and navigation"]
|
|
34
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Specify AppShell component contract for professional layout"]
|
|
35
|
+
// CIL[DOC-CONTEXT module="src/components/layout/AppShell" layer="frontend-component" related_files="[\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/functions/_shared/schema-utils.ts\",\".framework/src/components/ui/dialog.tsx\",\".framework/functions/_shared/principal.ts\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"\\\"]"]
|
|
36
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="AppShellProps interface" errors="none" side_effects="none"]
|
|
37
|
+
// CIL[DOC-MICRO logic_summary="Interface with sidebar, children, breadcrumbs, and optional header actions" notable_decisions="[]"]
|
|
38
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
39
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
40
|
+
// CIL[DOC-VECTOR type="context" content="Layer: frontend-component. Direct callers: 0. Direct calls: 0. Related files: custom/apps/portal/pages/ContentPage.tsx, .framework/functions/_shared/schema-utils.ts, .framework/src/components/ui/dialog.tsx, .framework/functions/_shared/principal.ts, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, \."]
|
|
41
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
42
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
43
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
44
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
45
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
24
46
|
interface AppShellProps {
|
|
25
47
|
sidebar: React.ReactNode
|
|
26
48
|
children: React.ReactNode
|
|
27
49
|
breadcrumbs?: { title: string; url?: string }[]
|
|
28
50
|
headerActions?: React.ReactNode
|
|
51
|
+
banner?: React.ReactNode
|
|
29
52
|
}
|
|
30
|
-
|
|
53
|
+
// CIL[END chunk_id="appshell_appshellprops_1783929861"]
|
|
54
|
+
// CIL[BEGIN chunk_id="appshell_appshell_1783929861" type="function" identity="AppShell" parent="none"]
|
|
55
|
+
// CIL[DOC-MACRO purpose="Render professional app shell with sidebar, header breadcrumbs and main content"]
|
|
56
|
+
// CIL[DOC-RESPONSIBILITY responsibility="Provide complete app layout with navigation and content areas"]
|
|
57
|
+
// CIL[DOC-CONTEXT module="src/components/layout/AppShell" layer="frontend-component" related_files="[\"custom/apps/portal/pages/ContentPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\".framework/src/pages/admin/RolesPage.tsx\",\"\\\"]"]
|
|
58
|
+
// CIL[DOC-CONTRACT inputs="none" outputs="JSX app shell layout" errors="none" side_effects="Manages sidebar state and provides navigation structure"]
|
|
59
|
+
// CIL[DOC-MICRO logic_summary="SidebarProvider with collapsible sidebar, header with breadcrumbs, and main content area" notable_decisions="[]"]
|
|
60
|
+
// CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
|
|
61
|
+
// CIL[DOC-STABILITY stability="stable"]
|
|
62
|
+
// CIL[DOC-VECTOR type="context" content="Layer: frontend-component. Direct callers: 0. Direct calls: 0. Related files: custom/apps/portal/pages/ContentPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, .framework/src/pages/admin/RolesPage.tsx, \."]
|
|
63
|
+
// CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
|
|
64
|
+
// CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
|
|
65
|
+
// CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
|
|
66
|
+
// CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
|
|
67
|
+
// CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
|
|
31
68
|
export function AppShell({
|
|
32
69
|
sidebar,
|
|
33
70
|
children,
|
|
34
71
|
breadcrumbs,
|
|
35
72
|
headerActions,
|
|
73
|
+
banner,
|
|
36
74
|
}: AppShellProps) {
|
|
37
75
|
return (
|
|
38
76
|
<SidebarProvider>
|
|
39
77
|
{sidebar}
|
|
40
|
-
<SidebarInset>
|
|
41
|
-
{
|
|
42
|
-
<header className="flex h-
|
|
78
|
+
<SidebarInset className="h-svh min-h-0 overflow-hidden">
|
|
79
|
+
{banner}
|
|
80
|
+
<header className="flex h-12 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
|
43
81
|
<div className="flex items-center gap-2 px-4">
|
|
44
82
|
<SidebarTrigger className="-ml-1" />
|
|
45
83
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
@@ -81,11 +119,11 @@ export function AppShell({
|
|
|
81
119
|
)}
|
|
82
120
|
</header>
|
|
83
121
|
|
|
84
|
-
|
|
85
|
-
<main className="flex-1 flex flex-col min-h-0 overflow-hidden p-4 md:p-6 lg:p-8">
|
|
122
|
+
<main className="flex-1 flex flex-col min-h-0 overflow-auto p-4 md:p-6 lg:p-8">
|
|
86
123
|
{children}
|
|
87
124
|
</main>
|
|
88
125
|
</SidebarInset>
|
|
89
126
|
</SidebarProvider>
|
|
90
127
|
)
|
|
91
128
|
}
|
|
129
|
+
// CIL[END chunk_id="appshell_appshell_1783929861"]
|