@taskforcehq/taskforce 0.3.300 → 0.3.302
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 +19 -0
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +16 -11
- package/dist/hooks/useTaskData.test.js +127 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
|
@@ -2,16 +2,21 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React, { useMemo, useCallback, useState, useEffect, useRef, useLayoutEffect, Suspense } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
5
|
-
import { Settings, Plus, Filter, Layers, User, Maximize2, Minimize2, Check, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, LogOut, NotebookTabs, ClipboardCheck, Cloud, CloudOff,
|
|
5
|
+
import { Settings, Plus, Filter, Layers, User, Maximize2, Minimize2, Check, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, LogOut, NotebookTabs, ClipboardCheck, Cloud, CloudOff, Users, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, Bot, Image, Workflow } from 'lucide-react';
|
|
6
6
|
import { STATUS_OPTIONS, getStatusDefinition } from '../../utils/constants';
|
|
7
7
|
import styles from '../../Taskforce.module.css';
|
|
8
8
|
import shellStyles from './WorkspaceShell.module.css';
|
|
9
9
|
import accountStyles from './StandaloneAccount.module.css';
|
|
10
10
|
import authStyles from './AuthShell.module.css';
|
|
11
11
|
import folderBrowserStyles from './FolderBrowser.module.css';
|
|
12
|
-
import managementStyles from './StandaloneManagement.module.css';
|
|
13
|
-
import syncStatusStyles from './SyncStatusModal.module.css';
|
|
14
12
|
import modalStyles from '../ui/Modal.module.css';
|
|
13
|
+
import { CreateWorkspaceConfirmModal } from './standalone/modals/CreateWorkspaceConfirmModal';
|
|
14
|
+
import { AccountHubModal } from './standalone/modals/AccountHubModal';
|
|
15
|
+
import { EditProfileModal } from './standalone/modals/EditProfileModal';
|
|
16
|
+
import { HelpModal } from './standalone/modals/HelpModal';
|
|
17
|
+
import { ScheduleWarningModal } from './standalone/modals/ScheduleWarningModal';
|
|
18
|
+
import { SyncStatusModal } from './standalone/modals/SyncStatusModal';
|
|
19
|
+
import { TeamManagementModal } from './standalone/modals/TeamManagementModal';
|
|
15
20
|
import { Modal } from '../ui/Modal';
|
|
16
21
|
import { TaskForm } from '../task/TaskForm';
|
|
17
22
|
import { TaskKanban } from '../task/TaskKanban';
|
|
@@ -28,8 +33,11 @@ import { getImageReferenceLabel } from '../../utils/imageReferences';
|
|
|
28
33
|
import { buildCustomTaxonomySortOptions } from '../../utils/taxonomySorting.js';
|
|
29
34
|
import { getLocale, t } from '../../localization';
|
|
30
35
|
import { resolveAnnotatedAttachmentDetail } from '../../utils/annotatedAttachments';
|
|
36
|
+
import { invalidateBillingStatusCache, loadBillingStatusCached } from '../../utils/billingStatusCache';
|
|
37
|
+
import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
|
|
31
38
|
import { AppShellHeader } from './AppShellHeader';
|
|
32
39
|
import { getTaskforceCloudEnvironmentConfig, parseTaskforceCloudEnvironment, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
|
|
40
|
+
import { logBootstrapDebug } from '../../hooks/workspace/workspaceLocalState';
|
|
33
41
|
import { FilterToolbar } from './panels/FilterToolbar';
|
|
34
42
|
import { DocumentFilterToolbar } from './panels/DocumentFilterToolbar';
|
|
35
43
|
import { PlanningDrawer, getPlanningDrawerWidthPx, } from './panels/PlanningDrawer';
|
|
@@ -37,7 +45,7 @@ import { getWorkspaceChromeDefinition, getWorkspaceModuleDefinitions, resolveWor
|
|
|
37
45
|
import { EmptyWorkspaceFilterBar, WorkspaceHeaderActions, WorkspaceHeaderDivider, WorkspaceHeaderSearch, WorkspaceHeaderSelect, WorkspacePrimaryAction, } from './chrome/WorkspaceChromeSections';
|
|
38
46
|
import { ScheduleSidebar, SCHEDULE_SIDEBAR_WIDTH_PX } from './panels/ScheduleSidebar';
|
|
39
47
|
import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from '../features/documentWorkspaceFilters';
|
|
40
|
-
import { canManageTeamWorkspace as computeCanManageTeamWorkspace, canOpenTeamManagement as computeCanOpenTeamManagement
|
|
48
|
+
import { canManageTeamWorkspace as computeCanManageTeamWorkspace, canOpenTeamManagement as computeCanOpenTeamManagement } from './teamManagementAccess';
|
|
41
49
|
import { toDateOnlyLocal, parseDateOnlyLocal, startOfWeek, getDayOffsetFromWeekStart, addDays, dateOnlyToIsoWeekKey, SCHEDULE_DAY_KEYS, SCHEDULE_DAY_LABELS, } from './panels/scheduleUtils';
|
|
42
50
|
import { AGENTS_WORKSPACE_FEATURE_KEY, ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, DOCUMENT_WORKSPACE_FEATURE_KEY, INITIATIVES_WORKSPACE_FEATURE_KEY, WORKFLOWS_WORKSPACE_FEATURE_KEY, resolveFeatureAccess, } from '../../runtime/appGates';
|
|
43
51
|
// Heavy feature modules — lazy-loaded to reduce the initial app chunk size.
|
|
@@ -48,6 +56,8 @@ const WorkflowsModule = React.lazy(() => import('../features/WorkflowsModule').t
|
|
|
48
56
|
const AgentsModule = React.lazy(() => import('../features/AgentsModule').then(m => ({ default: m.AgentsModule })));
|
|
49
57
|
const InitiativesModule = React.lazy(() => import('../features/InitiativesModule').then(m => ({ default: m.InitiativesModule })));
|
|
50
58
|
const PlansPage = React.lazy(() => import('./PlansPage').then(m => ({ default: m.PlansPage })));
|
|
59
|
+
const PROFILE_AVATAR_ACCEPT = 'image/png,image/jpeg,image/webp,image/gif';
|
|
60
|
+
const PROFILE_AVATAR_MAX_BYTES = 5 * 1024 * 1024;
|
|
51
61
|
function getSyncEventStableKey(event, index = 0) {
|
|
52
62
|
const explicitId = String(event.id ?? '').trim();
|
|
53
63
|
if (explicitId)
|
|
@@ -147,6 +157,9 @@ function isDebugModeEnabled() {
|
|
|
147
157
|
return runtimeOverride;
|
|
148
158
|
return typeof __DEBUG_MODE__ !== 'undefined' && __DEBUG_MODE__;
|
|
149
159
|
}
|
|
160
|
+
function getTimingNow() {
|
|
161
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
162
|
+
}
|
|
150
163
|
export function StandaloneLayout(props) {
|
|
151
164
|
const WORKSPACE_TOOL_RAIL_WIDTH = 56;
|
|
152
165
|
const WORKSPACE_CONTENT_GUTTER = 6;
|
|
@@ -202,7 +215,7 @@ export function StandaloneLayout(props) {
|
|
|
202
215
|
// clearFilters, // This was duplicated, removed one instance
|
|
203
216
|
// activeTypes, // This was duplicated, removed one instance
|
|
204
217
|
// Settings State & Props
|
|
205
|
-
settingsModel, configLoaded, currentTheme, setCurrentTheme, pathSaved, saveSettings, keyShortcut, setKeyShortcut, globalWeekStartsOn, locale, jsonBackupEnabled, setJsonBackupEnabled, mcpHostRoot, setMcpHostRoot, settingsSection, setSettingsSection, runtimeMode, workspaceSwitchingEnabled, cloudAuthConfigured, authRequiredForApi, authBlocked, isAuthenticated, authWorkspaceId, authUserEmail, authUserDisplayName, authSessionResolved, realtimeSyncEnabled, realtimeSyncFlagSource, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, saveWorkspaceCloudSyncSettings, pushNotice, userGlobalSyncStatus, workspaceLastSuccessfulSyncAt, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, userGlobalSyncError, workspaceLastErrorMessage, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics, currentWorkspaceId, currentWorkspaceRole, availableWorkspaces, switchWorkspace, updateCurrentUserProfile, logout, projectRoot, projectName, mcpScriptPath, serverHostRoot,
|
|
218
|
+
settingsModel, configLoaded, currentTheme, setCurrentTheme, pathSaved, saveSettings, keyShortcut, setKeyShortcut, globalWeekStartsOn, locale, jsonBackupEnabled, setJsonBackupEnabled, mcpHostRoot, setMcpHostRoot, settingsSection, setSettingsSection, runtimeMode, workspaceSwitchingEnabled, cloudAuthConfigured, authRequiredForApi, authBlocked, isAuthenticated, authUserId, authWorkspaceId, authUserEmail, authUserDisplayName, authUserAvatarUrl, authSessionResolved, realtimeSyncEnabled, realtimeSyncFlagSource, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, saveWorkspaceCloudSyncSettings, pushNotice, userGlobalSyncStatus, workspaceLastSuccessfulSyncAt, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, userGlobalSyncError, workspaceLastErrorMessage, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics, currentWorkspaceId, currentWorkspaceRole, availableWorkspaces, switchWorkspace, updateCurrentUserProfile, logout, projectRoot, projectName, mcpScriptPath, serverHostRoot,
|
|
206
219
|
// Browser Props
|
|
207
220
|
showFolderBrowser, setShowFolderBrowser, folders, files, currentBrowsePath, fetchFolders, browserTarget, setBrowserTarget, handleSelectPath, handleAddPath, handleRemovePath,
|
|
208
221
|
// Settings Managers
|
|
@@ -430,6 +443,15 @@ export function StandaloneLayout(props) {
|
|
|
430
443
|
if (typeof state.planningDrawerOpen === 'boolean') {
|
|
431
444
|
setPlanningDrawerOpen(state.planningDrawerOpen);
|
|
432
445
|
}
|
|
446
|
+
if (typeof state.planningTreeCollapsed === 'boolean') {
|
|
447
|
+
setPlanningTreeCollapsed(state.planningTreeCollapsed);
|
|
448
|
+
}
|
|
449
|
+
if (typeof state.planningPrimaryCollapsed === 'boolean') {
|
|
450
|
+
setPlanningPrimaryCollapsed(state.planningPrimaryCollapsed);
|
|
451
|
+
}
|
|
452
|
+
if (typeof state.planningSecondaryCollapsed === 'boolean') {
|
|
453
|
+
setPlanningSecondaryCollapsed(state.planningSecondaryCollapsed);
|
|
454
|
+
}
|
|
433
455
|
if (Array.isArray(state.planningExpandedInitiativeIds)) {
|
|
434
456
|
setExpandedPlanningInitiativeIds(new Set(state.planningExpandedInitiativeIds
|
|
435
457
|
.map((value) => String(value || '').trim())
|
|
@@ -499,6 +521,9 @@ export function StandaloneLayout(props) {
|
|
|
499
521
|
documentTypeFilters,
|
|
500
522
|
documentAttachmentFilters,
|
|
501
523
|
planningDrawerOpen,
|
|
524
|
+
planningTreeCollapsed,
|
|
525
|
+
planningPrimaryCollapsed,
|
|
526
|
+
planningSecondaryCollapsed,
|
|
502
527
|
planningExpandedInitiativeIds: Array.from(expandedPlanningInitiativeIds),
|
|
503
528
|
planningDrawerDetail,
|
|
504
529
|
planningNestedWorkstreamDetailId,
|
|
@@ -526,6 +551,9 @@ export function StandaloneLayout(props) {
|
|
|
526
551
|
documentTypeFilters,
|
|
527
552
|
documentAttachmentFilters,
|
|
528
553
|
planningDrawerOpen,
|
|
554
|
+
planningTreeCollapsed,
|
|
555
|
+
planningPrimaryCollapsed,
|
|
556
|
+
planningSecondaryCollapsed,
|
|
529
557
|
expandedPlanningInitiativeIds,
|
|
530
558
|
planningDrawerDetail,
|
|
531
559
|
planningNestedWorkstreamDetailId,
|
|
@@ -802,16 +830,11 @@ export function StandaloneLayout(props) {
|
|
|
802
830
|
return map;
|
|
803
831
|
}, [assigneeOptions]);
|
|
804
832
|
const planningStructure = useMemo(() => {
|
|
805
|
-
const
|
|
806
|
-
?
|
|
807
|
-
:
|
|
808
|
-
?
|
|
809
|
-
: !
|
|
810
|
-
const planningWorkstreams = taskScope === 'deleted'
|
|
811
|
-
? []
|
|
812
|
-
: props.workstreams.filter((workstream) => taskScope === 'archived'
|
|
813
|
-
? Boolean(workstream.isArchived)
|
|
814
|
-
: !workstream.isArchived);
|
|
833
|
+
const planningScopedTasks = taskScope === 'archived'
|
|
834
|
+
? archivedScopeAllTasks
|
|
835
|
+
: taskScope === 'deleted'
|
|
836
|
+
? deletedScopeAllTasks
|
|
837
|
+
: props.tasks.filter((task) => !task.isArchived);
|
|
815
838
|
const summarizeTasks = (scopeTasks) => {
|
|
816
839
|
const taskCount = scopeTasks.length;
|
|
817
840
|
const completedTaskCount = scopeTasks.filter((task) => task.isArchived || task.status === 'done' || task.status === 'cancelled').length;
|
|
@@ -827,7 +850,7 @@ export function StandaloneLayout(props) {
|
|
|
827
850
|
const workstreamById = new Map();
|
|
828
851
|
const initiativeById = new Map();
|
|
829
852
|
const buildWorkstreamSummary = (workstream) => {
|
|
830
|
-
const scopedTasks =
|
|
853
|
+
const scopedTasks = planningScopedTasks.filter((task) => (task.workstreamId || null) === workstream.id);
|
|
831
854
|
const taskPreviews = scopedTasks.map((task) => ({
|
|
832
855
|
id: task.id,
|
|
833
856
|
referenceNumber: task.referenceNumber ?? null,
|
|
@@ -853,10 +876,10 @@ export function StandaloneLayout(props) {
|
|
|
853
876
|
workstreamById.set(summary.id, summary);
|
|
854
877
|
return summary;
|
|
855
878
|
};
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
.
|
|
879
|
+
const allWorkstreamSummaries = props.workstreams.map(buildWorkstreamSummary);
|
|
880
|
+
const allInitiatives = props.initiatives.map((initiative) => {
|
|
881
|
+
const workstreams = allWorkstreamSummaries
|
|
882
|
+
.filter((workstream) => (workstream.initiativeId || null) === initiative.id);
|
|
860
883
|
const initiativeScopedTasks = workstreams
|
|
861
884
|
.flatMap((workstream) => workstreamTaskIds.get(workstream.id) || [])
|
|
862
885
|
.map((taskId) => hierarchyTaskById.get(taskId))
|
|
@@ -878,9 +901,18 @@ export function StandaloneLayout(props) {
|
|
|
878
901
|
initiativeById.set(initiativeSummary.id, initiativeSummary);
|
|
879
902
|
return initiativeSummary;
|
|
880
903
|
});
|
|
881
|
-
const
|
|
904
|
+
const initiatives = taskScope === 'deleted'
|
|
905
|
+
? []
|
|
906
|
+
: allInitiatives.filter((initiative) => taskScope === 'archived'
|
|
907
|
+
? Boolean(initiative.isArchived)
|
|
908
|
+
: !initiative.isArchived);
|
|
909
|
+
const standaloneWorkstreams = allWorkstreamSummaries
|
|
882
910
|
.filter((workstream) => !workstream.initiativeId)
|
|
883
|
-
.
|
|
911
|
+
.filter((workstream) => taskScope === 'deleted'
|
|
912
|
+
? false
|
|
913
|
+
: taskScope === 'archived'
|
|
914
|
+
? Boolean(workstream.isArchived)
|
|
915
|
+
: !workstream.isArchived);
|
|
884
916
|
return {
|
|
885
917
|
initiatives,
|
|
886
918
|
standaloneWorkstreams,
|
|
@@ -890,11 +922,32 @@ export function StandaloneLayout(props) {
|
|
|
890
922
|
workstreamById,
|
|
891
923
|
initiativeById,
|
|
892
924
|
};
|
|
893
|
-
}, [assigneeLabelByValue,
|
|
894
|
-
const initiativeFilterOptions = useMemo(() => planningStructure.initiatives.map((initiative) =>
|
|
925
|
+
}, [archivedScopeAllTasks, assigneeLabelByValue, deletedScopeAllTasks, hierarchyTaskById, props.initiatives, props.tasks, props.workstreams, taskScope]);
|
|
926
|
+
const initiativeFilterOptions = useMemo(() => planningStructure.initiatives.map((initiative) => {
|
|
927
|
+
const referenceLabel = formatInitiativeReference(initiative);
|
|
928
|
+
return {
|
|
929
|
+
value: initiative.id,
|
|
930
|
+
label: referenceLabel ? `${referenceLabel} - ${initiative.title}` : initiative.title,
|
|
931
|
+
selectedLabel: referenceLabel || initiative.title,
|
|
932
|
+
};
|
|
933
|
+
}), [planningStructure.initiatives]);
|
|
895
934
|
const workstreamFilterOptions = useMemo(() => [
|
|
896
|
-
...planningStructure.initiatives.flatMap((initiative) => initiative.workstreams.map((workstream) =>
|
|
897
|
-
|
|
935
|
+
...planningStructure.initiatives.flatMap((initiative) => initiative.workstreams.map((workstream) => {
|
|
936
|
+
const referenceLabel = formatWorkstreamReference(workstream);
|
|
937
|
+
return {
|
|
938
|
+
value: workstream.id,
|
|
939
|
+
label: referenceLabel ? `${referenceLabel} - ${workstream.title}` : workstream.title,
|
|
940
|
+
selectedLabel: referenceLabel || workstream.title,
|
|
941
|
+
};
|
|
942
|
+
})),
|
|
943
|
+
...planningStructure.standaloneWorkstreams.map((workstream) => {
|
|
944
|
+
const referenceLabel = formatWorkstreamReference(workstream);
|
|
945
|
+
return {
|
|
946
|
+
value: workstream.id,
|
|
947
|
+
label: referenceLabel ? `${referenceLabel} - ${workstream.title}` : workstream.title,
|
|
948
|
+
selectedLabel: referenceLabel || workstream.title,
|
|
949
|
+
};
|
|
950
|
+
}),
|
|
898
951
|
], [planningStructure.initiatives, planningStructure.standaloneWorkstreams]);
|
|
899
952
|
const selectedPlanningTaskIds = useMemo(() => {
|
|
900
953
|
if (selectedPlanningWorkstreamId) {
|
|
@@ -941,13 +994,6 @@ export function StandaloneLayout(props) {
|
|
|
941
994
|
return exactInitiative;
|
|
942
995
|
return resolveInitiativeReference(planningStructure.initiatives, planningDraftInitiativeId);
|
|
943
996
|
}, [planningDraftInitiativeId, planningStructure.initiativeById, planningStructure.initiatives]);
|
|
944
|
-
useEffect(() => {
|
|
945
|
-
if (!planningDrawerOpen)
|
|
946
|
-
return;
|
|
947
|
-
setPlanningTreeCollapsed(false);
|
|
948
|
-
setPlanningPrimaryCollapsed(false);
|
|
949
|
-
setPlanningSecondaryCollapsed(false);
|
|
950
|
-
}, [planningDrawerOpen]);
|
|
951
997
|
useEffect(() => {
|
|
952
998
|
if (selectedPlanningInitiativeId && !planningStructure.initiativeById.has(selectedPlanningInitiativeId)) {
|
|
953
999
|
setSelectedPlanningInitiativeId('');
|
|
@@ -955,23 +1001,7 @@ export function StandaloneLayout(props) {
|
|
|
955
1001
|
if (selectedPlanningWorkstreamId && !planningStructure.workstreamById.has(selectedPlanningWorkstreamId)) {
|
|
956
1002
|
setSelectedPlanningWorkstreamId('');
|
|
957
1003
|
}
|
|
958
|
-
if (planningDrawerDetail?.type === 'initiative' && !planningStructure.initiativeById.has(planningDrawerDetail.id)) {
|
|
959
|
-
setPlanningDrawerDetail(null);
|
|
960
|
-
}
|
|
961
|
-
if (planningDrawerDetail?.type === 'workstream' && !planningStructure.workstreamById.has(planningDrawerDetail.id)) {
|
|
962
|
-
setPlanningDrawerDetail(null);
|
|
963
|
-
}
|
|
964
|
-
if (planningNestedWorkstreamDetailId && !planningStructure.workstreamById.has(planningNestedWorkstreamDetailId)) {
|
|
965
|
-
setPlanningNestedWorkstreamDetailId(null);
|
|
966
|
-
}
|
|
967
|
-
if (planningSecondaryEditor?.mode === 'edit'
|
|
968
|
-
&& !planningStructure.workstreamById.has(planningSecondaryEditor.targetId)) {
|
|
969
|
-
setPlanningSecondaryEditor(null);
|
|
970
|
-
}
|
|
971
1004
|
}, [
|
|
972
|
-
planningDrawerDetail,
|
|
973
|
-
planningNestedWorkstreamDetailId,
|
|
974
|
-
planningSecondaryEditor,
|
|
975
1005
|
planningStructure.initiativeById,
|
|
976
1006
|
planningStructure.workstreamById,
|
|
977
1007
|
selectedPlanningInitiativeId,
|
|
@@ -1038,6 +1068,29 @@ export function StandaloneLayout(props) {
|
|
|
1038
1068
|
return !!task.dueDate && task.dueDate < todayDateOnly;
|
|
1039
1069
|
}).length;
|
|
1040
1070
|
}, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
|
|
1071
|
+
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1072
|
+
const assignedTaskCount = useMemo(() => {
|
|
1073
|
+
if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
|
|
1074
|
+
return 0;
|
|
1075
|
+
return tasks.filter((task) => {
|
|
1076
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
1077
|
+
if (isTerminal)
|
|
1078
|
+
return false;
|
|
1079
|
+
return String(task.assignee || '').trim() === normalizedAuthUserId;
|
|
1080
|
+
}).length;
|
|
1081
|
+
}, [normalizedAuthUserId, tasks]);
|
|
1082
|
+
const assignedOverdueTaskCount = useMemo(() => {
|
|
1083
|
+
if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
|
|
1084
|
+
return 0;
|
|
1085
|
+
return tasks.filter((task) => {
|
|
1086
|
+
const isTerminal = task.status === 'done' || task.status === 'cancelled';
|
|
1087
|
+
if (isTerminal)
|
|
1088
|
+
return false;
|
|
1089
|
+
if (String(task.assignee || '').trim() !== normalizedAuthUserId)
|
|
1090
|
+
return false;
|
|
1091
|
+
return !!task.dueDate && task.dueDate < todayDateOnly;
|
|
1092
|
+
}).length;
|
|
1093
|
+
}, [normalizedAuthUserId, tasks, todayDateOnly]);
|
|
1041
1094
|
const expiredColumnCandidates = useMemo(() => {
|
|
1042
1095
|
if (groupBy !== 'schedule')
|
|
1043
1096
|
return [];
|
|
@@ -1147,7 +1200,10 @@ export function StandaloneLayout(props) {
|
|
|
1147
1200
|
const [showCreateWorkspaceConfirm, setShowCreateWorkspaceConfirm] = useState(false);
|
|
1148
1201
|
const [showTeamManagementModal, setShowTeamManagementModal] = useState(false);
|
|
1149
1202
|
const [teamManagementTab, setTeamManagementTab] = useState('members');
|
|
1203
|
+
const [teamWorkspaceRole, setTeamWorkspaceRole] = useState(null);
|
|
1150
1204
|
const [teamPlanMode, setTeamPlanMode] = useState('unknown');
|
|
1205
|
+
const [teamManagementAccessLoading, setTeamManagementAccessLoading] = useState(false);
|
|
1206
|
+
const [teamManagementAccessError, setTeamManagementAccessError] = useState(null);
|
|
1151
1207
|
const [teamMgmtError, setTeamMgmtError] = useState(null);
|
|
1152
1208
|
const [teamUsersLoading, setTeamUsersLoading] = useState(false);
|
|
1153
1209
|
const [teamUsers, setTeamUsers] = useState([]);
|
|
@@ -1170,11 +1226,20 @@ export function StandaloneLayout(props) {
|
|
|
1170
1226
|
const [billingNotice, setBillingNotice] = useState(null);
|
|
1171
1227
|
const [billingIntervalChoice, setBillingIntervalChoice] = useState('month');
|
|
1172
1228
|
const [profileDisplayNameDraft, setProfileDisplayNameDraft] = useState('');
|
|
1229
|
+
const [profileAvatarPreviewUrl, setProfileAvatarPreviewUrl] = useState('');
|
|
1230
|
+
const [profileAvatarDraftId, setProfileAvatarDraftId] = useState(null);
|
|
1231
|
+
const [profileAvatarBusy, setProfileAvatarBusy] = useState(false);
|
|
1173
1232
|
const [profileSaveBusy, setProfileSaveBusy] = useState(false);
|
|
1174
1233
|
const [profileSaveError, setProfileSaveError] = useState(null);
|
|
1175
1234
|
const [profileSaveNotice, setProfileSaveNotice] = useState(null);
|
|
1176
1235
|
const TEAM_AUDIT_PAGE_SIZE = 25;
|
|
1177
1236
|
const accountMenuRef = useRef(null);
|
|
1237
|
+
const profileAvatarInputRef = useRef(null);
|
|
1238
|
+
const accountSurfaceOpenStartedAtRef = useRef(null);
|
|
1239
|
+
const accountSurfacePendingRef = useRef({
|
|
1240
|
+
billing: false,
|
|
1241
|
+
teamManagement: false
|
|
1242
|
+
});
|
|
1178
1243
|
// showSearch state removed - search always visible in actions area
|
|
1179
1244
|
useEffect(() => {
|
|
1180
1245
|
if (!showAccountMenu)
|
|
@@ -1218,20 +1283,29 @@ export function StandaloneLayout(props) {
|
|
|
1218
1283
|
const fallbackId = String(currentWorkspaceId || '').trim();
|
|
1219
1284
|
return fallbackId || 'Workspace';
|
|
1220
1285
|
}, [availableWorkspaces, currentWorkspaceId, projectName, runtimeMode]);
|
|
1221
|
-
const canManageTeamWorkspace = computeCanManageTeamWorkspace(canAccessCloudTeamManagement,
|
|
1222
|
-
const canOpenTeamManagement = computeCanOpenTeamManagement(canAccessCloudTeamManagement,
|
|
1286
|
+
const canManageTeamWorkspace = computeCanManageTeamWorkspace(canAccessCloudTeamManagement, teamWorkspaceRole);
|
|
1287
|
+
const canOpenTeamManagement = computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode);
|
|
1223
1288
|
const authDisplayName = String(authUserDisplayName || '').trim();
|
|
1289
|
+
const authAvatarUrl = String(authUserAvatarUrl || '').trim();
|
|
1224
1290
|
const authEmailLabel = String(authUserEmail || '').trim();
|
|
1225
1291
|
const authIdentityLabel = authDisplayName || authEmailLabel;
|
|
1226
1292
|
const hasAuthIdentity = authIdentityLabel.length > 0;
|
|
1227
1293
|
const hasAuthEmail = authEmailLabel.length > 0;
|
|
1228
1294
|
const accountBadgeLabel = (authIdentityLabel || authEmailLabel || 'U').trim();
|
|
1229
1295
|
const accountBadgeInitial = accountBadgeLabel.charAt(0).toUpperCase() || 'U';
|
|
1296
|
+
const accountAvatarUrl = authAvatarUrl;
|
|
1297
|
+
const profileAvatarDisplayUrl = String(profileAvatarPreviewUrl || authAvatarUrl).trim();
|
|
1230
1298
|
const subscriptionLabel = String(billingStatus?.planName || billingStatus?.planId || '').trim();
|
|
1231
1299
|
const hasSubscription = subscriptionLabel.length > 0;
|
|
1232
1300
|
const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();
|
|
1233
1301
|
const runtimeLabel = runtimeMode === 'cloud' ? 'CLOUD' : 'LOCAL';
|
|
1234
1302
|
const apiBaseUrl = props.config?.apiBaseUrl || '';
|
|
1303
|
+
const accountCapabilityApiBaseUrl = useMemo(() => {
|
|
1304
|
+
const preferredBase = runtimeMode === 'local'
|
|
1305
|
+
? (props.config?.cloudAuthBaseUrl || props.config?.apiBaseUrl || '')
|
|
1306
|
+
: (props.config?.apiBaseUrl || props.config?.cloudAuthBaseUrl || '');
|
|
1307
|
+
return String(preferredBase || '').trim().replace(/\/+$/, '');
|
|
1308
|
+
}, [props.config?.apiBaseUrl, props.config?.cloudAuthBaseUrl, runtimeMode]);
|
|
1235
1309
|
const billingApiBaseUrl = useMemo(() => {
|
|
1236
1310
|
const preferredBase = runtimeMode === 'local'
|
|
1237
1311
|
? (props.config?.cloudAuthBaseUrl || props.config?.apiBaseUrl || '')
|
|
@@ -1240,10 +1314,10 @@ export function StandaloneLayout(props) {
|
|
|
1240
1314
|
}, [props.config?.apiBaseUrl, props.config?.cloudAuthBaseUrl, runtimeMode]);
|
|
1241
1315
|
const buildBillingApiUrl = useCallback((path) => {
|
|
1242
1316
|
const normalizedPath = String(path || '').trim();
|
|
1243
|
-
if (!
|
|
1317
|
+
if (!accountCapabilityApiBaseUrl)
|
|
1244
1318
|
return normalizedPath;
|
|
1245
|
-
return `${
|
|
1246
|
-
}, [
|
|
1319
|
+
return `${accountCapabilityApiBaseUrl}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`;
|
|
1320
|
+
}, [accountCapabilityApiBaseUrl]);
|
|
1247
1321
|
const connectedEnvironmentLabel = useMemo(() => resolveConnectedEnvironmentLabel(props.config?.cloudEnvironment, [
|
|
1248
1322
|
String(props.config?.cloudBaseUrl || ''),
|
|
1249
1323
|
String(props.config?.cloudAuthBaseUrl || ''),
|
|
@@ -1261,9 +1335,141 @@ export function StandaloneLayout(props) {
|
|
|
1261
1335
|
if (!showEditProfileModal)
|
|
1262
1336
|
return;
|
|
1263
1337
|
setProfileDisplayNameDraft(authDisplayName);
|
|
1338
|
+
setProfileAvatarPreviewUrl(authAvatarUrl);
|
|
1339
|
+
setProfileAvatarDraftId(null);
|
|
1264
1340
|
setProfileSaveError(null);
|
|
1265
1341
|
setProfileSaveNotice(null);
|
|
1266
|
-
}, [authDisplayName, showEditProfileModal]);
|
|
1342
|
+
}, [authAvatarUrl, authDisplayName, showEditProfileModal]);
|
|
1343
|
+
const discardProfileAvatarDraft = useCallback(async (draftId) => {
|
|
1344
|
+
const normalizedDraftId = String(draftId || '').trim();
|
|
1345
|
+
if (!normalizedDraftId || !cloudAuthConfigured)
|
|
1346
|
+
return;
|
|
1347
|
+
try {
|
|
1348
|
+
const baseUrl = runtimeMode === 'local'
|
|
1349
|
+
? (props.config?.cloudAuthBaseUrl || props.config?.apiBaseUrl || '')
|
|
1350
|
+
: (props.config?.apiBaseUrl || props.config?.cloudAuthBaseUrl || '');
|
|
1351
|
+
await fetchTaskforceApi('/api/taskforce/auth/profile/avatar/discard', {
|
|
1352
|
+
method: 'POST',
|
|
1353
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1354
|
+
credentials: 'include',
|
|
1355
|
+
body: JSON.stringify({ draftId: normalizedDraftId })
|
|
1356
|
+
}, baseUrl);
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
// Best-effort cleanup only.
|
|
1360
|
+
}
|
|
1361
|
+
}, [cloudAuthConfigured, props.config?.apiBaseUrl, props.config?.cloudAuthBaseUrl, runtimeMode]);
|
|
1362
|
+
const closeEditProfileModal = useCallback(() => {
|
|
1363
|
+
const pendingDraftId = profileAvatarDraftId;
|
|
1364
|
+
setShowEditProfileModal(false);
|
|
1365
|
+
setProfileSaveError(null);
|
|
1366
|
+
setProfileSaveNotice(null);
|
|
1367
|
+
setProfileAvatarDraftId(null);
|
|
1368
|
+
setProfileAvatarPreviewUrl(authAvatarUrl);
|
|
1369
|
+
if (pendingDraftId) {
|
|
1370
|
+
void discardProfileAvatarDraft(pendingDraftId);
|
|
1371
|
+
}
|
|
1372
|
+
}, [authAvatarUrl, discardProfileAvatarDraft, profileAvatarDraftId]);
|
|
1373
|
+
const handleProfileAvatarSelected = useCallback(async (file) => {
|
|
1374
|
+
if (!file || !cloudAuthConfigured)
|
|
1375
|
+
return;
|
|
1376
|
+
if (!file.type.startsWith('image/')) {
|
|
1377
|
+
setProfileSaveError('Profile photo must be an image file.');
|
|
1378
|
+
setProfileSaveNotice(null);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (file.size > PROFILE_AVATAR_MAX_BYTES) {
|
|
1382
|
+
setProfileSaveError('Profile photo must be 5 MB or smaller.');
|
|
1383
|
+
setProfileSaveNotice(null);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
setProfileAvatarBusy(true);
|
|
1387
|
+
setProfileSaveError(null);
|
|
1388
|
+
setProfileSaveNotice(null);
|
|
1389
|
+
try {
|
|
1390
|
+
const baseUrl = runtimeMode === 'local'
|
|
1391
|
+
? (props.config?.cloudAuthBaseUrl || props.config?.apiBaseUrl || '')
|
|
1392
|
+
: (props.config?.apiBaseUrl || props.config?.cloudAuthBaseUrl || '');
|
|
1393
|
+
const initRes = await fetchTaskforceApi('/api/taskforce/auth/profile/avatar/init', {
|
|
1394
|
+
method: 'POST',
|
|
1395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1396
|
+
credentials: 'include',
|
|
1397
|
+
body: JSON.stringify({
|
|
1398
|
+
originalName: file.name,
|
|
1399
|
+
mimeType: file.type,
|
|
1400
|
+
size: file.size
|
|
1401
|
+
})
|
|
1402
|
+
}, baseUrl);
|
|
1403
|
+
const initData = await initRes.json().catch(() => ({}));
|
|
1404
|
+
if (!initRes.ok || !initData?.success || typeof initData?.uploadUrl !== 'string' || typeof initData?.relativePath !== 'string') {
|
|
1405
|
+
throw new Error(initData?.error || 'Failed to start avatar upload.');
|
|
1406
|
+
}
|
|
1407
|
+
const uploadRes = await fetch(initData.uploadUrl, {
|
|
1408
|
+
method: String(initData.method || 'PUT'),
|
|
1409
|
+
headers: initData.headers || { 'Content-Type': file.type || 'application/octet-stream' },
|
|
1410
|
+
body: file
|
|
1411
|
+
});
|
|
1412
|
+
if (!uploadRes.ok) {
|
|
1413
|
+
throw new Error('Failed to upload avatar.');
|
|
1414
|
+
}
|
|
1415
|
+
const finalizeRes = await fetchTaskforceApi('/api/taskforce/auth/profile/avatar/finalize', {
|
|
1416
|
+
method: 'POST',
|
|
1417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1418
|
+
credentials: 'include',
|
|
1419
|
+
body: JSON.stringify({
|
|
1420
|
+
draftId: initData.draftId,
|
|
1421
|
+
relativePath: initData.relativePath
|
|
1422
|
+
})
|
|
1423
|
+
}, baseUrl);
|
|
1424
|
+
const finalizeData = await finalizeRes.json().catch(() => ({}));
|
|
1425
|
+
if (!finalizeRes.ok || !finalizeData?.success || typeof finalizeData?.draftId !== 'string') {
|
|
1426
|
+
throw new Error(finalizeData?.error || 'Failed to finalize avatar upload.');
|
|
1427
|
+
}
|
|
1428
|
+
const previousDraftId = profileAvatarDraftId;
|
|
1429
|
+
setProfileAvatarDraftId(finalizeData.draftId);
|
|
1430
|
+
setProfileAvatarPreviewUrl(typeof finalizeData?.avatarUrl === 'string' ? finalizeData.avatarUrl : '');
|
|
1431
|
+
setProfileSaveNotice('Profile photo ready to save.');
|
|
1432
|
+
if (previousDraftId && previousDraftId !== finalizeData.draftId) {
|
|
1433
|
+
void discardProfileAvatarDraft(previousDraftId);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
setProfileSaveError(error instanceof Error ? error.message : 'Failed to upload profile photo.');
|
|
1438
|
+
}
|
|
1439
|
+
finally {
|
|
1440
|
+
setProfileAvatarBusy(false);
|
|
1441
|
+
if (profileAvatarInputRef.current) {
|
|
1442
|
+
profileAvatarInputRef.current.value = '';
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}, [
|
|
1446
|
+
cloudAuthConfigured,
|
|
1447
|
+
discardProfileAvatarDraft,
|
|
1448
|
+
profileAvatarDraftId,
|
|
1449
|
+
props.config?.apiBaseUrl,
|
|
1450
|
+
props.config?.cloudAuthBaseUrl,
|
|
1451
|
+
runtimeMode
|
|
1452
|
+
]);
|
|
1453
|
+
const handleDiscardPendingProfileAvatar = useCallback(() => {
|
|
1454
|
+
if (!profileAvatarDraftId)
|
|
1455
|
+
return;
|
|
1456
|
+
const draftId = profileAvatarDraftId;
|
|
1457
|
+
setProfileAvatarDraftId(null);
|
|
1458
|
+
setProfileAvatarPreviewUrl(authAvatarUrl);
|
|
1459
|
+
setProfileSaveNotice(null);
|
|
1460
|
+
setProfileSaveError(null);
|
|
1461
|
+
void discardProfileAvatarDraft(draftId);
|
|
1462
|
+
}, [authAvatarUrl, discardProfileAvatarDraft, profileAvatarDraftId]);
|
|
1463
|
+
const handleRemoveProfileAvatar = useCallback(() => {
|
|
1464
|
+
const pendingDraftId = profileAvatarDraftId;
|
|
1465
|
+
setProfileAvatarDraftId(null);
|
|
1466
|
+
setProfileAvatarPreviewUrl('');
|
|
1467
|
+
setProfileSaveNotice(authAvatarUrl ? 'Profile photo will be removed when you save.' : null);
|
|
1468
|
+
setProfileSaveError(null);
|
|
1469
|
+
if (pendingDraftId) {
|
|
1470
|
+
void discardProfileAvatarDraft(pendingDraftId);
|
|
1471
|
+
}
|
|
1472
|
+
}, [authAvatarUrl, discardProfileAvatarDraft, profileAvatarDraftId]);
|
|
1267
1473
|
const handleSaveProfile = useCallback(async () => {
|
|
1268
1474
|
const trimmedDisplayName = profileDisplayNameDraft.trim();
|
|
1269
1475
|
if (!trimmedDisplayName) {
|
|
@@ -1274,53 +1480,111 @@ export function StandaloneLayout(props) {
|
|
|
1274
1480
|
setProfileSaveBusy(true);
|
|
1275
1481
|
setProfileSaveError(null);
|
|
1276
1482
|
setProfileSaveNotice(null);
|
|
1277
|
-
const result = await updateCurrentUserProfile({
|
|
1483
|
+
const result = await updateCurrentUserProfile({
|
|
1484
|
+
displayName: trimmedDisplayName,
|
|
1485
|
+
avatarDraftId: profileAvatarDraftId,
|
|
1486
|
+
clearAvatar: !profileAvatarDraftId && !profileAvatarPreviewUrl && Boolean(authAvatarUrl)
|
|
1487
|
+
});
|
|
1278
1488
|
if (!result.success) {
|
|
1279
1489
|
setProfileSaveError(result.error || 'Failed to update profile.');
|
|
1280
1490
|
setProfileSaveBusy(false);
|
|
1281
1491
|
return;
|
|
1282
1492
|
}
|
|
1283
1493
|
setProfileSaveNotice('Profile updated.');
|
|
1494
|
+
setProfileAvatarDraftId(null);
|
|
1495
|
+
setProfileAvatarPreviewUrl('');
|
|
1284
1496
|
setProfileSaveBusy(false);
|
|
1285
1497
|
setShowEditProfileModal(false);
|
|
1286
|
-
}, [profileDisplayNameDraft, updateCurrentUserProfile]);
|
|
1498
|
+
}, [authAvatarUrl, profileAvatarDraftId, profileAvatarPreviewUrl, profileDisplayNameDraft, updateCurrentUserProfile]);
|
|
1287
1499
|
const teamAdminHeaders = useCallback(() => {
|
|
1288
1500
|
const headers = {};
|
|
1289
1501
|
const workspaceId = String(teamManagementWorkspaceId || '').trim();
|
|
1290
1502
|
if (workspaceId)
|
|
1291
1503
|
headers['x-taskforce-workspace-id'] = workspaceId;
|
|
1504
|
+
if (currentWorkspaceRole === 'owner'
|
|
1505
|
+
|| currentWorkspaceRole === 'admin'
|
|
1506
|
+
|| currentWorkspaceRole === 'member'
|
|
1507
|
+
|| currentWorkspaceRole === 'read-only') {
|
|
1508
|
+
headers['x-taskforce-workspace-role'] = currentWorkspaceRole;
|
|
1509
|
+
}
|
|
1292
1510
|
return headers;
|
|
1293
|
-
}, [teamManagementWorkspaceId]);
|
|
1511
|
+
}, [currentWorkspaceRole, teamManagementWorkspaceId]);
|
|
1512
|
+
const startAccountSurfaceTiming = useCallback((surface) => {
|
|
1513
|
+
const expectsBilling = isAuthenticated;
|
|
1514
|
+
const expectsTeamManagement = canAccessCloudTeamManagement;
|
|
1515
|
+
accountSurfaceOpenStartedAtRef.current = getTimingNow();
|
|
1516
|
+
accountSurfacePendingRef.current = {
|
|
1517
|
+
billing: expectsBilling,
|
|
1518
|
+
teamManagement: expectsTeamManagement
|
|
1519
|
+
};
|
|
1520
|
+
logBootstrapDebug('account_surface_opened', {
|
|
1521
|
+
surface,
|
|
1522
|
+
expectsBilling,
|
|
1523
|
+
expectsTeamManagement
|
|
1524
|
+
});
|
|
1525
|
+
if (!expectsBilling && !expectsTeamManagement) {
|
|
1526
|
+
logBootstrapDebug('account_surface_ready', {
|
|
1527
|
+
surface,
|
|
1528
|
+
durationMs: 0,
|
|
1529
|
+
teamManagementVisible: false,
|
|
1530
|
+
billingPlanId: null
|
|
1531
|
+
});
|
|
1532
|
+
accountSurfaceOpenStartedAtRef.current = null;
|
|
1533
|
+
}
|
|
1534
|
+
}, [canAccessCloudTeamManagement, isAuthenticated]);
|
|
1535
|
+
const markAccountSurfaceLoadSettled = useCallback((kind) => {
|
|
1536
|
+
const pending = accountSurfacePendingRef.current;
|
|
1537
|
+
pending[kind] = false;
|
|
1538
|
+
if (pending.billing || pending.teamManagement)
|
|
1539
|
+
return;
|
|
1540
|
+
const startedAt = accountSurfaceOpenStartedAtRef.current;
|
|
1541
|
+
if (startedAt === null)
|
|
1542
|
+
return;
|
|
1543
|
+
logBootstrapDebug('account_surface_ready', {
|
|
1544
|
+
surface: showAccountHub ? 'account_hub' : (showAccountMenu ? 'account_menu' : 'closed'),
|
|
1545
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1546
|
+
teamManagementVisible: canOpenTeamManagement,
|
|
1547
|
+
billingPlanId: String(billingStatus?.planId || '').trim() || null
|
|
1548
|
+
});
|
|
1549
|
+
accountSurfaceOpenStartedAtRef.current = null;
|
|
1550
|
+
}, [billingStatus?.planId, canOpenTeamManagement, showAccountHub, showAccountMenu]);
|
|
1294
1551
|
const loadBillingStatus = useCallback(async () => {
|
|
1552
|
+
const billingStatusPath = '/api/taskforce/billing/status';
|
|
1553
|
+
const billingStatusUrl = buildBillingApiUrl(billingStatusPath);
|
|
1295
1554
|
if (!isAuthenticated) {
|
|
1296
1555
|
setBillingStatus(null);
|
|
1556
|
+
setBillingError(null);
|
|
1557
|
+
invalidateBillingStatusCache(billingStatusUrl);
|
|
1558
|
+
markAccountSurfaceLoadSettled('billing');
|
|
1297
1559
|
return null;
|
|
1298
1560
|
}
|
|
1561
|
+
const startedAt = getTimingNow();
|
|
1299
1562
|
setBillingLoading(true);
|
|
1300
1563
|
setBillingError(null);
|
|
1301
1564
|
try {
|
|
1302
|
-
const
|
|
1303
|
-
method: 'GET',
|
|
1304
|
-
credentials: 'include'
|
|
1305
|
-
});
|
|
1306
|
-
const payload = await res.json().catch(() => ({}));
|
|
1307
|
-
if (!res.ok) {
|
|
1308
|
-
setBillingStatus(null);
|
|
1309
|
-
setBillingError(String(payload?.error || 'Failed to load billing status.'));
|
|
1310
|
-
return null;
|
|
1311
|
-
}
|
|
1565
|
+
const payload = await loadBillingStatusCached(billingStatusUrl);
|
|
1312
1566
|
setBillingStatus(payload);
|
|
1567
|
+
logBootstrapDebug('account_billing_status_resolved', {
|
|
1568
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1569
|
+
planId: String(payload?.planId || '').trim() || null,
|
|
1570
|
+
gate: String(payload?.gate || '').trim() || null
|
|
1571
|
+
});
|
|
1313
1572
|
return payload;
|
|
1314
1573
|
}
|
|
1315
|
-
catch {
|
|
1574
|
+
catch (error) {
|
|
1316
1575
|
setBillingStatus(null);
|
|
1317
|
-
setBillingError('Failed to load billing status.');
|
|
1576
|
+
setBillingError(error instanceof Error ? error.message : 'Failed to load billing status.');
|
|
1577
|
+
logBootstrapDebug('account_billing_status_failed', {
|
|
1578
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1579
|
+
error: error instanceof Error ? error.message : 'Failed to load billing status.'
|
|
1580
|
+
});
|
|
1318
1581
|
return null;
|
|
1319
1582
|
}
|
|
1320
1583
|
finally {
|
|
1321
1584
|
setBillingLoading(false);
|
|
1585
|
+
markAccountSurfaceLoadSettled('billing');
|
|
1322
1586
|
}
|
|
1323
|
-
}, [buildBillingApiUrl, isAuthenticated]);
|
|
1587
|
+
}, [buildBillingApiUrl, isAuthenticated, markAccountSurfaceLoadSettled]);
|
|
1324
1588
|
const handleStartCheckout = useCallback(async () => {
|
|
1325
1589
|
const params = new URLSearchParams();
|
|
1326
1590
|
params.set('screen', 'plans');
|
|
@@ -1380,6 +1644,7 @@ export function StandaloneLayout(props) {
|
|
|
1380
1644
|
return;
|
|
1381
1645
|
}
|
|
1382
1646
|
setBillingNotice('Subscription updated.');
|
|
1647
|
+
invalidateBillingStatusCache(buildBillingApiUrl('/api/taskforce/billing/status'));
|
|
1383
1648
|
await loadBillingStatus();
|
|
1384
1649
|
}
|
|
1385
1650
|
catch {
|
|
@@ -1389,45 +1654,88 @@ export function StandaloneLayout(props) {
|
|
|
1389
1654
|
setBillingActionBusy(false);
|
|
1390
1655
|
}
|
|
1391
1656
|
}, [billingIntervalChoice, billingStatus?.planVersionId, buildBillingApiUrl, loadBillingStatus]);
|
|
1392
|
-
const
|
|
1393
|
-
if (!
|
|
1657
|
+
const loadTeamManagementAccess = useCallback(async () => {
|
|
1658
|
+
if (!canAccessCloudTeamManagement) {
|
|
1659
|
+
setTeamWorkspaceRole(null);
|
|
1394
1660
|
setTeamPlanMode('unknown');
|
|
1395
|
-
|
|
1396
|
-
|
|
1661
|
+
setTeamManagementAccessLoading(false);
|
|
1662
|
+
setTeamManagementAccessError(null);
|
|
1663
|
+
markAccountSurfaceLoadSettled('teamManagement');
|
|
1664
|
+
return { role: null, teamPlanMode: 'unknown', allowed: false };
|
|
1665
|
+
}
|
|
1666
|
+
const startedAt = getTimingNow();
|
|
1667
|
+
setTeamManagementAccessLoading(true);
|
|
1668
|
+
setTeamManagementAccessError(null);
|
|
1397
1669
|
try {
|
|
1398
|
-
const res = await
|
|
1670
|
+
const res = await fetchTaskforceApi('/api/taskforce/account/team-management-access', {
|
|
1399
1671
|
method: 'GET',
|
|
1400
1672
|
credentials: 'include',
|
|
1401
1673
|
headers: teamAdminHeaders()
|
|
1402
|
-
});
|
|
1674
|
+
}, accountCapabilityApiBaseUrl);
|
|
1403
1675
|
const payload = await res.json().catch(() => ({}));
|
|
1404
1676
|
if (!res.ok) {
|
|
1677
|
+
setTeamWorkspaceRole(null);
|
|
1405
1678
|
setTeamPlanMode('unknown');
|
|
1406
|
-
|
|
1679
|
+
setTeamManagementAccessError(String(payload?.error || 'Failed to load team management access.'));
|
|
1680
|
+
logBootstrapDebug('team_management_access_failed', {
|
|
1681
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1682
|
+
status: res.status
|
|
1683
|
+
});
|
|
1684
|
+
return { role: null, teamPlanMode: 'unknown', allowed: false };
|
|
1407
1685
|
}
|
|
1408
|
-
const
|
|
1686
|
+
const nextRole = payload?.role === 'owner' || payload?.role === 'admin' || payload?.role === 'member' || payload?.role === 'read-only'
|
|
1687
|
+
? payload.role
|
|
1688
|
+
: null;
|
|
1689
|
+
const nextMode = payload?.teamPlanMode === 'team' || payload?.teamPlanMode === 'personal'
|
|
1690
|
+
? payload.teamPlanMode
|
|
1691
|
+
: 'unknown';
|
|
1692
|
+
const allowed = payload?.allowed === true;
|
|
1693
|
+
setTeamWorkspaceRole(nextRole);
|
|
1409
1694
|
setTeamPlanMode(nextMode);
|
|
1410
|
-
|
|
1695
|
+
setTeamManagementAccessError(null);
|
|
1696
|
+
logBootstrapDebug('team_management_access_loaded', {
|
|
1697
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1698
|
+
role: nextRole,
|
|
1699
|
+
teamPlanMode: nextMode,
|
|
1700
|
+
allowed
|
|
1701
|
+
});
|
|
1702
|
+
return { role: nextRole, teamPlanMode: nextMode, allowed };
|
|
1411
1703
|
}
|
|
1412
|
-
catch {
|
|
1704
|
+
catch (error) {
|
|
1705
|
+
setTeamWorkspaceRole(null);
|
|
1413
1706
|
setTeamPlanMode('unknown');
|
|
1414
|
-
|
|
1707
|
+
setTeamManagementAccessError(error instanceof Error ? error.message : 'Failed to load team management access.');
|
|
1708
|
+
logBootstrapDebug('team_management_access_failed', {
|
|
1709
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
1710
|
+
error: error instanceof Error ? error.message : 'Failed to load team management access.'
|
|
1711
|
+
});
|
|
1712
|
+
return { role: null, teamPlanMode: 'unknown', allowed: false };
|
|
1415
1713
|
}
|
|
1416
|
-
|
|
1714
|
+
finally {
|
|
1715
|
+
setTeamManagementAccessLoading(false);
|
|
1716
|
+
markAccountSurfaceLoadSettled('teamManagement');
|
|
1717
|
+
}
|
|
1718
|
+
}, [accountCapabilityApiBaseUrl, canAccessCloudTeamManagement, markAccountSurfaceLoadSettled, teamAdminHeaders]);
|
|
1417
1719
|
useEffect(() => {
|
|
1418
1720
|
if (!showAccountMenu && !showTeamManagementModal && !showAccountHub)
|
|
1419
1721
|
return;
|
|
1420
|
-
void
|
|
1421
|
-
}, [showAccountHub, showAccountMenu, showTeamManagementModal,
|
|
1722
|
+
void loadTeamManagementAccess();
|
|
1723
|
+
}, [showAccountHub, showAccountMenu, showTeamManagementModal, loadTeamManagementAccess]);
|
|
1422
1724
|
useEffect(() => {
|
|
1423
1725
|
if (!isAuthenticated || !authSessionResolved)
|
|
1424
1726
|
return;
|
|
1425
|
-
if (!
|
|
1727
|
+
if (!canAccessCloudTeamManagement) {
|
|
1728
|
+
setTeamWorkspaceRole(null);
|
|
1426
1729
|
setTeamPlanMode('unknown');
|
|
1427
1730
|
return;
|
|
1428
1731
|
}
|
|
1429
|
-
void
|
|
1430
|
-
}, [authSessionResolved,
|
|
1732
|
+
void loadTeamManagementAccess();
|
|
1733
|
+
}, [authSessionResolved, canAccessCloudTeamManagement, isAuthenticated, loadTeamManagementAccess]);
|
|
1734
|
+
useEffect(() => {
|
|
1735
|
+
if (!isAuthenticated || !authSessionResolved)
|
|
1736
|
+
return;
|
|
1737
|
+
void loadBillingStatus();
|
|
1738
|
+
}, [authSessionResolved, isAuthenticated, loadBillingStatus]);
|
|
1431
1739
|
useEffect(() => {
|
|
1432
1740
|
if (!showAccountHub && !showAccountMenu)
|
|
1433
1741
|
return;
|
|
@@ -1561,12 +1869,12 @@ export function StandaloneLayout(props) {
|
|
|
1561
1869
|
setShowAccountMenu(false);
|
|
1562
1870
|
setShowTeamManagementModal(true);
|
|
1563
1871
|
setTeamManagementTab('members');
|
|
1564
|
-
const
|
|
1565
|
-
if (
|
|
1872
|
+
const access = await loadTeamManagementAccess();
|
|
1873
|
+
if (!access.allowed)
|
|
1566
1874
|
return;
|
|
1567
1875
|
await loadTeamUsers();
|
|
1568
1876
|
await loadTeamAuditPage(0);
|
|
1569
|
-
}, [loadTeamAuditPage,
|
|
1877
|
+
}, [loadTeamAuditPage, loadTeamManagementAccess, loadTeamUsers]);
|
|
1570
1878
|
const executeTeamUserAction = useCallback(async (userId, fn) => {
|
|
1571
1879
|
setTeamMgmtError(null);
|
|
1572
1880
|
setTeamActionBusyUserId(userId);
|
|
@@ -1870,12 +2178,17 @@ export function StandaloneLayout(props) {
|
|
|
1870
2178
|
const activeReferenceMismatchSummaries = useMemo(() => (activeReferenceMismatchEvents.map((event, index) => {
|
|
1871
2179
|
const details = event.details && typeof event.details === 'object' ? event.details : null;
|
|
1872
2180
|
const pathValue = String(details?.path || '').trim();
|
|
2181
|
+
const referenceLabelValue = String(details?.referenceLabel || '').trim();
|
|
2182
|
+
const taskTitleValue = String(details?.taskTitle || details?.existingTaskTitle || '').trim();
|
|
1873
2183
|
const existingRefValue = Number(details?.existingReferenceNumber);
|
|
1874
2184
|
const incomingRefValue = Number(details?.incomingReferenceNumber);
|
|
1875
|
-
const pathLabel = pathValue || `Mismatch ${index + 1}`;
|
|
1876
|
-
const refsLabel = (Number.isFinite(existingRefValue)
|
|
2185
|
+
const pathLabel = pathValue || taskTitleValue || referenceLabelValue || `Mismatch ${index + 1}`;
|
|
2186
|
+
const refsLabel = (Number.isFinite(existingRefValue)
|
|
2187
|
+
|| Number.isFinite(incomingRefValue))
|
|
1877
2188
|
? `Existing ${Number.isFinite(existingRefValue) ? existingRefValue : '?'} vs incoming ${Number.isFinite(incomingRefValue) ? incomingRefValue : '?'}`
|
|
1878
|
-
:
|
|
2189
|
+
: referenceLabelValue
|
|
2190
|
+
? `Both claimed ${referenceLabelValue}`
|
|
2191
|
+
: null;
|
|
1879
2192
|
return {
|
|
1880
2193
|
key: getSyncEventStableKey(event, index),
|
|
1881
2194
|
pathLabel,
|
|
@@ -2234,20 +2547,32 @@ export function StandaloneLayout(props) {
|
|
|
2234
2547
|
// References: isAuthenticated, authIdentityLabel, billingStatus, runtimeLabel,
|
|
2235
2548
|
// connectedEnvironmentLabel, canManageCloudWorkspaces, workspaceActionBusy,
|
|
2236
2549
|
// setWorkspacePanelOpen, canOpenTeamManagement, openTeamManagementModal, and more.
|
|
2237
|
-
const accountMenuControl = (_jsxs("div", { className: accountStyles.accountMenuWrap, ref: accountMenuRef, children: [_jsx("button", { className: `tf-control-icon ${accountStyles.avatarBtn}`, onClick: () =>
|
|
2550
|
+
const accountMenuControl = (_jsxs("div", { className: accountStyles.accountMenuWrap, ref: accountMenuRef, children: [_jsx("button", { className: `tf-control-icon ${accountStyles.avatarBtn}`, onClick: () => {
|
|
2551
|
+
setShowAccountMenu((prev) => {
|
|
2552
|
+
const next = !prev;
|
|
2553
|
+
if (next) {
|
|
2554
|
+
startAccountSurfaceTiming('account_menu');
|
|
2555
|
+
}
|
|
2556
|
+
else {
|
|
2557
|
+
accountSurfaceOpenStartedAtRef.current = null;
|
|
2558
|
+
accountSurfacePendingRef.current = { billing: false, teamManagement: false };
|
|
2559
|
+
}
|
|
2560
|
+
return next;
|
|
2561
|
+
});
|
|
2562
|
+
}, title: isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-label": isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-haspopup": "menu", "aria-expanded": showAccountMenu, children: _jsx("span", { className: accountStyles.avatarBadge, "aria-hidden": "true", children: isAuthenticated && accountAvatarUrl ? (_jsx("img", { src: accountAvatarUrl, alt: "", className: accountStyles.avatarImage })) : (isAuthenticated ? accountBadgeInitial : '?') }) }), showAccountMenu && (_jsxs("div", { className: accountStyles.accountMenu, role: "menu", "aria-label": "Account menu", children: [_jsxs("div", { className: accountStyles.accountMenuSection, children: [_jsx("div", { className: accountStyles.accountMenuSectionLabel, children: "Account" }), _jsx("div", { className: accountStyles.accountMenuHint, children: isAuthBootstrapPending
|
|
2238
2563
|
? 'Checking sign-in status...'
|
|
2239
2564
|
: isAuthenticated
|
|
2240
2565
|
? (_jsx(_Fragment, { children: hasAuthIdentity
|
|
2241
|
-
? (_jsx(_Fragment, { children: _jsxs("span", { className: accountStyles.accountIdentityBlock, children: [_jsx("span", { className: accountStyles.accountIdentityEmail, children: authIdentityLabel }), hasAuthEmail && authDisplayName && (_jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Email" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: authEmailLabel })] })), hasSubscription
|
|
2566
|
+
? (_jsx(_Fragment, { children: _jsxs("span", { className: accountStyles.accountIdentityBlock, children: [_jsx("span", { className: accountStyles.accountIdentityEmail, children: authIdentityLabel }), hasAuthEmail && authDisplayName && (_jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Email" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: authEmailLabel })] })), billingLoading ? (_jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Subscription" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: "Loading\u2026" })] })) : hasSubscription ? (_jsxs("button", { "aria-label": `Subscription ${subscriptionLabel}`, className: `${accountStyles.accountIdentityMetaLine} ${accountStyles.accountIdentityMetaAction}`, onClick: () => {
|
|
2242
2567
|
setShowAccountMenu(false);
|
|
2243
2568
|
openPlansScreen();
|
|
2244
|
-
}, type: "button", children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Subscription" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: subscriptionLabel })] })), _jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Runtime" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: runtimeLabel })] }), _jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Environment" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: connectedEnvironmentLabel })] })] }) }))
|
|
2569
|
+
}, type: "button", children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Subscription" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: subscriptionLabel })] })) : billingError ? (_jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Subscription" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: "Unavailable" })] })) : null, _jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Runtime" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: runtimeLabel })] }), _jsxs("span", { className: accountStyles.accountIdentityMetaLine, children: [_jsx("span", { className: accountStyles.accountIdentityMetaLabel, children: "Environment" }), _jsx("span", { className: accountStyles.accountIdentityMetaValue, children: connectedEnvironmentLabel })] })] }) }))
|
|
2245
2570
|
: 'Signed in' }))
|
|
2246
2571
|
: 'Not signed in' })] }), canManageCloudWorkspaces && (_jsxs("div", { className: accountStyles.accountMenuSection, children: [_jsx("div", { className: accountStyles.accountMenuSectionLabel, children: "Workspaces (Owned + Invited)" }), availableWorkspaces.length === 0 && (_jsx("div", { className: accountStyles.accountMenuHint, children: "No workspaces found for this account yet." })), availableWorkspaces.map((workspace) => (_jsxs("button", { className: `${accountStyles.accountMenuItem} ${workspace.id === currentWorkspaceId ? accountStyles.accountMenuItemActive : ''}`, onClick: () => handleSwitchWorkspace(workspace.id), role: "menuitem", disabled: workspaceActionBusy || workspace.id === currentWorkspaceId, title: workspace.description || workspace.name, children: [_jsx(Folder, { size: 15 }), _jsxs("span", { style: { display: 'flex', flexDirection: 'column', gap: '2px' }, children: [_jsx("span", { children: workspace.name }), _jsx("span", { className: accountStyles.accountMenuMeta, children: workspace.role })] })] }, workspace.id))), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2247
2572
|
setWorkspaceActionError('');
|
|
2248
2573
|
setShowCreateWorkspaceConfirm(true);
|
|
2249
2574
|
setShowAccountMenu(false);
|
|
2250
|
-
}, role: "menuitem", disabled: workspaceActionBusy, children: [_jsx(Plus, { size: 15 }), "Create Workspace"] }), workspaceActionError && (_jsx("div", { className: accountStyles.accountMenuError, children: workspaceActionError }))] })), canOpenTeamManagement
|
|
2575
|
+
}, role: "menuitem", disabled: workspaceActionBusy, children: [_jsx(Plus, { size: 15 }), "Create Workspace"] }), workspaceActionError && (_jsx("div", { className: accountStyles.accountMenuError, children: workspaceActionError }))] })), teamManagementAccessLoading ? (_jsx("div", { className: accountStyles.accountMenuHint, children: "Resolving team management access..." })) : canOpenTeamManagement ? (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => { void openTeamManagementModal(); }, role: "menuitem", children: [_jsx(Users, { size: 15 }), "Team Management"] })) : teamManagementAccessError ? (_jsx("div", { className: accountStyles.accountMenuHint, children: "Team Management unavailable right now." })) : null, isAuthenticated && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2251
2576
|
setShowAccountMenu(false);
|
|
2252
2577
|
setShowEditProfileModal(true);
|
|
2253
2578
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Edit Profile"] })), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
@@ -2255,6 +2580,7 @@ export function StandaloneLayout(props) {
|
|
|
2255
2580
|
openPlansScreen();
|
|
2256
2581
|
}, role: "menuitem", children: [_jsx(Layers, { size: 15 }), "Plans"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2257
2582
|
setShowAccountMenu(false);
|
|
2583
|
+
startAccountSurfaceTiming('account_hub');
|
|
2258
2584
|
setShowAccountHub(true);
|
|
2259
2585
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2260
2586
|
setShowAccountMenu(false);
|
|
@@ -2887,7 +3213,7 @@ export function StandaloneLayout(props) {
|
|
|
2887
3213
|
const workspacePrimaryAction = workspaceChromeDefinition.primaryAction
|
|
2888
3214
|
? primaryActionRegistry[workspaceChromeDefinition.primaryAction]
|
|
2889
3215
|
: null;
|
|
2890
|
-
return (_jsxs("div", { className: `${shellStyles.standaloneWrapper} ${zenMode ? styles.zenModeEnabled : ''}`, "data-theme": currentTheme, children: [_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode, theme: currentTheme, meta: isPlansScreen ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.taskCountBadge, title: activeCountTitle, children: activeCountLabel }), shouldShowSyncStatus && (_jsx("button", { className: "tf-control-icon", onClick: () => setShowSyncStatusModal(true), title: `Sync manager: ${syncStatusMeta.label} | Last success: ${formattedLastSyncTime}`, style: {
|
|
3216
|
+
return (_jsxs("div", { className: `${shellStyles.standaloneWrapper} ${zenMode ? styles.zenModeEnabled : ''}`, "data-theme": currentTheme, children: [_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode, theme: currentTheme, meta: isPlansScreen ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.taskCountBadge, title: activeCountTitle, children: activeCountLabel }), normalizedAuthUserId && normalizedAuthUserId !== 'anonymous' && (_jsxs(_Fragment, { children: [_jsxs("span", { className: styles.taskCountBadge, title: "Open tasks assigned to you", children: ["Mine ", assignedTaskCount] }), _jsxs("span", { className: styles.taskCountBadge, title: "Your open tasks with overdue due dates", children: ["Overdue ", assignedOverdueTaskCount] })] })), shouldShowSyncStatus && (_jsx("button", { className: "tf-control-icon", onClick: () => setShowSyncStatusModal(true), title: `Sync manager: ${syncStatusMeta.label} | Last success: ${formattedLastSyncTime}`, style: {
|
|
2891
3217
|
marginLeft: '6px',
|
|
2892
3218
|
height: '24px',
|
|
2893
3219
|
width: '24px',
|
|
@@ -3020,114 +3346,71 @@ export function StandaloneLayout(props) {
|
|
|
3020
3346
|
return;
|
|
3021
3347
|
}
|
|
3022
3348
|
void handleUnarchive(taskId);
|
|
3023
|
-
}, currentTask: currentTask })] }), _jsx(Modal, { isOpen: activeTab === 'settings', onClose: handleCloseModal, title: "Settings", size: "xl", theme: currentTheme, draggable: true, isSettings: true, closeOnOverlayClick: false, children: _jsx(Suspense, { fallback: _jsx("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem' }, children: _jsx(Loader2, { size: 20, className: styles.spinner }) }), children: _jsx(TaskSettings, { settingsModel: settingsModel, onSectionChange: setSettingsSection }) }) }), _jsx(
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
}));
|
|
3089
|
-
}, children: "Revoke Invite" })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
|
|
3090
|
-
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}?workspaceId=${encodeURIComponent(teamManagementWorkspaceId)}`, {
|
|
3091
|
-
method: 'DELETE',
|
|
3092
|
-
headers: teamAdminHeaders(),
|
|
3093
|
-
credentials: 'include'
|
|
3094
|
-
}));
|
|
3095
|
-
}, children: "Remove" })] })] }, member.userId)))] })), teamManagementTab === 'invites' && (_jsxs("div", { className: accountStyles.accountHubCard, children: [teamInviteFeedback && (_jsx("p", { className: managementStyles.sectionText, children: teamInviteFeedback })), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: `${styles.label} ${managementStyles.labelFixed}`, children: "Email" }), _jsx("input", { className: styles.input, value: teamInviteEmail, onChange: (event) => setTeamInviteEmail(event.target.value), placeholder: "name@company.com" })] }), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: `${styles.label} ${managementStyles.labelFixed}`, children: "Role" }), _jsxs("select", { className: `${styles.input} ${managementStyles.selectRole}`, value: teamInviteRole, onChange: (event) => setTeamInviteRole(event.target.value === 'admin' ? 'admin' : 'member'), children: [_jsx("option", { value: "member", children: "Member" }), _jsx("option", { value: "admin", children: "Admin" })] }), teamInviteRole === 'member' && (_jsxs("select", { className: `${styles.input} ${managementStyles.selectPermission}`, value: teamInvitePermissionMode, onChange: (event) => setTeamInvitePermissionMode(event.target.value === 'read-only' ? 'read-only' : 'read-write'), children: [_jsx("option", { value: "read-write", children: "Read / Write" }), _jsx("option", { value: "read-only", children: "Read Only" })] })), _jsx("button", { className: styles.submitBtn, disabled: teamInviteBusy || !teamInviteEmail.trim() || teamPlanMode !== 'team', onClick: () => { void submitTeamInvite(); }, children: teamInviteBusy ? 'Sending...' : 'Send Invite' })] }), _jsxs("div", { className: managementStyles.inviteRow, children: [_jsx("div", { className: managementStyles.listHeading, children: "Pending Invites" }), pendingInvites.length === 0 && (_jsx("p", { className: managementStyles.mutedText, children: "No pending invites." })), pendingInvites.map((invite) => (_jsxs("div", { className: managementStyles.memberRow, children: [_jsx("div", { className: managementStyles.memberTitle, children: invite.displayName || invite.email }), _jsxs("div", { className: accountStyles.accountMenuMeta, children: [invite.email, " \u00B7 ", invite.role] })] }, invite.userId)))] })] })), teamManagementTab === 'audit' && (_jsxs("div", { className: accountStyles.accountHubCard, children: [teamAuditLoading && _jsx("p", { className: managementStyles.mutedText, children: "Loading audit entries..." }), !teamAuditLoading && teamAuditEvents.length === 0 && (_jsx("p", { className: managementStyles.mutedText, children: "No audit events for this workspace." })), !teamAuditLoading && (_jsx("div", { className: managementStyles.auditList, children: teamAuditEvents.map((event) => (_jsxs("div", { className: managementStyles.memberRow, children: [_jsx("div", { className: accountStyles.accountMenuMeta, children: new Date(event.createdAt).toLocaleString() }), _jsx("div", { className: managementStyles.memberTitle, children: event.action }), _jsxs("div", { className: accountStyles.accountMenuMeta, children: [event.actorUserId, " (", event.actorRole, ")"] })] }, event.id))) })), _jsxs("div", { className: managementStyles.auditPager, children: [_jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || teamAuditPage === 0, onClick: () => { void loadTeamAuditPage(teamAuditPage - 1); }, children: "Previous" }), _jsxs("span", { className: accountStyles.accountMenuMeta, children: ["Page ", teamAuditPage + 1, " of ", teamAuditPages] }), _jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || !teamAuditHasMore || teamAuditPage + 1 >= teamAuditPages, onClick: () => { void loadTeamAuditPage(teamAuditPage + 1); }, children: "Next" })] })] })), _jsx("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: _jsx("button", { className: styles.cancelBtn, onClick: () => setShowTeamManagementModal(false), children: "Close" }) })] }) }), _jsx(Modal, { isOpen: showCreateWorkspaceConfirm, onClose: () => setShowCreateWorkspaceConfirm(false), title: "Create Workspace", size: "sm", theme: currentTheme, draggable: true, footer: (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: styles.secondaryButton, onClick: () => setShowCreateWorkspaceConfirm(false), children: "Cancel" }), _jsx("button", { type: "button", className: styles.primaryUpdateBtn, onClick: () => openWorkspaceSetup({ intent: 'create-workspace' }), children: "Continue to Setup" })] })), children: _jsx("div", { className: `${modalStyles.form} ${managementStyles.modalBody}`, children: _jsx("p", { className: managementStyles.sectionText, children: "Start a new workspace with the guided setup flow. You will choose the workspace name, description, and starter taxonomy library before anything is created." }) }) }), _jsx(Modal, { isOpen: showSyncStatusModal, onClose: () => {
|
|
3096
|
-
setShowSyncStatusModal(false);
|
|
3097
|
-
}, title: "Sync Manager", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${syncStatusStyles.syncStatusModal}`, style: { gap: '10px' }, children: [_jsxs("div", { className: syncStatusStyles.syncStatusTop, children: [_jsx("div", { className: syncStatusStyles.syncStatusHeaderInfo, children: _jsxs("div", { className: syncStatusStyles.syncStatusMetaHeader, children: [_jsx("div", { className: syncStatusStyles.syncStatusBadge, style: {
|
|
3098
|
-
borderColor: syncStatusMeta.border,
|
|
3099
|
-
background: syncStatusMeta.background,
|
|
3100
|
-
color: syncStatusMeta.color
|
|
3101
|
-
}, children: syncStatusMeta.label }), _jsxs("span", { className: syncStatusStyles.syncStatusLabel, children: ["Workspace: ", _jsx("code", { children: currentWorkspaceLabel })] })] }) }), _jsx("label", { className: syncStatusStyles.syncToggle, children: _jsxs("span", { className: [
|
|
3102
|
-
syncStatusStyles.syncToggleControl,
|
|
3103
|
-
workspaceCloudSyncEnabled ? syncStatusStyles.syncToggleControlEnabled : syncStatusStyles.syncToggleControlDisabled,
|
|
3104
|
-
syncControlBusy ? syncStatusStyles.syncToggleControlBusy : '',
|
|
3105
|
-
!canManageWorkspaceSync ? syncStatusStyles.syncToggleControlBlocked : ''
|
|
3106
|
-
].filter(Boolean).join(' '), children: [_jsx("input", { className: syncStatusStyles.syncToggleInput, type: "checkbox", role: "switch", "aria-label": "Enable Sync", checked: workspaceCloudSyncEnabled, disabled: !canManageWorkspaceSync || syncControlBusy, onChange: (event) => {
|
|
3107
|
-
void handleWorkspaceSyncToggle(event.target.checked);
|
|
3108
|
-
} }), _jsx("span", { className: `${syncStatusStyles.syncToggleState} ${syncStatusStyles.syncToggleStateOn}`, children: "On" }), _jsx("span", { className: `${syncStatusStyles.syncToggleState} ${syncStatusStyles.syncToggleStateOff}`, children: "Off" }), _jsx("span", { className: syncStatusStyles.syncToggleThumb })] }) })] }), _jsxs("div", { className: syncStatusStyles.syncStatusSummaryCompact, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Health" }), _jsx("span", { className: syncStatusStyles.syncStatusSummaryValue, children: workspaceSyncSummary })] }), workspaceSyncRepairBusy && (_jsxs("div", { className: syncStatusStyles.syncStatusRepairBanner, children: [_jsxs("div", { className: syncStatusStyles.syncStatusRepairHeader, children: [_jsxs("div", { className: syncStatusStyles.syncStatusRepairBadge, children: [_jsx(Loader2, { size: 13, className: styles.spinner }), _jsx("span", { children: "Repair In Progress" })] }), _jsx("span", { className: syncStatusStyles.syncStatusRepairMeta, children: "Advanced recovery mode" })] }), _jsx("span", { className: syncStatusStyles.syncStatusRepairText, children: referenceMismatchCount > 0
|
|
3109
|
-
? `Taskforce is clearing the saved sync cursor and reconciling workspace state from the cloud again, including ${referenceMismatchCount} detected reference mismatch${referenceMismatchCount === 1 ? '' : 'es'}. The panel may stay busy for a while during large repairs.`
|
|
3110
|
-
: 'Taskforce is clearing the saved sync cursor and reconciling workspace state from the cloud again. The panel may stay busy for a while during large repairs.' })] })), _jsx("div", { className: syncStatusStyles.syncStatusSectionHeader, children: _jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Current Session" }) }), _jsxs("div", { className: syncStatusStyles.syncStatusCompactGrid, children: [_jsxs("div", { className: syncStatusStyles.syncStatusCompactItem, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Stage" }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: syncStageLabel })] }), _jsxs("div", { className: syncStatusStyles.syncStatusCompactItem, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Local Changes" }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: workspaceSyncPendingChanges })] }), _jsxs("div", { className: syncStatusStyles.syncStatusCompactItem, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Last Successful" }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: formattedLastSyncTime })] })] }), _jsx("div", { className: syncStatusStyles.syncStatusSectionHeader, children: _jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Activity" }) }), _jsxs("div", { className: syncStatusStyles.syncStatusCompactGrid, children: [_jsxs("div", { className: syncStatusStyles.syncStatusCompactItem, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Last Pull" }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: formattedLastPullTime })] }), _jsxs("div", { className: syncStatusStyles.syncStatusCompactItem, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Last Push" }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: formattedLastPushTime })] })] }), syncLastError !== 'None' && (_jsxs("div", { className: syncStatusStyles.syncStatusPanel, style: { background: 'rgba(239, 68, 68, 0.05)', borderColor: 'rgba(239, 68, 68, 0.2)' }, children: [_jsxs("div", { className: syncStatusStyles.syncStatusSectionHeader, style: { marginTop: 0 }, children: [_jsx(AlertTriangle, { size: 12, color: "#fda4af" }), _jsxs("span", { className: syncStatusStyles.syncStatusLabel, style: { color: '#fda4af' }, children: ["Last Error (", workspaceSyncDiagnostics.lastErrorAt ? new Date(workspaceSyncDiagnostics.lastErrorAt).toLocaleTimeString() : 'Recent', ")"] })] }), _jsx("span", { className: `${syncStatusStyles.syncStatusValue} ${syncStatusStyles.syncStatusValueError}`, children: syncLastError })] })), referenceMismatchCount > 0 && (_jsxs("div", { className: syncStatusStyles.syncStatusPanel, style: { background: 'rgba(250, 204, 21, 0.08)', borderColor: 'rgba(250, 204, 21, 0.22)' }, children: [_jsxs("div", { className: syncStatusStyles.syncStatusPanelHeader, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, style: { color: '#fde68a' }, children: "Identifier Integrity" }), _jsxs("span", { className: syncStatusStyles.syncStatusPanelMeta, children: [referenceMismatchCount, " mismatch", referenceMismatchCount === 1 ? '' : 'es', " detected"] })] }), _jsx("span", { className: syncStatusStyles.syncStatusValue, children: "Document or image reference numbers disagreed during normal sync. Standard sync now preserves existing identifiers; use Repair to reconcile mismatched references deliberately." }), activeReferenceMismatchSummaries.length > 0 && (_jsx("div", { style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }, children: activeReferenceMismatchSummaries.map((item) => (_jsxs("div", { style: {
|
|
3111
|
-
display: 'flex',
|
|
3112
|
-
flexDirection: 'column',
|
|
3113
|
-
gap: '2px',
|
|
3114
|
-
padding: '8px 10px',
|
|
3115
|
-
borderRadius: '8px',
|
|
3116
|
-
background: 'rgba(15, 23, 42, 0.18)',
|
|
3117
|
-
border: '1px solid rgba(250, 204, 21, 0.16)'
|
|
3118
|
-
}, children: [_jsx("span", { className: syncStatusStyles.syncStatusValue, style: { fontSize: '12px', wordBreak: 'break-all' }, children: item.pathLabel }), item.refsLabel ? (_jsx("span", { className: syncStatusStyles.syncStatusPanelMeta, children: item.refsLabel })) : null] }, item.key))) }))] })), _jsxs("div", { className: syncStatusStyles.syncStatusPanel, children: [_jsxs("div", { className: syncStatusStyles.syncStatusPanelHeader, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Local Sync Diagnostics" }), _jsx("span", { className: syncStatusStyles.syncStatusPanelMeta, style: { fontSize: '10px' }, children: "AI profiles & metadata" })] }), _jsx("div", { className: syncStatusStyles.syncStatusCompactGrid, style: { gridTemplateColumns: 'repeat(2, 1fr)' }, children: syncDiagnosticsSummary.map((item) => (_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '8px' }, children: [_jsx("span", { className: syncStatusStyles.syncStatusLabel, style: { fontSize: '9px', textTransform: 'capitalize' }, children: item.label }), _jsx("span", { className: syncStatusStyles.syncStatusValue, style: { fontSize: '11px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: item.value })] }, item.label))) })] }), _jsxs("div", { className: syncStatusStyles.syncStatusPanel, children: [_jsx("div", { className: syncStatusStyles.syncStatusPanelHeader, children: _jsx("span", { className: syncStatusStyles.syncStatusLabel, children: "Recent Events" }) }), _jsx("div", { ref: syncEventsListRef, className: syncStatusStyles.syncStatusEventsList, style: { height: '120px' }, children: syncEventRows.map((row) => (_jsx("div", { className: [
|
|
3119
|
-
syncStatusStyles.syncStatusEventItem,
|
|
3120
|
-
row.tone === 'error' ? syncStatusStyles.syncStatusEventItemError : '',
|
|
3121
|
-
row.tone === 'muted' ? syncStatusStyles.syncStatusEventItemMuted : ''
|
|
3122
|
-
].filter(Boolean).join(' '), style: { padding: '6px 8px', borderRadius: '6px' }, children: _jsx("span", { className: syncStatusStyles.syncStatusEventLine, style: { fontSize: '11px' }, children: row.text }) }, row.key))) })] }), _jsxs("div", { className: `${modalStyles.formActions} ${syncStatusStyles.syncStatusActions}`, children: [_jsxs("div", { className: syncStatusStyles.syncStatusSecondaryActions, children: [_jsx("button", { className: styles.cancelBtn, onClick: handleQueueOrRunRepairSync, disabled: workspaceSyncRepairBusy, title: workspaceSyncRepairBusy
|
|
3123
|
-
? 'Repair is running now.'
|
|
3124
|
-
: workspaceSyncRepairQueued
|
|
3125
|
-
? 'Repair is queued and will start when the current sync finishes.'
|
|
3126
|
-
: workspaceSyncBusy
|
|
3127
|
-
? 'Queue a repair to start automatically when the current sync finishes.'
|
|
3128
|
-
: 'Advanced recovery: clear the saved pull cursor and re-fetch cloud state from the beginning.', children: workspaceSyncRepairBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), _jsx("span", { style: { marginLeft: '6px' }, children: "Repairing..." })] })) : workspaceSyncRepairQueued ? ('Repair Queued') : ('Repair') }), _jsxs("button", { className: styles.cancelBtn, onClick: () => { void handleCopySyncDetails(); }, title: "Copy the current sync manager report for support or AI troubleshooting.", children: [_jsx(Copy, { size: 14 }), _jsx("span", { style: { marginLeft: '6px' }, children: workspaceSyncCopied ? 'Copied' : 'Copy Report' })] })] }), _jsxs("div", { className: syncStatusStyles.syncStatusPrimaryActions, children: [_jsx("div", { className: syncStatusStyles.syncStatusPrimaryActionSlot, children: runtimeMode === 'local' && !isAuthenticated ? (_jsx("button", { className: styles.submitBtn, onClick: () => {
|
|
3129
|
-
openSharedLoginRoute('login');
|
|
3130
|
-
}, children: "Sign In / Register" })) : (_jsx("div", { className: syncStatusStyles.syncStatusActionPlaceholder, "aria-hidden": "true" })) }), _jsx("div", { className: syncStatusStyles.syncStatusPrimaryActionSlot, children: _jsx("button", { className: styles.submitBtn, onClick: () => void retryWorkspaceCloudSync(), disabled: workspaceSyncBusy, children: workspaceSyncBusy ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), "Syncing..."] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { size: 14 }), "Sync"] })) }) })] })] })] }) }), unsavedModalOpen && createPortal(_jsx("div", { className: `${modalStyles.overlay} ${modalStyles.unsavedOverlay}`, style: { zIndex: 2000 }, children: _jsxs("div", { className: `tf-surface-modal tf-modal-shell ${modalStyles.modal} ${modalStyles.unsavedModal}`, "data-theme": currentTheme, children: [_jsx("div", { className: `tf-modal-header ${modalStyles.unsavedHeader}`, children: _jsxs("div", { className: `tf-modal-title ${modalStyles.unsavedTitle}`, children: [_jsx(AlertTriangle, { size: 20 }), "Unsaved Changes"] }) }), _jsx("div", { className: `${modalStyles.form} ${modalStyles.unsavedContent}`, children: _jsx("p", { className: modalStyles.unsavedText, children: "You have unsaved changes. Would you like to save them?" }) }), _jsxs("div", { className: `${modalStyles.formActions} ${modalStyles.unsavedActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => setUnsavedModalOpen(false), title: "Close dialog and continue editing", children: "Keep Editing" }), _jsx("button", { className: styles.destructiveBtn, onClick: handleDiscard, title: "Discard unsaved changes and leave", children: "Discard" }), _jsxs("button", { className: styles.submitBtn, onClick: handleSaveAndNavigate, disabled: loading, title: "Save changes and leave", children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Save, { size: 16 }), "Save"] })] })] }) }), document.body), showFolderBrowser && createPortal(_jsx("div", { className: folderBrowserStyles.overlay, children: _jsxs("div", { className: folderBrowserStyles.browser, children: [_jsxs("div", { className: folderBrowserStyles.header, children: [_jsxs("div", { className: folderBrowserStyles.pathInfo, children: [_jsx(Folder, { size: 14 }), _jsx("span", { children: currentBrowsePath || 'Project Root' })] }), _jsxs("div", { className: folderBrowserStyles.actions, children: [currentBrowsePath && (_jsx("button", { className: styles.helpLink, onClick: () => {
|
|
3349
|
+
}, currentTask: currentTask })] }), _jsx(Modal, { isOpen: activeTab === 'settings', onClose: handleCloseModal, title: "Settings", size: "xl", theme: currentTheme, draggable: true, isSettings: true, closeOnOverlayClick: false, children: _jsx(Suspense, { fallback: _jsx("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem' }, children: _jsx(Loader2, { size: 20, className: styles.spinner }) }), children: _jsx(TaskSettings, { settingsModel: settingsModel, onSectionChange: setSettingsSection }) }) }), _jsx(ScheduleWarningModal, { prompt: scheduleWarningPrompt, theme: currentTheme, onClose: cancelScheduleWarning, onConfirm: confirmScheduleWarning }), _jsx(HelpModal, { isOpen: showHelpModal, theme: currentTheme, onClose: () => setShowHelpModal(false), onOpenSettings: () => {
|
|
3350
|
+
setShowHelpModal(false);
|
|
3351
|
+
setActiveTab('settings');
|
|
3352
|
+
setSettingsSection('general');
|
|
3353
|
+
} }), _jsx(EditProfileModal, { isOpen: showEditProfileModal, theme: currentTheme, onClose: closeEditProfileModal, onSave: () => { void handleSaveProfile(); }, displayName: profileDisplayNameDraft, email: authEmailLabel, avatarDisplayUrl: profileAvatarDisplayUrl, accountBadgeInitial: accountBadgeInitial, avatarInputRef: profileAvatarInputRef, avatarAccept: PROFILE_AVATAR_ACCEPT, avatarDraftId: profileAvatarDraftId, saveBusy: profileSaveBusy, avatarBusy: profileAvatarBusy, saveError: profileSaveError, saveNotice: profileSaveNotice, onDisplayNameChange: setProfileDisplayNameDraft, onAvatarInputChange: (file) => { void handleProfileAvatarSelected(file); }, onStartAvatarUpload: () => profileAvatarInputRef.current?.click(), onDiscardUpload: handleDiscardPendingProfileAvatar, onRemovePhoto: handleRemoveProfileAvatar }), _jsx(AccountHubModal, { isOpen: showAccountHub, theme: currentTheme, runtimeMode: runtimeMode, authRequiredForApi: authRequiredForApi, isAuthenticated: isAuthenticated, hasAuthIdentity: hasAuthIdentity, authIdentityLabel: authIdentityLabel, billingLoading: billingLoading, billingError: billingError, billingActionError: billingActionError, billingNotice: billingNotice, billingActionBusy: billingActionBusy, billingIntervalChoice: billingIntervalChoice, billingStatus: billingStatus, currentWorkspaceId: currentWorkspaceId, canOpenTeamManagement: canOpenTeamManagement, canManageWorkspaceSync: canManageWorkspaceSync, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncStatusLabel: syncStatusMeta.label, workspaceSyncError: workspaceSyncError, syncControlBusy: syncControlBusy, onClose: () => setShowAccountHub(false), onOpenWorkspaceAudit: () => {
|
|
3354
|
+
setShowAccountHub(false);
|
|
3355
|
+
void openTeamManagementModal();
|
|
3356
|
+
setTeamManagementTab('audit');
|
|
3357
|
+
}, onBillingIntervalChange: setBillingIntervalChoice, onRefreshBilling: () => { void loadBillingStatus(); }, onUpdateInterval: () => { void handleSwitchSubscriptionInterval(); }, onManageBilling: () => { void handleOpenBillingPortal(); }, onStartCheckout: () => { void handleStartCheckout(); }, onToggleWorkspaceSync: (enabled) => { void handleWorkspaceSyncToggle(enabled); }, onOpenHelp: () => {
|
|
3358
|
+
setShowAccountHub(false);
|
|
3359
|
+
setShowHelpModal(true);
|
|
3360
|
+
} }), _jsx(TeamManagementModal, { isOpen: showTeamManagementModal, theme: currentTheme, teamPlanMode: teamPlanMode, teamMgmtError: teamMgmtError, teamManagementTab: teamManagementTab, teamUsersLoading: teamUsersLoading, teamUsers: teamUsers, teamActionBusyUserId: teamActionBusyUserId, teamInviteFeedback: teamInviteFeedback, teamInviteEmail: teamInviteEmail, teamInviteRole: teamInviteRole, teamInvitePermissionMode: teamInvitePermissionMode, teamInviteBusy: teamInviteBusy, pendingInvites: pendingInvites, teamAuditLoading: teamAuditLoading, teamAuditEvents: teamAuditEvents, teamAuditPage: teamAuditPage, teamAuditPages: teamAuditPages, teamAuditHasMore: teamAuditHasMore, onClose: () => setShowTeamManagementModal(false), onOpenMembersTab: () => {
|
|
3361
|
+
setTeamManagementTab('members');
|
|
3362
|
+
void loadTeamUsers();
|
|
3363
|
+
}, onOpenInvitesTab: () => setTeamManagementTab('invites'), onOpenAuditTab: () => {
|
|
3364
|
+
setTeamManagementTab('audit');
|
|
3365
|
+
void loadTeamAuditPage(teamAuditPage);
|
|
3366
|
+
}, onMemberRoleChange: (userId, role) => {
|
|
3367
|
+
void executeTeamUserAction(userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(userId)}/role`, {
|
|
3368
|
+
method: 'PATCH',
|
|
3369
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
3370
|
+
credentials: 'include',
|
|
3371
|
+
body: JSON.stringify({
|
|
3372
|
+
workspaceId: teamManagementWorkspaceId,
|
|
3373
|
+
role
|
|
3374
|
+
})
|
|
3375
|
+
}));
|
|
3376
|
+
}, onMemberPermissionChange: (userId, mode) => {
|
|
3377
|
+
void executeTeamUserAction(userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(userId)}/permission-mode`, {
|
|
3378
|
+
method: 'PATCH',
|
|
3379
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
3380
|
+
credentials: 'include',
|
|
3381
|
+
body: JSON.stringify({
|
|
3382
|
+
workspaceId: teamManagementWorkspaceId,
|
|
3383
|
+
mode
|
|
3384
|
+
})
|
|
3385
|
+
}));
|
|
3386
|
+
}, onToggleMemberDisabled: (userId, disabled) => {
|
|
3387
|
+
void executeTeamUserAction(userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(userId)}/disable`, {
|
|
3388
|
+
method: 'PATCH',
|
|
3389
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
3390
|
+
credentials: 'include',
|
|
3391
|
+
body: JSON.stringify({
|
|
3392
|
+
workspaceId: teamManagementWorkspaceId,
|
|
3393
|
+
disabled
|
|
3394
|
+
})
|
|
3395
|
+
}));
|
|
3396
|
+
}, onRevokeInvite: (userId) => {
|
|
3397
|
+
void executeTeamUserAction(userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(userId)}/invite/revoke`, {
|
|
3398
|
+
method: 'POST',
|
|
3399
|
+
headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
|
|
3400
|
+
credentials: 'include',
|
|
3401
|
+
body: JSON.stringify({ workspaceId: teamManagementWorkspaceId })
|
|
3402
|
+
}));
|
|
3403
|
+
}, onRemoveMember: (userId) => {
|
|
3404
|
+
void executeTeamUserAction(userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(userId)}?workspaceId=${encodeURIComponent(teamManagementWorkspaceId)}`, {
|
|
3405
|
+
method: 'DELETE',
|
|
3406
|
+
headers: teamAdminHeaders(),
|
|
3407
|
+
credentials: 'include'
|
|
3408
|
+
}));
|
|
3409
|
+
}, onInviteEmailChange: setTeamInviteEmail, onInviteRoleChange: setTeamInviteRole, onInvitePermissionModeChange: setTeamInvitePermissionMode, onSubmitInvite: () => { void submitTeamInvite(); }, onLoadAuditPrevious: () => { void loadTeamAuditPage(teamAuditPage - 1); }, onLoadAuditNext: () => { void loadTeamAuditPage(teamAuditPage + 1); } }), _jsx(CreateWorkspaceConfirmModal, { isOpen: showCreateWorkspaceConfirm, theme: currentTheme, onClose: () => setShowCreateWorkspaceConfirm(false), onConfirm: () => openWorkspaceSetup({ intent: 'create-workspace' }) }), _jsx(SyncStatusModal, { isOpen: showSyncStatusModal, theme: currentTheme, currentWorkspaceLabel: currentWorkspaceLabel, syncStatusMeta: syncStatusMeta, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncControlBusy: syncControlBusy, canManageWorkspaceSync: canManageWorkspaceSync, workspaceSyncSummary: workspaceSyncSummary, workspaceSyncRepairBusy: workspaceSyncRepairBusy, referenceMismatchCount: referenceMismatchCount, syncStageLabel: syncStageLabel, workspaceSyncPendingChanges: workspaceSyncPendingChanges, formattedLastSyncTime: formattedLastSyncTime, formattedLastPullTime: formattedLastPullTime, formattedLastPushTime: formattedLastPushTime, syncLastError: syncLastError, workspaceSyncDiagnostics: workspaceSyncDiagnostics, activeReferenceMismatchSummaries: activeReferenceMismatchSummaries, syncDiagnosticsSummary: syncDiagnosticsSummary, syncEventRows: syncEventRows, syncEventsListRef: syncEventsListRef, workspaceSyncRepairQueued: workspaceSyncRepairQueued, workspaceSyncBusy: workspaceSyncBusy, workspaceSyncCopied: workspaceSyncCopied, runtimeMode: runtimeMode, isAuthenticated: isAuthenticated, onClose: () => setShowSyncStatusModal(false), onToggleWorkspaceSync: (enabled) => {
|
|
3410
|
+
void handleWorkspaceSyncToggle(enabled);
|
|
3411
|
+
}, onRepairSync: handleQueueOrRunRepairSync, onCopyReport: () => { void handleCopySyncDetails(); }, onOpenLogin: () => {
|
|
3412
|
+
openSharedLoginRoute('login');
|
|
3413
|
+
}, onRetrySync: () => { void retryWorkspaceCloudSync(); } }), unsavedModalOpen && createPortal(_jsx("div", { className: `${modalStyles.overlay} ${modalStyles.unsavedOverlay}`, style: { zIndex: 2000 }, children: _jsxs("div", { className: `tf-surface-modal tf-modal-shell ${modalStyles.modal} ${modalStyles.unsavedModal}`, "data-theme": currentTheme, children: [_jsx("div", { className: `tf-modal-header ${modalStyles.unsavedHeader}`, children: _jsxs("div", { className: `tf-modal-title ${modalStyles.unsavedTitle}`, children: [_jsx(AlertTriangle, { size: 20 }), "Unsaved Changes"] }) }), _jsx("div", { className: `${modalStyles.form} ${modalStyles.unsavedContent}`, children: _jsx("p", { className: modalStyles.unsavedText, children: "You have unsaved changes. Would you like to save them?" }) }), _jsxs("div", { className: `${modalStyles.formActions} ${modalStyles.unsavedActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => setUnsavedModalOpen(false), title: "Close dialog and continue editing", children: "Keep Editing" }), _jsx("button", { className: styles.destructiveBtn, onClick: handleDiscard, title: "Discard unsaved changes and leave", children: "Discard" }), _jsxs("button", { className: styles.submitBtn, onClick: handleSaveAndNavigate, disabled: loading, title: "Save changes and leave", children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Save, { size: 16 }), "Save"] })] })] }) }), document.body), showFolderBrowser && createPortal(_jsx("div", { className: folderBrowserStyles.overlay, children: _jsxs("div", { className: folderBrowserStyles.browser, children: [_jsxs("div", { className: folderBrowserStyles.header, children: [_jsxs("div", { className: folderBrowserStyles.pathInfo, children: [_jsx(Folder, { size: 14 }), _jsx("span", { children: currentBrowsePath || 'Project Root' })] }), _jsxs("div", { className: folderBrowserStyles.actions, children: [currentBrowsePath && (_jsx("button", { className: styles.helpLink, onClick: () => {
|
|
3131
3414
|
const parts = currentBrowsePath.split('/').filter(Boolean);
|
|
3132
3415
|
parts.pop();
|
|
3133
3416
|
fetchFolders(parts.length ? parts.join('/') + '/' : '');
|