@allbluecn/web-app 0.5.0 → 0.5.1
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/package.json +11 -7
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- package/src/components/story/constants.ts +11 -1
- package/src/components/story/create-story-dialog.tsx +12 -6
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/activity-timeline.tsx +15 -0
- package/src/components/story/detail/story-detail-body.tsx +1 -0
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +2 -0
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +29 -21
- package/src/components/story/detail/story-detail-toolbar.tsx +154 -8
- package/src/components/story/inline-editors/story-select-dialog.tsx +19 -4
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
- package/src/components/story/relations/file-preview.tsx +173 -0
- package/src/components/story/relations/format.ts +120 -0
- package/src/components/story/relations/general-groups.ts +32 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +191 -0
- package/src/components/story/types.ts +13 -0
- package/src/lib/changelog/sdk-versions.ts +19 -19
- package/src/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/routeTree.gen.ts +42 -0
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
- package/src/server/serverFns/knowledge.ts +50 -4
- package/src/server/serverFns/project.ts +43 -0
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +26 -0
- package/src/server/serverFns/story/index.ts +6 -4
- package/src/server/serverFns/story/kb-general.ts +39 -0
- package/src/server/serverFns/story/kb-scope.ts +52 -0
- package/src/server/serverFns/story/link-preview.ts +115 -0
- package/src/server/serverFns/story/story-assoc.ts +339 -50
- package/src/server/serverFns/story/story-attachments.ts +12 -43
- package/src/server/serverFns/story/story-crud.ts +21 -4
- package/src/server/serverFns/story/story-dependencies.ts +35 -3
- package/src/server/serverFns/story/story-links.ts +13 -1
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +37 -0
- package/src/styles/globals.css +25 -0
- package/vite.shared.ts +2 -0
- package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- package/src/components/story/relations/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
package/src/routeTree.gen.ts
CHANGED
|
@@ -82,6 +82,7 @@ import { Route as NavTagsIdRouteRouteImport } from './routes/_nav/tags/$id/route
|
|
|
82
82
|
import { Route as NavTagsSettingsRouteRouteImport } from './routes/_nav/tags/settings/route'
|
|
83
83
|
import { Route as NavWorkspacesIndexRouteImport } from './routes/_nav/workspaces/index'
|
|
84
84
|
import { Route as NavWorkspacesWorkspaceIdRouteRouteImport } from './routes/_nav/workspaces/$workspaceId/route'
|
|
85
|
+
import { Route as ApiAttachmentsUploadRouteImport } from './routes/api/attachments/upload'
|
|
85
86
|
import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
|
|
86
87
|
import { Route as ApiAuthDesktopCallbackRouteImport } from './routes/api/auth/desktop-callback'
|
|
87
88
|
import { Route as ApiDesignUploadRouteImport } from './routes/api/design/upload'
|
|
@@ -100,6 +101,7 @@ import { Route as NavProjectsProjectIdStoryIdRouteRouteImport } from './routes/_
|
|
|
100
101
|
import { Route as NavProjectsProjectIdSettingsRouteRouteImport } from './routes/_nav/projects/$projectId/settings/route'
|
|
101
102
|
import { Route as NavSettingsAiProviderIdRouteRouteImport } from './routes/_nav/settings/ai/$providerId/route'
|
|
102
103
|
import { Route as NavWorkspacesWorkspaceIdSettingsRouteRouteImport } from './routes/_nav/workspaces/$workspaceId/settings/route'
|
|
104
|
+
import { Route as ApiAttachmentsIdContentRouteImport } from './routes/api/attachments/$id/content'
|
|
103
105
|
import { Route as DesignFilesProjectIdArtboardIdFilenameRouteImport } from './routes/design-files/$projectId/$artboardId/$filename'
|
|
104
106
|
import { Route as NavKnowledgeKbIdDocDocIdRouteRouteImport } from './routes/_nav/knowledge/$kbId/doc/$docId/route'
|
|
105
107
|
import { Route as ApiSharePrdPrdIdEventsRouteImport } from './routes/api/share/prd/$prdId/events'
|
|
@@ -575,6 +577,11 @@ const NavWorkspacesWorkspaceIdRouteRoute =
|
|
|
575
577
|
(d) => d.Route,
|
|
576
578
|
),
|
|
577
579
|
)
|
|
580
|
+
const ApiAttachmentsUploadRoute = ApiAttachmentsUploadRouteImport.update({
|
|
581
|
+
id: '/api/attachments/upload',
|
|
582
|
+
path: '/api/attachments/upload',
|
|
583
|
+
getParentRoute: () => rootRouteImport,
|
|
584
|
+
} as any)
|
|
578
585
|
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
|
579
586
|
id: '/api/auth/$',
|
|
580
587
|
path: '/api/auth/$',
|
|
@@ -790,6 +797,11 @@ const NavWorkspacesWorkspaceIdSettingsRouteRoute =
|
|
|
790
797
|
(d) => d.Route,
|
|
791
798
|
),
|
|
792
799
|
)
|
|
800
|
+
const ApiAttachmentsIdContentRoute = ApiAttachmentsIdContentRouteImport.update({
|
|
801
|
+
id: '/api/attachments/$id/content',
|
|
802
|
+
path: '/api/attachments/$id/content',
|
|
803
|
+
getParentRoute: () => rootRouteImport,
|
|
804
|
+
} as any)
|
|
793
805
|
const DesignFilesProjectIdArtboardIdFilenameRoute =
|
|
794
806
|
DesignFilesProjectIdArtboardIdFilenameRouteImport.update({
|
|
795
807
|
id: '/design-files/$projectId/$artboardId/$filename',
|
|
@@ -898,6 +910,7 @@ export interface FileRoutesByFullPath {
|
|
|
898
910
|
'/share/law/$id': typeof ShareLawIdRouteRoute
|
|
899
911
|
'/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
|
|
900
912
|
'/chat/$threadId': typeof NavChatThreadIdRoute
|
|
913
|
+
'/api/attachments/upload': typeof ApiAttachmentsUploadRoute
|
|
901
914
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
902
915
|
'/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
|
|
903
916
|
'/api/design/upload': typeof ApiDesignUploadRoute
|
|
@@ -916,6 +929,7 @@ export interface FileRoutesByFullPath {
|
|
|
916
929
|
'/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
|
|
917
930
|
'/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
|
|
918
931
|
'/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
|
|
932
|
+
'/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
|
|
919
933
|
'/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
|
|
920
934
|
'/settings/ai/': typeof NavSettingsAiIndexRouteLazyRoute
|
|
921
935
|
'/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
|
|
@@ -988,6 +1002,7 @@ export interface FileRoutesByTo {
|
|
|
988
1002
|
'/share/law/$id': typeof ShareLawIdRouteRoute
|
|
989
1003
|
'/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
|
|
990
1004
|
'/chat/$threadId': typeof NavChatThreadIdRoute
|
|
1005
|
+
'/api/attachments/upload': typeof ApiAttachmentsUploadRoute
|
|
991
1006
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
992
1007
|
'/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
|
|
993
1008
|
'/api/design/upload': typeof ApiDesignUploadRoute
|
|
@@ -1006,6 +1021,7 @@ export interface FileRoutesByTo {
|
|
|
1006
1021
|
'/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
|
|
1007
1022
|
'/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
|
|
1008
1023
|
'/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
|
|
1024
|
+
'/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
|
|
1009
1025
|
'/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
|
|
1010
1026
|
'/settings/ai': typeof NavSettingsAiIndexRouteLazyRoute
|
|
1011
1027
|
'/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
|
|
@@ -1096,6 +1112,7 @@ export interface FileRoutesById {
|
|
|
1096
1112
|
'/share/law/$id': typeof ShareLawIdRouteRoute
|
|
1097
1113
|
'/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
|
|
1098
1114
|
'/_nav/chat/$threadId': typeof NavChatThreadIdRoute
|
|
1115
|
+
'/api/attachments/upload': typeof ApiAttachmentsUploadRoute
|
|
1099
1116
|
'/api/auth/$': typeof ApiAuthSplatRoute
|
|
1100
1117
|
'/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
|
|
1101
1118
|
'/api/design/upload': typeof ApiDesignUploadRoute
|
|
@@ -1114,6 +1131,7 @@ export interface FileRoutesById {
|
|
|
1114
1131
|
'/_nav/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
|
|
1115
1132
|
'/_nav/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
|
|
1116
1133
|
'/_nav/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
|
|
1134
|
+
'/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
|
|
1117
1135
|
'/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
|
|
1118
1136
|
'/_nav/settings/ai/': typeof NavSettingsAiIndexRouteLazyRoute
|
|
1119
1137
|
'/_nav/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
|
|
@@ -1202,6 +1220,7 @@ export interface FileRouteTypes {
|
|
|
1202
1220
|
| '/share/law/$id'
|
|
1203
1221
|
| '/share/prd/$prdId'
|
|
1204
1222
|
| '/chat/$threadId'
|
|
1223
|
+
| '/api/attachments/upload'
|
|
1205
1224
|
| '/api/auth/$'
|
|
1206
1225
|
| '/api/auth/desktop-callback'
|
|
1207
1226
|
| '/api/design/upload'
|
|
@@ -1220,6 +1239,7 @@ export interface FileRouteTypes {
|
|
|
1220
1239
|
| '/projects/$projectId/settings'
|
|
1221
1240
|
| '/settings/ai/$providerId'
|
|
1222
1241
|
| '/workspaces/$workspaceId/settings'
|
|
1242
|
+
| '/api/attachments/$id/content'
|
|
1223
1243
|
| '/design-files/$projectId/$artboardId/$filename'
|
|
1224
1244
|
| '/settings/ai/'
|
|
1225
1245
|
| '/knowledge/bookmarks/$id'
|
|
@@ -1292,6 +1312,7 @@ export interface FileRouteTypes {
|
|
|
1292
1312
|
| '/share/law/$id'
|
|
1293
1313
|
| '/share/prd/$prdId'
|
|
1294
1314
|
| '/chat/$threadId'
|
|
1315
|
+
| '/api/attachments/upload'
|
|
1295
1316
|
| '/api/auth/$'
|
|
1296
1317
|
| '/api/auth/desktop-callback'
|
|
1297
1318
|
| '/api/design/upload'
|
|
@@ -1310,6 +1331,7 @@ export interface FileRouteTypes {
|
|
|
1310
1331
|
| '/projects/$projectId/settings'
|
|
1311
1332
|
| '/settings/ai/$providerId'
|
|
1312
1333
|
| '/workspaces/$workspaceId/settings'
|
|
1334
|
+
| '/api/attachments/$id/content'
|
|
1313
1335
|
| '/design-files/$projectId/$artboardId/$filename'
|
|
1314
1336
|
| '/settings/ai'
|
|
1315
1337
|
| '/knowledge/bookmarks/$id'
|
|
@@ -1399,6 +1421,7 @@ export interface FileRouteTypes {
|
|
|
1399
1421
|
| '/share/law/$id'
|
|
1400
1422
|
| '/share/prd/$prdId'
|
|
1401
1423
|
| '/_nav/chat/$threadId'
|
|
1424
|
+
| '/api/attachments/upload'
|
|
1402
1425
|
| '/api/auth/$'
|
|
1403
1426
|
| '/api/auth/desktop-callback'
|
|
1404
1427
|
| '/api/design/upload'
|
|
@@ -1417,6 +1440,7 @@ export interface FileRouteTypes {
|
|
|
1417
1440
|
| '/_nav/projects/$projectId/settings'
|
|
1418
1441
|
| '/_nav/settings/ai/$providerId'
|
|
1419
1442
|
| '/_nav/workspaces/$workspaceId/settings'
|
|
1443
|
+
| '/api/attachments/$id/content'
|
|
1420
1444
|
| '/design-files/$projectId/$artboardId/$filename'
|
|
1421
1445
|
| '/_nav/settings/ai/'
|
|
1422
1446
|
| '/_nav/knowledge/bookmarks/$id'
|
|
@@ -1452,12 +1476,14 @@ export interface RootRouteChildren {
|
|
|
1452
1476
|
ServerWsRoute: typeof ServerWsRoute
|
|
1453
1477
|
ApiLicenseActivateRouteRoute: typeof ApiLicenseActivateRouteRoute
|
|
1454
1478
|
ApiLicenseHeartbeatRouteRoute: typeof ApiLicenseHeartbeatRouteRoute
|
|
1479
|
+
ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
|
|
1455
1480
|
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
|
|
1456
1481
|
ApiAuthDesktopCallbackRoute: typeof ApiAuthDesktopCallbackRoute
|
|
1457
1482
|
ApiDesignUploadRoute: typeof ApiDesignUploadRoute
|
|
1458
1483
|
ApiKnowledgeSyncGithubRoute: typeof ApiKnowledgeSyncGithubRoute
|
|
1459
1484
|
ApiNotificationsStreamRoute: typeof ApiNotificationsStreamRoute
|
|
1460
1485
|
ServerAnyhubProxySplatRoute: typeof ServerAnyhubProxySplatRoute
|
|
1486
|
+
ApiAttachmentsIdContentRoute: typeof ApiAttachmentsIdContentRoute
|
|
1461
1487
|
DesignFilesProjectIdArtboardIdFilenameRoute: typeof DesignFilesProjectIdArtboardIdFilenameRoute
|
|
1462
1488
|
ApiSharePrdPrdIdEventsRoute: typeof ApiSharePrdPrdIdEventsRoute
|
|
1463
1489
|
}
|
|
@@ -1961,6 +1987,13 @@ declare module '@tanstack/react-router' {
|
|
|
1961
1987
|
preLoaderRoute: typeof NavWorkspacesWorkspaceIdRouteRouteImport
|
|
1962
1988
|
parentRoute: typeof NavWorkspacesRouteRoute
|
|
1963
1989
|
}
|
|
1990
|
+
'/api/attachments/upload': {
|
|
1991
|
+
id: '/api/attachments/upload'
|
|
1992
|
+
path: '/api/attachments/upload'
|
|
1993
|
+
fullPath: '/api/attachments/upload'
|
|
1994
|
+
preLoaderRoute: typeof ApiAttachmentsUploadRouteImport
|
|
1995
|
+
parentRoute: typeof rootRouteImport
|
|
1996
|
+
}
|
|
1964
1997
|
'/api/auth/$': {
|
|
1965
1998
|
id: '/api/auth/$'
|
|
1966
1999
|
path: '/api/auth/$'
|
|
@@ -2164,6 +2197,13 @@ declare module '@tanstack/react-router' {
|
|
|
2164
2197
|
preLoaderRoute: typeof NavWorkspacesWorkspaceIdSettingsRouteRouteImport
|
|
2165
2198
|
parentRoute: typeof NavWorkspacesWorkspaceIdRouteRoute
|
|
2166
2199
|
}
|
|
2200
|
+
'/api/attachments/$id/content': {
|
|
2201
|
+
id: '/api/attachments/$id/content'
|
|
2202
|
+
path: '/api/attachments/$id/content'
|
|
2203
|
+
fullPath: '/api/attachments/$id/content'
|
|
2204
|
+
preLoaderRoute: typeof ApiAttachmentsIdContentRouteImport
|
|
2205
|
+
parentRoute: typeof rootRouteImport
|
|
2206
|
+
}
|
|
2167
2207
|
'/design-files/$projectId/$artboardId/$filename': {
|
|
2168
2208
|
id: '/design-files/$projectId/$artboardId/$filename'
|
|
2169
2209
|
path: '/design-files/$projectId/$artboardId/$filename'
|
|
@@ -2651,12 +2691,14 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
2651
2691
|
ServerWsRoute: ServerWsRoute,
|
|
2652
2692
|
ApiLicenseActivateRouteRoute: ApiLicenseActivateRouteRoute,
|
|
2653
2693
|
ApiLicenseHeartbeatRouteRoute: ApiLicenseHeartbeatRouteRoute,
|
|
2694
|
+
ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
|
|
2654
2695
|
ApiAuthSplatRoute: ApiAuthSplatRoute,
|
|
2655
2696
|
ApiAuthDesktopCallbackRoute: ApiAuthDesktopCallbackRoute,
|
|
2656
2697
|
ApiDesignUploadRoute: ApiDesignUploadRoute,
|
|
2657
2698
|
ApiKnowledgeSyncGithubRoute: ApiKnowledgeSyncGithubRoute,
|
|
2658
2699
|
ApiNotificationsStreamRoute: ApiNotificationsStreamRoute,
|
|
2659
2700
|
ServerAnyhubProxySplatRoute: ServerAnyhubProxySplatRoute,
|
|
2701
|
+
ApiAttachmentsIdContentRoute: ApiAttachmentsIdContentRoute,
|
|
2660
2702
|
DesignFilesProjectIdArtboardIdFilenameRoute:
|
|
2661
2703
|
DesignFilesProjectIdArtboardIdFilenameRoute,
|
|
2662
2704
|
ApiSharePrdPrdIdEventsRoute: ApiSharePrdPrdIdEventsRoute,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { inspiration_route_0 } from '@allbluecn/plugins-core/inspiration/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/inspiration')({
|
|
7
|
+
component: inspiration_route_0.Component as any,
|
|
8
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_2 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd/$id')({
|
|
7
|
+
beforeLoad: ((ctx: PluginRouteContext) => prd_route_2.beforeLoad(ctx)) as any,
|
|
8
|
+
component: prd_route_2.Component as any,
|
|
9
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_1 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd/')({
|
|
7
|
+
loader: ((ctx: PluginRouteContext) => prd_route_1.loader(ctx)) as any,
|
|
8
|
+
component: prd_route_1.Component as any,
|
|
9
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_0 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/prd')({
|
|
7
|
+
component: prd_route_0.Component as any,
|
|
8
|
+
})
|
|
@@ -19,19 +19,21 @@ import { createLazyFileRoute, Link } from "@tanstack/react-router";
|
|
|
19
19
|
import { useRouter } from "@tanstack/react-router";
|
|
20
20
|
import { useTranslation } from "react-i18next";
|
|
21
21
|
import { useEffect, useState } from "react";
|
|
22
|
-
import { ArrowLeft, Loader2 } from "lucide-react";
|
|
22
|
+
import { ArrowLeft, BookOpenText, Loader2 } from "lucide-react";
|
|
23
23
|
import { toast } from '@/components/ui/sonner';
|
|
24
24
|
import { Button } from "@/components/ui/button";
|
|
25
25
|
import { Input } from "@/components/ui/input";
|
|
26
26
|
import { Textarea } from "@/components/ui/textarea";
|
|
27
27
|
import { Label } from "@/components/ui/label";
|
|
28
28
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
29
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
30
|
+
import { Badge } from "@/components/ui/badge";
|
|
29
31
|
import { IconPicker } from "@/components/shared/icon-picker";
|
|
30
32
|
import { PROJECT_ICONS } from "@/components/shared/icon-packs/project";
|
|
31
33
|
import { NameConfirmDialog } from "@/components/shared/name-confirm-dialog";
|
|
32
34
|
import { ProjectMemberManager } from "@/components/project/project-member-manager";
|
|
33
35
|
import { extractDisplayId, validateDisplayId } from "@/lib/project-display-id";
|
|
34
|
-
import { checkDisplayIdAvailableFn, updateProjectFn, deleteProjectFn } from "@/server/serverFns/project";
|
|
36
|
+
import { checkDisplayIdAvailableFn, updateProjectFn, deleteProjectFn, setProjectKbsFn } from "@/server/serverFns/project";
|
|
35
37
|
import { useTimezone } from "@/providers/timezone-provider";
|
|
36
38
|
import { TimezoneSelect } from "@/components/shared/timezone-select";
|
|
37
39
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
@@ -39,7 +41,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
|
39
41
|
function ProjectSettingsPage() {
|
|
40
42
|
const { t } = useTranslation('projects');
|
|
41
43
|
const router = useRouter();
|
|
42
|
-
const { project, members } = Route.useLoaderData();
|
|
44
|
+
const { project, members, linkedKbIds, candidateKbs } = Route.useLoaderData();
|
|
43
45
|
const session = Route.useRouteContext({ select: (c) => c.session });
|
|
44
46
|
const currentUserId = session?.user?.id;
|
|
45
47
|
const currentUserRole =
|
|
@@ -57,6 +59,8 @@ function ProjectSettingsPage() {
|
|
|
57
59
|
const [projectTimezone, setProjectTimezone] = useState(project.timezone ?? "");
|
|
58
60
|
const [saving, setSaving] = useState(false);
|
|
59
61
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
62
|
+
const [kbChecked, setKbChecked] = useState<Set<string>>(() => new Set(linkedKbIds));
|
|
63
|
+
const [savingKbs, setSavingKbs] = useState(false);
|
|
60
64
|
const [deleteMode, setDeleteMode] = useState<"transfer" | "clear">("transfer");
|
|
61
65
|
const userTimezone = useTimezone();
|
|
62
66
|
|
|
@@ -102,6 +106,18 @@ function ProjectSettingsPage() {
|
|
|
102
106
|
}
|
|
103
107
|
};
|
|
104
108
|
|
|
109
|
+
const saveKbs = async () => {
|
|
110
|
+
setSavingKbs(true);
|
|
111
|
+
try {
|
|
112
|
+
await setProjectKbsFn({ data: { projectId: project.id, kbIds: [...kbChecked] } });
|
|
113
|
+
toast.success(t('settings.kbSection.saved'));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
toast.error((err as Error).message || t('settings.kbSection.saveFailed'));
|
|
116
|
+
} finally {
|
|
117
|
+
setSavingKbs(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
105
121
|
const handleDelete = async (mode: "transfer" | "clear") => {
|
|
106
122
|
try {
|
|
107
123
|
await deleteProjectFn({ data: { projectId: project.id, mode } })
|
|
@@ -225,6 +241,61 @@ function ProjectSettingsPage() {
|
|
|
225
241
|
</Card>
|
|
226
242
|
)}
|
|
227
243
|
|
|
244
|
+
<Card className="mb-6">
|
|
245
|
+
<CardHeader>
|
|
246
|
+
<CardTitle>{t('settings.kbSection.title')}</CardTitle>
|
|
247
|
+
</CardHeader>
|
|
248
|
+
<CardContent>
|
|
249
|
+
<p className="mb-3 text-sm text-muted-foreground">
|
|
250
|
+
{t('settings.kbSection.desc')}
|
|
251
|
+
</p>
|
|
252
|
+
{candidateKbs.filter((kb) => kb.visibility !== 'private').length === 0 && (
|
|
253
|
+
<p className="text-sm text-muted-foreground">{t('settings.kbSection.empty')}</p>
|
|
254
|
+
)}
|
|
255
|
+
<div className="flex flex-col gap-2">
|
|
256
|
+
{candidateKbs
|
|
257
|
+
.filter((kb) => kb.visibility !== 'private')
|
|
258
|
+
.map((kb) => {
|
|
259
|
+
const isGeneral = kb.visibility === 'general';
|
|
260
|
+
return (
|
|
261
|
+
<label
|
|
262
|
+
key={kb.id}
|
|
263
|
+
className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${
|
|
264
|
+
isGeneral ? 'opacity-60' : 'cursor-pointer'
|
|
265
|
+
}`}
|
|
266
|
+
>
|
|
267
|
+
<Checkbox
|
|
268
|
+
checked={isGeneral ? false : kbChecked.has(kb.id)}
|
|
269
|
+
disabled={isGeneral}
|
|
270
|
+
onCheckedChange={(v) =>
|
|
271
|
+
setKbChecked((prev) => {
|
|
272
|
+
const next = new Set(prev);
|
|
273
|
+
if (v) next.add(kb.id);
|
|
274
|
+
else next.delete(kb.id);
|
|
275
|
+
return next;
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
/>
|
|
279
|
+
<BookOpenText className="size-4 text-muted-foreground" />
|
|
280
|
+
<span className="flex-1">{kb.name}</span>
|
|
281
|
+
{isGeneral ? (
|
|
282
|
+
<span className="text-xs text-muted-foreground">
|
|
283
|
+
{t('settings.kbSection.generalDisabled')}
|
|
284
|
+
</span>
|
|
285
|
+
) : (
|
|
286
|
+
<Badge variant="outline" className="text-xs">{t('knowledge:toolbar.team')}</Badge>
|
|
287
|
+
)}
|
|
288
|
+
</label>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
</div>
|
|
292
|
+
<Button onClick={saveKbs} disabled={savingKbs} className="mt-3">
|
|
293
|
+
{savingKbs ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
|
|
294
|
+
{t('settings.kbSection.save')}
|
|
295
|
+
</Button>
|
|
296
|
+
</CardContent>
|
|
297
|
+
</Card>
|
|
298
|
+
|
|
228
299
|
<Card className="mb-6">
|
|
229
300
|
<CardHeader>
|
|
230
301
|
<CardTitle>{t('settings.members')}</CardTitle>
|
|
@@ -19,14 +19,18 @@ import { createFileRoute } from "@tanstack/react-router";
|
|
|
19
19
|
import {
|
|
20
20
|
getProjectDetailFn,
|
|
21
21
|
listProjectMembersFn,
|
|
22
|
+
listProjectKbsFn,
|
|
22
23
|
} from "@/server/serverFns/project";
|
|
24
|
+
import { listKnowledgeFn } from "@/server/serverFns/knowledge";
|
|
23
25
|
|
|
24
26
|
export const Route = createFileRoute("/_nav/projects/$projectId/settings")({
|
|
25
27
|
loader: async ({ params }) => {
|
|
26
|
-
const [project, members] = await Promise.all([
|
|
28
|
+
const [project, members, linkedKbs, myKbs] = await Promise.all([
|
|
27
29
|
getProjectDetailFn({ data: { projectId: params.projectId } }),
|
|
28
30
|
listProjectMembersFn({ data: { projectId: params.projectId } }),
|
|
31
|
+
listProjectKbsFn({ data: { projectId: params.projectId } }),
|
|
32
|
+
listKnowledgeFn({ data: {} }),
|
|
29
33
|
]);
|
|
30
|
-
return { project, members };
|
|
34
|
+
return { project, members, linkedKbIds: linkedKbs.map((k) => k.id), candidateKbs: myKbs };
|
|
31
35
|
},
|
|
32
36
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { tags_route_2 } from '@allbluecn/plugins-core/tags/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/tags/$id')({
|
|
7
|
+
component: tags_route_2.Component as any,
|
|
8
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { tags_route_1 } from '@allbluecn/plugins-core/tags/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/tags/')({
|
|
7
|
+
component: tags_route_1.Component as any,
|
|
8
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { tags_route_0 } from '@allbluecn/plugins-core/tags/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/tags')({
|
|
7
|
+
component: tags_route_0.Component as any,
|
|
8
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { tags_route_3 } from '@allbluecn/plugins-core/tags/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_nav/tags/settings')({
|
|
7
|
+
loader: ((ctx: PluginRouteContext) => tags_route_3.loader(ctx)) as any,
|
|
8
|
+
component: tags_route_3.Component as any,
|
|
9
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
19
|
+
import { prisma } from "@allbluecn/database";
|
|
20
|
+
import { requireAuth } from "@/server/auth-helpers";
|
|
21
|
+
|
|
22
|
+
// URL 路径中的文件名不可信,Content-Disposition 用 RFC 5987 filename* 编码
|
|
23
|
+
function contentDisposition(fileName: string): string {
|
|
24
|
+
const encoded = encodeURIComponent(fileName);
|
|
25
|
+
return `inline; filename*=UTF-8''${encoded}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Route = createFileRoute("/api/attachments/$id/content")({
|
|
29
|
+
server: {
|
|
30
|
+
handlers: {
|
|
31
|
+
GET: async ({ request, params }) => {
|
|
32
|
+
try {
|
|
33
|
+
await requireAuth(request);
|
|
34
|
+
|
|
35
|
+
const attachment = await prisma.storyAttachment.findUnique({
|
|
36
|
+
where: { id: params.id },
|
|
37
|
+
select: { fileName: true, mimeType: true, bytes: true },
|
|
38
|
+
});
|
|
39
|
+
if (!attachment || !attachment.bytes) {
|
|
40
|
+
return new Response("Not found", { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = new Uint8Array(attachment.bytes);
|
|
44
|
+
return new Response(body, {
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": attachment.mimeType || "application/octet-stream",
|
|
47
|
+
"Content-Length": String(body.byteLength),
|
|
48
|
+
"Content-Disposition": contentDisposition(attachment.fileName),
|
|
49
|
+
"Cache-Control": "private, max-age=31536000, immutable",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const message = e instanceof Error ? e.message : "读取失败";
|
|
54
|
+
const status = message === "UNAUTHORIZED" ? 401 : 500;
|
|
55
|
+
return new Response(message, { status });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
19
|
+
import { prisma } from "@allbluecn/database";
|
|
20
|
+
import { requireAuth, requireUserInDb } from "@/server/auth-helpers";
|
|
21
|
+
import { ACTIVITY_SELECT, toActivityItem } from "@/server/serverFns/story/types";
|
|
22
|
+
import { inferMimeType } from "@/components/story/relations/format";
|
|
23
|
+
|
|
24
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
export const Route = createFileRoute("/api/attachments/upload")({
|
|
27
|
+
server: {
|
|
28
|
+
handlers: {
|
|
29
|
+
POST: async ({ request }) => {
|
|
30
|
+
try {
|
|
31
|
+
const session = await requireAuth(request);
|
|
32
|
+
const userId = session.user!.id!;
|
|
33
|
+
await requireUserInDb(userId);
|
|
34
|
+
|
|
35
|
+
const formData = await request.formData();
|
|
36
|
+
const storyId = formData.get("storyId");
|
|
37
|
+
const file = formData.get("file");
|
|
38
|
+
const rawDescription = formData.get("description");
|
|
39
|
+
const description =
|
|
40
|
+
typeof rawDescription === "string" && rawDescription.trim()
|
|
41
|
+
? rawDescription.trim().slice(0, 500)
|
|
42
|
+
: null;
|
|
43
|
+
const attachmentId = formData.get("attachmentId") as string | null;
|
|
44
|
+
const fileHash = formData.get("fileHash") as string | null;
|
|
45
|
+
if (typeof storyId !== "string" || !storyId) {
|
|
46
|
+
return Response.json({ error: "缺少 storyId" }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
if (!(file instanceof File)) {
|
|
49
|
+
return Response.json({ error: "缺少文件" }, { status: 400 });
|
|
50
|
+
}
|
|
51
|
+
if (file.size <= 0 || file.size > MAX_FILE_SIZE) {
|
|
52
|
+
return Response.json({ error: "文件大小需在 10MB 以内" }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const story = await prisma.story.findUnique({
|
|
56
|
+
where: { id: storyId },
|
|
57
|
+
select: { id: true },
|
|
58
|
+
});
|
|
59
|
+
if (!story) {
|
|
60
|
+
return Response.json({ error: "需求不存在" }, { status: 404 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
64
|
+
let attachment;
|
|
65
|
+
let activity;
|
|
66
|
+
|
|
67
|
+
if (attachmentId) {
|
|
68
|
+
// 编辑模式:替换现有附件
|
|
69
|
+
attachment = await prisma.storyAttachment.update({
|
|
70
|
+
where: { id: attachmentId },
|
|
71
|
+
data: {
|
|
72
|
+
fileName: file.name.slice(0, 255),
|
|
73
|
+
fileSize: BigInt(file.size),
|
|
74
|
+
mimeType: inferMimeType(file.name, file.type),
|
|
75
|
+
description,
|
|
76
|
+
bytes,
|
|
77
|
+
fileHash: fileHash ?? null,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
activity = await prisma.storyActivity.create({
|
|
81
|
+
data: {
|
|
82
|
+
storyId,
|
|
83
|
+
userId,
|
|
84
|
+
action: "attachment_updated",
|
|
85
|
+
newValue: file.name.slice(0, 255),
|
|
86
|
+
},
|
|
87
|
+
select: ACTIVITY_SELECT,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
// 新增模式:先检查同 story 是否已有相同 hash 的附件
|
|
91
|
+
const existing = fileHash
|
|
92
|
+
? await prisma.storyAttachment.findFirst({
|
|
93
|
+
where: { storyId, fileHash },
|
|
94
|
+
select: { id: true },
|
|
95
|
+
})
|
|
96
|
+
: null;
|
|
97
|
+
if (existing) {
|
|
98
|
+
return Response.json({ duplicated: true, existingId: existing.id }, { status: 200 });
|
|
99
|
+
}
|
|
100
|
+
attachment = await prisma.storyAttachment.create({
|
|
101
|
+
data: {
|
|
102
|
+
storyId,
|
|
103
|
+
fileName: file.name.slice(0, 255),
|
|
104
|
+
fileSize: BigInt(file.size),
|
|
105
|
+
mimeType: inferMimeType(file.name, file.type),
|
|
106
|
+
description,
|
|
107
|
+
storageUrl: "pending",
|
|
108
|
+
bytes,
|
|
109
|
+
fileHash: fileHash ?? null,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const storageUrl = `/api/attachments/${attachment.id}/content`;
|
|
113
|
+
await prisma.storyAttachment.update({
|
|
114
|
+
where: { id: attachment.id },
|
|
115
|
+
data: { storageUrl },
|
|
116
|
+
});
|
|
117
|
+
activity = await prisma.storyActivity.create({
|
|
118
|
+
data: {
|
|
119
|
+
storyId,
|
|
120
|
+
userId,
|
|
121
|
+
action: "attachment_added",
|
|
122
|
+
newValue: file.name.slice(0, 255),
|
|
123
|
+
},
|
|
124
|
+
select: ACTIVITY_SELECT,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const storageUrl = `/api/attachments/${attachment.id}/content`;
|
|
129
|
+
|
|
130
|
+
return Response.json({
|
|
131
|
+
id: attachment.id,
|
|
132
|
+
fileName: attachment.fileName,
|
|
133
|
+
fileSize: Number(attachment.fileSize),
|
|
134
|
+
mimeType: attachment.mimeType,
|
|
135
|
+
description: attachment.description,
|
|
136
|
+
storageUrl,
|
|
137
|
+
fileHash: attachment.fileHash,
|
|
138
|
+
activity: toActivityItem(activity),
|
|
139
|
+
});
|
|
140
|
+
} catch (e) {
|
|
141
|
+
const message = e instanceof Error ? e.message : "上传失败";
|
|
142
|
+
const status = message === "UNAUTHORIZED" ? 401 : 500;
|
|
143
|
+
return Response.json({ error: message }, { status });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import type { PluginRouteContext } from '@allbluecn/kernel'
|
|
4
|
+
import { prd_route_3 } from '@allbluecn/plugins-core/prd/routes'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/share/prd/$prdId')({
|
|
7
|
+
beforeLoad: ((ctx: PluginRouteContext) => prd_route_3.beforeLoad(ctx)) as any,
|
|
8
|
+
validateSearch: prd_route_3.validateSearch as any,
|
|
9
|
+
component: prd_route_3.Component as any,
|
|
10
|
+
})
|