@taskforcehq/taskforce 0.3.303 → 0.3.305
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/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskActor } from '../
|
|
1
|
+
import type { TaskActor } from '../shared/taskActor.js';
|
|
2
2
|
import type { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
3
3
|
import { AnnotatedAttachmentSessionStore } from './AnnotatedAttachmentSessionStore.js';
|
|
4
4
|
import type { AnnotatedAttachmentAnnotation, AnnotatedAttachmentPayload, AnnotatedAttachmentSession } from './types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskActor } from '../
|
|
1
|
+
import type { TaskActor } from '../shared/taskActor.js';
|
|
2
2
|
export type AnnotatedAttachmentAnnotationKind = 'pin' | 'arrow' | 'box' | 'text-note';
|
|
3
3
|
export type AnnotatedAttachmentMarkerType = 'question' | 'change' | 'issue' | 'idea' | 'review';
|
|
4
4
|
export interface AnnotatedAttachmentBaseAnnotation {
|
|
@@ -4,6 +4,10 @@ function normalizeTimestamp(value) {
|
|
|
4
4
|
const normalized = String(value || '').trim();
|
|
5
5
|
return normalized.length > 0 ? normalized : null;
|
|
6
6
|
}
|
|
7
|
+
function normalizeSetupIntent(value) {
|
|
8
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
9
|
+
return normalized === 'attach-cloud-import' ? 'attach-cloud-import' : null;
|
|
10
|
+
}
|
|
7
11
|
function resolveLegacyPhase(input, base) {
|
|
8
12
|
const enabled = Boolean(input.enabled ?? input.cloudSyncEnabled);
|
|
9
13
|
if (!enabled)
|
|
@@ -46,6 +50,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
|
|
|
46
50
|
version: 2,
|
|
47
51
|
enabled: Boolean(state.enabled ?? state.cloudSyncEnabled),
|
|
48
52
|
phase: 'idle',
|
|
53
|
+
setupIntent: normalizeSetupIntent(state.setupIntent),
|
|
49
54
|
pullCursor: normalizeTimestamp(state.pullCursor),
|
|
50
55
|
lastPullAt: normalizeTimestamp(state.lastPullAt),
|
|
51
56
|
lastPushAt: normalizeTimestamp(state.lastPushAt),
|
|
@@ -67,6 +72,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
|
|
|
67
72
|
const changed = currentVersion !== 2
|
|
68
73
|
|| Boolean(state.enabled ?? state.cloudSyncEnabled) !== normalized.enabled
|
|
69
74
|
|| String(state.phase || '').trim() !== normalized.phase
|
|
75
|
+
|| normalizeSetupIntent(state.setupIntent) !== normalized.setupIntent
|
|
70
76
|
|| normalizeTimestamp(state.pullCursor) !== normalized.pullCursor
|
|
71
77
|
|| normalizeTimestamp(state.lastPullAt) !== normalized.lastPullAt
|
|
72
78
|
|| normalizeTimestamp(state.lastPushAt) !== normalized.lastPushAt
|
|
@@ -119,7 +119,7 @@ export function AgentsModule({ workspaceId }) {
|
|
|
119
119
|
timeStyle: 'short'
|
|
120
120
|
});
|
|
121
121
|
};
|
|
122
|
-
return (_jsx("div", { className: `${styles.standalonePage} ${styles.standaloneContent}`, style: { overflow: 'auto' }, children: _jsx("div", { className: styles.settingsContent, style: { maxWidth:
|
|
122
|
+
return (_jsx("div", { className: `${styles.standalonePage} ${styles.standaloneContent}`, style: { overflow: 'auto' }, children: _jsx("div", { className: styles.settingsContent, style: { maxWidth: 1080, margin: '0 auto', width: '100%' }, children: _jsx("div", { className: styles.settingGroup, children: _jsxs("div", { children: [_jsx("h4", { className: styles.settingSubTitle, children: "Registered AI Profiles" }), _jsx("p", { className: `${styles.settingsHint} ${styles.marginBottom12}`, children: "AI agents register profiles when connecting via MCP. Merge duplicates created when a token was lost." }), aiProfilesLoading && (_jsxs("div", { className: styles.settingsHint, children: [_jsx(Loader2, { size: 13, className: styles.spinner }), " Loading profiles\u2026"] })), !aiProfilesLoading && aiProfiles.length === 0 && (_jsx("div", { className: styles.settingsHint, children: "No AI profiles registered yet." })), !aiProfilesLoading && groupedProfilesBySurfaceType.length > 0 && (_jsxs("div", { className: styles.aiProfilesExplorer, children: [_jsx("div", { className: styles.aiProfilesListPane, children: _jsx("div", { className: styles.aiProfilesList, children: groupedProfilesBySurfaceType.map((section) => (_jsxs("div", { className: styles.aiProfilesSection, children: [_jsx("div", { className: styles.aiProfilesSectionHeader, children: section.label }), section.groups.map((group) => {
|
|
123
123
|
const isSelected = selectedGroup?.groupId === group.groupId;
|
|
124
124
|
const primary = group.primaryProfile;
|
|
125
125
|
return (_jsxs("button", { type: "button", className: `${styles.aiProfileGroup} ${group.profiles.length > 1 ? styles.aiProfileGroupDuplicate : ''} ${isSelected ? styles.aiProfileGroupSelected : ''}`, onClick: () => setSelectedGroupId(group.groupId), "aria-pressed": isSelected, children: [_jsxs("div", { className: styles.aiProfileGroupHeader, children: [_jsx(Bot, { size: 13, style: { color: primary.color } }), _jsx("span", { className: styles.aiProfileName, children: primary.name }), group.profiles.length > 1 && (_jsxs("span", { className: styles.aiProfileDuplicateBadge, children: [_jsx(AlertTriangle, { size: 11 }), " ", group.profiles.length, " duplicates"] }))] }), _jsxs("div", { className: styles.settingsHint, style: { marginTop: 6 }, children: ["@", primary.username] }), (primary.surfaceType || primary.role || primary.provider || primary.model) && (_jsxs("div", { className: styles.aiProfileMetaRow, children: [primary.surfaceType && (_jsx("span", { className: styles.aiProfileSurfaceBadge, children: formatAiProfileSurfaceTypeLabel(primary.surfaceType) })), primary.role && _jsx("span", { children: primary.role }), primary.provider && _jsx("span", { children: primary.provider }), primary.model && _jsx("span", { children: primary.model })] })), primary.description && (_jsx("div", { className: styles.settingsHint, style: { marginTop: 4 }, children: primary.description }))] }, group.groupId));
|
|
@@ -95,7 +95,7 @@ describe('AgentsModule', () => {
|
|
|
95
95
|
expect(screen.getAllByText('Terminal-first coding agent.').length).toBeGreaterThan(0);
|
|
96
96
|
expect(screen.getAllByText('Implementation partner')[0]).toBeInTheDocument();
|
|
97
97
|
expect(screen.getByText('OpenAI · gpt-5-codex')).toBeInTheDocument();
|
|
98
|
-
expect(screen.getByText(/2 linked
|
|
98
|
+
expect(screen.getByText(/2 linked/i)).toBeInTheDocument();
|
|
99
99
|
expect(screen.getAllByTitle('Keep this profile, merge others into it')).toHaveLength(2);
|
|
100
100
|
expect(screen.getByText('@codex-duplicate')).toBeInTheDocument();
|
|
101
101
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Search, RefreshCw, FileText, ClipboardList, Eye, BookOpen, File } from 'lucide-react';
|
|
3
3
|
import styles from '../../Taskforce.module.css';
|
|
4
4
|
import indexStyles from './DocumentIndex.module.css';
|
|
5
|
-
import { getEffectiveAttachmentState } from './
|
|
5
|
+
import { getEffectiveAttachmentState } from './documentWorkspaceModel';
|
|
6
6
|
import { getReadableDocumentName } from '../../utils/documentNames';
|
|
7
7
|
function DocTypeIcon({ docType }) {
|
|
8
8
|
switch (docType) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DocMeta } from './
|
|
1
|
+
import { type DocMeta } from './documentWorkspaceModel';
|
|
2
2
|
export declare function buildPrintDocumentHtml(markdown: string): string;
|
|
3
3
|
export declare function schedulePrintCleanup(cleanup: () => void): number;
|
|
4
4
|
interface Props {
|
|
@@ -8,7 +8,7 @@ import { Printer, Link, Download, Clock, HardDrive, Tag, Pencil, X, Save, Eye, S
|
|
|
8
8
|
import styles from '../../Taskforce.module.css';
|
|
9
9
|
import viewerStyles from './DocumentViewer.module.css';
|
|
10
10
|
import taskFormStyles from '../task/TaskForm.module.css';
|
|
11
|
-
import { getEffectiveAttachmentState } from './
|
|
11
|
+
import { getEffectiveAttachmentState } from './documentWorkspaceModel';
|
|
12
12
|
import { DocumentGenerateModal } from './DocumentGenerateModal';
|
|
13
13
|
import { fetchTaskforceApi, resolveTaskforceApiUrl } from '../../utils/taskforceApiClient';
|
|
14
14
|
import { getDocumentDownloadName, getReadableDocumentName } from '../../utils/documentNames';
|
|
@@ -522,6 +522,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
522
522
|
const [reviewTitleDraft, setReviewTitleDraft] = useState('');
|
|
523
523
|
const [editingCommentId, setEditingCommentId] = useState(null);
|
|
524
524
|
const [editingCommentDraft, setEditingCommentDraft] = useState('');
|
|
525
|
+
const [showReviewSidebar, setShowReviewSidebar] = useState(true);
|
|
525
526
|
// Edit mode state
|
|
526
527
|
const [editMode, setEditMode] = useState(false);
|
|
527
528
|
const [draft, setDraft] = useState('');
|
|
@@ -534,7 +535,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
534
535
|
const [renameValue, setRenameValue] = useState(readableName);
|
|
535
536
|
const isEditable = doc.editable !== false;
|
|
536
537
|
const attachmentState = getEffectiveAttachmentState(doc);
|
|
537
|
-
const canDelete =
|
|
538
|
+
const canDelete = !!doc.assetId;
|
|
538
539
|
const isDirty = editMode && draft !== (content ?? '');
|
|
539
540
|
const textareaRef = useRef(null);
|
|
540
541
|
const renameInputRef = useRef(null);
|
|
@@ -913,13 +914,20 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
913
914
|
const handleDelete = useCallback(async () => {
|
|
914
915
|
if (!doc.assetId || deleting)
|
|
915
916
|
return;
|
|
916
|
-
|
|
917
|
+
const isAttached = attachmentState === 'attached';
|
|
918
|
+
const linkedTaskCount = Math.max(0, Number(doc.attachmentCount || 0));
|
|
919
|
+
const confirmationMessage = isAttached
|
|
920
|
+
? `Delete "${readableName}" everywhere?\n\nThis will remove it from ${linkedTaskCount} linked task${linkedTaskCount === 1 ? '' : 's'} and soft-delete the document across sync.`
|
|
921
|
+
: `Permanently delete "${readableName}" from storage?`;
|
|
922
|
+
if (!window.confirm(confirmationMessage))
|
|
917
923
|
return;
|
|
918
924
|
setDeleting(true);
|
|
919
925
|
try {
|
|
920
926
|
const res = await fetchTaskforceApi(`/api/taskforce/documents/${encodeURIComponent(doc.assetId)}`, {
|
|
921
927
|
method: 'DELETE',
|
|
922
928
|
credentials: 'include',
|
|
929
|
+
headers: { 'Content-Type': 'application/json' },
|
|
930
|
+
body: JSON.stringify(isAttached ? { forceDetach: true } : {}),
|
|
923
931
|
}, apiBaseUrl);
|
|
924
932
|
const data = await res.json().catch(() => ({}));
|
|
925
933
|
if (!res.ok)
|
|
@@ -932,7 +940,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
932
940
|
finally {
|
|
933
941
|
setDeleting(false);
|
|
934
942
|
}
|
|
935
|
-
}, [apiBaseUrl, deleting, doc.assetId, onDeleted, readableName]);
|
|
943
|
+
}, [apiBaseUrl, attachmentState, deleting, doc.assetId, doc.attachmentCount, onDeleted, readableName]);
|
|
936
944
|
const handleToggleReviewStatus = useCallback(async () => {
|
|
937
945
|
if (!activeReviewSession || reviewBusy)
|
|
938
946
|
return;
|
|
@@ -992,7 +1000,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
992
1000
|
}, [activeReviewSession, apiBaseUrl, reviewBusy, reviewTitleDraft]);
|
|
993
1001
|
const previewContent = editMode ? draft : (content ?? '');
|
|
994
1002
|
const handleCaptureReviewAnchor = useCallback(() => {
|
|
995
|
-
if (editMode)
|
|
1003
|
+
if (editMode || !showReviewSidebar)
|
|
996
1004
|
return;
|
|
997
1005
|
const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;
|
|
998
1006
|
const selectedText = selection?.toString().replace(/\s+/g, ' ').trim() || '';
|
|
@@ -1011,7 +1019,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
1011
1019
|
if (!anchor)
|
|
1012
1020
|
return;
|
|
1013
1021
|
setPendingReviewAnchor(anchor);
|
|
1014
|
-
}, [editMode]);
|
|
1022
|
+
}, [editMode, showReviewSidebar]);
|
|
1023
|
+
const handleToggleReviewSidebar = useCallback(() => {
|
|
1024
|
+
setShowReviewSidebar((current) => {
|
|
1025
|
+
const nextValue = !current;
|
|
1026
|
+
if (!nextValue) {
|
|
1027
|
+
setPendingReviewAnchor(null);
|
|
1028
|
+
}
|
|
1029
|
+
return nextValue;
|
|
1030
|
+
});
|
|
1031
|
+
}, []);
|
|
1015
1032
|
const handleAddReviewComment = useCallback(async () => {
|
|
1016
1033
|
const body = reviewDraft.trim();
|
|
1017
1034
|
if (!body || reviewBusy)
|
|
@@ -1210,10 +1227,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
1210
1227
|
return;
|
|
1211
1228
|
setRenameValue(readableName);
|
|
1212
1229
|
setRenaming(true);
|
|
1213
|
-
}, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title:
|
|
1230
|
+
}, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleToggleReviewSidebar, title: showReviewSidebar ? 'Hide review and comments' : 'Show review and comments', children: [_jsx(MessageSquareText, { size: 13 }), showReviewSidebar ? 'Hide Review' : 'Show Review'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: attachmentState === 'attached'
|
|
1231
|
+
? 'Remove this document from linked tasks and delete it'
|
|
1232
|
+
: 'Permanently delete this unattached document', children: [_jsx(Trash2, { size: 13 }), deleting
|
|
1233
|
+
? 'Deleting…'
|
|
1234
|
+
: attachmentState === 'attached'
|
|
1235
|
+
? 'Delete & Unlink'
|
|
1236
|
+
: 'Delete Permanently'] }))] })) })] }), loadingContent && (_jsx("div", { className: viewerStyles.viewerState, children: "Loading\u2026" })), !loadingContent && loadError && (_jsx("div", { className: viewerStyles.viewerStateError, children: loadError })), !loadingContent && !loadError && (editMode ? (_jsxs("div", { className: `${viewerStyles.editorArea} ${EDITOR_LAYOUT_CLASS[layout]}`, children: [(layout === 'editor' || layout === 'split') && (_jsxs("div", { className: viewerStyles.editorPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Markdown" }), _jsx("textarea", { ref: textareaRef, className: `${viewerStyles.editorTextarea} tf-scrollbar`, value: draft, onScroll: () => syncPaneScroll('editor'), onChange: e => {
|
|
1214
1237
|
setDraft(e.target.value);
|
|
1215
1238
|
setSaveStatus(prev => prev.type === 'saving' ? prev : { type: 'idle' });
|
|
1216
|
-
}, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: viewerStyles.viewerBody
|
|
1239
|
+
}, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: `${viewerStyles.viewerBody} ${!showReviewSidebar ? viewerStyles.viewerBodyFullWidth : ''}`, children: [_jsx("div", { className: `${viewerStyles.viewerContent} ${viewerStyles.viewerMain} tf-scrollbar`, children: _jsx("div", { ref: markdownContainerRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, onMouseUp: showReviewSidebar ? handleCaptureReviewAnchor : undefined, children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) }) }), showReviewSidebar && (_jsx("aside", { className: viewerStyles.reviewRail, children: _jsxs("section", { className: viewerStyles.reviewSection, "aria-label": "Review and comments", children: [_jsxs("div", { className: viewerStyles.reviewHeader, children: [_jsxs("div", { children: [_jsxs("div", { className: viewerStyles.reviewTitle, children: [_jsx(MessageSquareText, { size: 14 }), "Review & Comments"] }), _jsx("p", { className: viewerStyles.reviewSubtitle, children: "Comments stay attached to this document review session." })] }), _jsxs("div", { className: viewerStyles.reviewControls, children: [reviewSessions.length > 1 && (_jsx("select", { className: `${styles.select} ${viewerStyles.reviewSelect}`, value: activeReviewSession?.id || '', onChange: (event) => setSelectedReviewSessionId(event.target.value || null), "aria-label": "Select review session", children: reviewSessions.map((session) => (_jsx("option", { value: session.id, children: session.title?.trim() || 'Untitled review' }, session.id))) })), _jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void createReviewSession(); }, disabled: reviewBusy || !doc.assetId, children: [_jsx(MessageSquareText, { size: 13 }), "Start Review"] }), activeReviewSession && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void handleToggleReviewStatus(); }, disabled: reviewBusy, children: [activeReviewSession.status === 'resolved' ? _jsx(RotateCcw, { size: 13 }) : _jsx(CheckCheck, { size: 13 }), activeReviewSession.status === 'resolved' ? 'Reopen' : 'Resolve'] }))] })] }), activeReviewSession && (_jsxs("div", { className: viewerStyles.reviewSessionMeta, children: [_jsx("span", { className: `${viewerStyles.statusBadge} ${activeReviewSession.status === 'resolved' ? viewerStyles.reviewStatusResolved : ''}`, children: activeReviewSession.status === 'resolved' ? 'Resolved' : 'Open' }), editingReviewTitle ? (_jsxs("div", { className: viewerStyles.reviewTitleEditRow, children: [_jsx("input", { type: "text", className: styles.input, value: reviewTitleDraft, onChange: (event) => setReviewTitleDraft(event.target.value), placeholder: "Untitled review" }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => { void handleSaveReviewTitle(); }, disabled: reviewBusy, children: "Save Title" })] })) : (_jsx("span", { className: viewerStyles.reviewSessionLabel, children: activeReviewSession.title?.trim() || 'Untitled review' })), _jsxs("span", { className: viewerStyles.reviewSessionTimestamp, children: ["Updated ", formatReviewTimestamp(activeReviewSession.updatedAt)] }), !editingReviewTitle && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => setEditingReviewTitle(true), disabled: reviewBusy, children: [_jsx(Pencil, { size: 13 }), "Rename Review"] }))] })), reviewError && (_jsx("div", { className: viewerStyles.viewerStateError, children: reviewError })), _jsxs("div", { className: taskFormStyles.commentPanel, children: [_jsxs("div", { className: taskFormStyles.commentThread, children: [loadingReviews && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Loading review comments\u2026" })), !loadingReviews && !activeReviewSession && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Start a review to collect comments for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.length === 0 && (_jsx("div", { className: taskFormStyles.emptyComments, children: "No comments yet. Add the first review note for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.map((comment) => {
|
|
1217
1240
|
const isAi = comment.authorActor?.kind === 'ai';
|
|
1218
1241
|
const anchorLabel = formatReviewAnchor(comment.anchor);
|
|
1219
1242
|
const ActorIcon = isAi ? Bot : User;
|
|
@@ -1231,5 +1254,5 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
|
|
|
1231
1254
|
event.preventDefault();
|
|
1232
1255
|
void handleAddReviewComment();
|
|
1233
1256
|
}
|
|
1234
|
-
} })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) })] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
|
|
1257
|
+
} })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) }))] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
|
|
1235
1258
|
}
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import { type DocumentAttachmentFilter, type DocumentTypeFilter } from './documentWorkspaceFilters';
|
|
2
|
-
|
|
3
|
-
id: string;
|
|
4
|
-
assetId?: string;
|
|
5
|
-
referenceNumber?: number | null;
|
|
6
|
-
referenceLabel?: string | null;
|
|
7
|
-
fsPath: string;
|
|
8
|
-
apiUrl: string;
|
|
9
|
-
taskId: string | null;
|
|
10
|
-
displayName: string;
|
|
11
|
-
originalFilename?: string | null;
|
|
12
|
-
title: string;
|
|
13
|
-
docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
|
|
14
|
-
sizeBytes: number;
|
|
15
|
-
updatedAt: string | null;
|
|
16
|
-
editable?: boolean;
|
|
17
|
-
attachmentCount?: number;
|
|
18
|
-
attachmentState?: 'attached' | 'unattached' | 'standalone';
|
|
19
|
-
}
|
|
2
|
+
import type { DocMeta } from './documentWorkspaceModel';
|
|
20
3
|
export type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
|
|
21
4
|
export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
|
|
5
|
+
export type { DocMeta } from './documentWorkspaceModel';
|
|
22
6
|
interface Props {
|
|
23
7
|
tasks: Array<{
|
|
24
8
|
id: string;
|
|
@@ -30,11 +14,11 @@ interface Props {
|
|
|
30
14
|
error: string | null;
|
|
31
15
|
selectedDoc: DocMeta | null;
|
|
32
16
|
searchQuery: string;
|
|
33
|
-
|
|
34
|
-
attachmentFilters: DocumentAttachmentFilter[];
|
|
17
|
+
hasActiveFilters: boolean;
|
|
35
18
|
onSearchChange: (value: string) => void;
|
|
36
19
|
onSelectDoc: (doc: DocMeta | null) => void;
|
|
37
20
|
onRefresh: () => Promise<void> | void;
|
|
21
|
+
onDeleteSelected: () => Promise<void> | void;
|
|
38
22
|
enableTaskGeneration?: boolean;
|
|
39
23
|
}
|
|
40
24
|
interface ShellProps {
|
|
@@ -54,6 +38,5 @@ interface ShellProps {
|
|
|
54
38
|
onRequestedDocHandled?: () => void;
|
|
55
39
|
enableTaskGeneration?: boolean;
|
|
56
40
|
}
|
|
57
|
-
export declare function
|
|
58
|
-
export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
|
|
59
42
|
export declare function DocumentWorkspaceShell({ tasks, runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, enableTaskGeneration }: ShellProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,168 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
2
|
import styles from '../../Taskforce.module.css';
|
|
4
3
|
import { DocumentIndex } from './DocumentIndex';
|
|
5
4
|
import { DocumentViewer } from './DocumentViewer';
|
|
6
|
-
import {
|
|
7
|
-
import { logDocumentManagerDebug, resolveDocumentManagerApiBase } from '../../utils/documentManagerApi';
|
|
8
|
-
import { getReadableDocumentName } from '../../utils/documentNames';
|
|
9
|
-
import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from './documentWorkspaceFilters';
|
|
5
|
+
import { useDocumentWorkspaceController } from './useDocumentWorkspaceController';
|
|
10
6
|
export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
|
|
11
|
-
function
|
|
12
|
-
let decoded = value;
|
|
13
|
-
try {
|
|
14
|
-
decoded = decodeURIComponent(value);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
decoded = value;
|
|
18
|
-
}
|
|
19
|
-
const normalized = decoded
|
|
20
|
-
.trim()
|
|
21
|
-
.replace(/\\/g, '/')
|
|
22
|
-
.replace(/^\/+/, '')
|
|
23
|
-
.replace(/^\.\//, '')
|
|
24
|
-
.replace(/^\.taskforce\//, '');
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
27
|
-
function docPathMatches(candidate, requested) {
|
|
28
|
-
if (candidate === requested)
|
|
29
|
-
return true;
|
|
30
|
-
if (candidate.endsWith(`/${requested}`))
|
|
31
|
-
return true;
|
|
32
|
-
if (requested.endsWith(`/${candidate}`))
|
|
33
|
-
return true;
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
function extractAssetIdFromPath(value) {
|
|
37
|
-
const normalized = normalizeDocPath(String(value || ''));
|
|
38
|
-
if (!normalized)
|
|
39
|
-
return null;
|
|
40
|
-
const match = normalized.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i);
|
|
41
|
-
return match?.[2] ? match[2].toLowerCase() : null;
|
|
42
|
-
}
|
|
43
|
-
export function getEffectiveAttachmentState(doc) {
|
|
44
|
-
if (doc.attachmentState === 'attached' || doc.attachmentState === 'unattached') {
|
|
45
|
-
return doc.attachmentState;
|
|
46
|
-
}
|
|
47
|
-
const attachmentCount = typeof doc.attachmentCount === 'number' && Number.isFinite(doc.attachmentCount)
|
|
48
|
-
? doc.attachmentCount
|
|
49
|
-
: 0;
|
|
50
|
-
if (attachmentCount > 0 || !!doc.taskId) {
|
|
51
|
-
return 'attached';
|
|
52
|
-
}
|
|
53
|
-
return 'unattached';
|
|
54
|
-
}
|
|
55
|
-
export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration = false }) {
|
|
56
|
-
const filteredDocs = documents.filter(doc => {
|
|
57
|
-
const readableName = getReadableDocumentName(doc);
|
|
58
|
-
const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
|
|
59
|
-
const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
|
|
60
|
-
const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
|
|
61
|
-
const attachmentState = getEffectiveAttachmentState(doc);
|
|
62
|
-
const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
|
|
63
|
-
return matchesSearch && matchesType && matchesAttachment;
|
|
64
|
-
});
|
|
65
|
-
const hasActiveFilters = searchQuery.trim().length > 0 || typeFilters.length > 0 || attachmentFilters.length > 0;
|
|
7
|
+
export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration = false }) {
|
|
66
8
|
const taskById = new Map(tasks.map(t => [t.id, t]));
|
|
67
|
-
return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents:
|
|
68
|
-
onSelectDoc(null);
|
|
69
|
-
void onRefresh();
|
|
70
|
-
}, enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: filteredDocs.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
|
|
9
|
+
return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: documents, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => void onDeleteSelected(), enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: documents.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
|
|
71
10
|
}
|
|
72
11
|
export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, enableTaskGeneration = false }) {
|
|
73
|
-
const
|
|
74
|
-
const [loading, setLoading] = useState(true);
|
|
75
|
-
const [error, setError] = useState(null);
|
|
76
|
-
const [selectedDoc, setSelectedDoc] = useState(null);
|
|
77
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
78
|
-
const [internalTypeFilters, setInternalTypeFilters] = useState(() => DOCUMENT_TYPE_FILTER_OPTIONS.map((option) => option.value));
|
|
79
|
-
const [internalAttachmentFilters, setInternalAttachmentFilters] = useState(() => DOCUMENT_ATTACHMENT_FILTER_OPTIONS.map((option) => option.value));
|
|
80
|
-
const documentApiBaseUrl = resolveDocumentManagerApiBase({
|
|
12
|
+
const { documentApiBaseUrl, filteredDocs, loading, error, selectedDoc, searchQuery, hasActiveFilters, setSearchQuery, fetchDocuments, selectDoc, handleDeleteSelected, } = useDocumentWorkspaceController({
|
|
81
13
|
runtimeMode,
|
|
82
14
|
apiBaseUrl,
|
|
83
15
|
cloudAuthBaseUrl,
|
|
16
|
+
typeFilters: controlledTypeFilters,
|
|
17
|
+
attachmentFilters: controlledAttachmentFilters,
|
|
18
|
+
onTypeFiltersChange,
|
|
19
|
+
onAttachmentFiltersChange,
|
|
20
|
+
requestedDocPath,
|
|
21
|
+
requestedDocAssetId,
|
|
22
|
+
onRequestedDocHandled,
|
|
84
23
|
});
|
|
85
|
-
|
|
86
|
-
const attachmentFilters = controlledAttachmentFilters ?? internalAttachmentFilters;
|
|
87
|
-
const handleTypeFiltersChange = onTypeFiltersChange ?? setInternalTypeFilters;
|
|
88
|
-
const handleAttachmentFiltersChange = onAttachmentFiltersChange ?? setInternalAttachmentFilters;
|
|
89
|
-
const fetchDocuments = useCallback(async () => {
|
|
90
|
-
setLoading(true);
|
|
91
|
-
setError(null);
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetchTaskforceApi('/api/taskforce/documents', {
|
|
94
|
-
credentials: 'include',
|
|
95
|
-
}, documentApiBaseUrl);
|
|
96
|
-
if (!res.ok)
|
|
97
|
-
throw new Error(`Failed to load documents (${res.status})`);
|
|
98
|
-
const data = await res.json();
|
|
99
|
-
const nextDocuments = data.documents || [];
|
|
100
|
-
logDocumentManagerDebug('list.loaded', {
|
|
101
|
-
runtimeMode,
|
|
102
|
-
apiBaseUrl: documentApiBaseUrl || '(same-origin)',
|
|
103
|
-
count: Array.isArray(nextDocuments) ? nextDocuments.length : 0,
|
|
104
|
-
});
|
|
105
|
-
setDocuments(nextDocuments);
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
setError(e?.message || 'Failed to load documents');
|
|
109
|
-
logDocumentManagerDebug('list.error', {
|
|
110
|
-
runtimeMode,
|
|
111
|
-
apiBaseUrl: documentApiBaseUrl || '(same-origin)',
|
|
112
|
-
message: e?.message || 'Failed to load documents',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
setLoading(false);
|
|
117
|
-
}
|
|
118
|
-
}, [documentApiBaseUrl, runtimeMode]);
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
fetchDocuments();
|
|
121
|
-
}, [fetchDocuments]);
|
|
122
|
-
// Auto-select first doc when list loads
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (requestedDocPath || requestedDocAssetId)
|
|
125
|
-
return;
|
|
126
|
-
if (!selectedDoc && documents.length > 0) {
|
|
127
|
-
setSelectedDoc(documents[0]);
|
|
128
|
-
}
|
|
129
|
-
}, [documents, selectedDoc, requestedDocAssetId, requestedDocPath]);
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if ((!requestedDocPath && !requestedDocAssetId) || documents.length === 0)
|
|
132
|
-
return;
|
|
133
|
-
const requested = requestedDocPath ? normalizeDocPath(requestedDocPath) : '';
|
|
134
|
-
const requestedAssetId = String(requestedDocAssetId || '').trim().toLowerCase()
|
|
135
|
-
|| extractAssetIdFromPath(requestedDocPath);
|
|
136
|
-
const matched = documents.find((doc) => {
|
|
137
|
-
const candidateAssetId = extractAssetIdFromPath(doc.fsPath) || String(doc.assetId || '').trim().toLowerCase() || null;
|
|
138
|
-
if (requestedAssetId && candidateAssetId && candidateAssetId === requestedAssetId)
|
|
139
|
-
return true;
|
|
140
|
-
if (!requested)
|
|
141
|
-
return false;
|
|
142
|
-
const candidate = normalizeDocPath(doc.fsPath);
|
|
143
|
-
return docPathMatches(candidate, requested);
|
|
144
|
-
});
|
|
145
|
-
if (matched) {
|
|
146
|
-
setSelectedDoc(matched);
|
|
147
|
-
onRequestedDocHandled?.();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
}, [requestedDocAssetId, requestedDocPath, documents, onRequestedDocHandled]);
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if ((requestedDocPath || requestedDocAssetId) && !selectedDoc)
|
|
153
|
-
return;
|
|
154
|
-
const filtered = documents.filter((doc) => {
|
|
155
|
-
const readableName = getReadableDocumentName(doc);
|
|
156
|
-
const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
|
|
157
|
-
const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
|
|
158
|
-
const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
|
|
159
|
-
const attachmentState = getEffectiveAttachmentState(doc);
|
|
160
|
-
const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
|
|
161
|
-
return matchesSearch && matchesType && matchesAttachment;
|
|
162
|
-
});
|
|
163
|
-
if (selectedDoc && filtered.some((doc) => doc.id === selectedDoc.id))
|
|
164
|
-
return;
|
|
165
|
-
setSelectedDoc(filtered[0] ?? null);
|
|
166
|
-
}, [attachmentFilters, documents, requestedDocAssetId, requestedDocPath, searchQuery, selectedDoc, typeFilters]);
|
|
167
|
-
return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: documents, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, typeFilters: typeFilters, attachmentFilters: attachmentFilters, onSearchChange: setSearchQuery, onSelectDoc: setSelectedDoc, onRefresh: fetchDocuments, enableTaskGeneration: enableTaskGeneration }));
|
|
24
|
+
return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: filteredDocs, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, hasActiveFilters: hasActiveFilters, onSearchChange: setSearchQuery, onSelectDoc: selectDoc, onRefresh: fetchDocuments, onDeleteSelected: handleDeleteSelected, enableTaskGeneration: enableTaskGeneration }));
|
|
168
25
|
}
|