@taskforcehq/taskforce 0.3.301 → 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/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 +466 -204
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- 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 +3 -2
- package/dist/hooks/useTaskData.test.js +66 -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 +41 -19
- package/dist/mcp/runtime.test.js +49 -2
- 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-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.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-b6TLuHwJ.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 +2 -1
- 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-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -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,7 +922,7 @@ export function StandaloneLayout(props) {
|
|
|
890
922
|
workstreamById,
|
|
891
923
|
initiativeById,
|
|
892
924
|
};
|
|
893
|
-
}, [assigneeLabelByValue,
|
|
925
|
+
}, [archivedScopeAllTasks, assigneeLabelByValue, deletedScopeAllTasks, hierarchyTaskById, props.initiatives, props.tasks, props.workstreams, taskScope]);
|
|
894
926
|
const initiativeFilterOptions = useMemo(() => planningStructure.initiatives.map((initiative) => {
|
|
895
927
|
const referenceLabel = formatInitiativeReference(initiative);
|
|
896
928
|
return {
|
|
@@ -962,13 +994,6 @@ export function StandaloneLayout(props) {
|
|
|
962
994
|
return exactInitiative;
|
|
963
995
|
return resolveInitiativeReference(planningStructure.initiatives, planningDraftInitiativeId);
|
|
964
996
|
}, [planningDraftInitiativeId, planningStructure.initiativeById, planningStructure.initiatives]);
|
|
965
|
-
useEffect(() => {
|
|
966
|
-
if (!planningDrawerOpen)
|
|
967
|
-
return;
|
|
968
|
-
setPlanningTreeCollapsed(false);
|
|
969
|
-
setPlanningPrimaryCollapsed(false);
|
|
970
|
-
setPlanningSecondaryCollapsed(false);
|
|
971
|
-
}, [planningDrawerOpen]);
|
|
972
997
|
useEffect(() => {
|
|
973
998
|
if (selectedPlanningInitiativeId && !planningStructure.initiativeById.has(selectedPlanningInitiativeId)) {
|
|
974
999
|
setSelectedPlanningInitiativeId('');
|
|
@@ -976,23 +1001,7 @@ export function StandaloneLayout(props) {
|
|
|
976
1001
|
if (selectedPlanningWorkstreamId && !planningStructure.workstreamById.has(selectedPlanningWorkstreamId)) {
|
|
977
1002
|
setSelectedPlanningWorkstreamId('');
|
|
978
1003
|
}
|
|
979
|
-
if (planningDrawerDetail?.type === 'initiative' && !planningStructure.initiativeById.has(planningDrawerDetail.id)) {
|
|
980
|
-
setPlanningDrawerDetail(null);
|
|
981
|
-
}
|
|
982
|
-
if (planningDrawerDetail?.type === 'workstream' && !planningStructure.workstreamById.has(planningDrawerDetail.id)) {
|
|
983
|
-
setPlanningDrawerDetail(null);
|
|
984
|
-
}
|
|
985
|
-
if (planningNestedWorkstreamDetailId && !planningStructure.workstreamById.has(planningNestedWorkstreamDetailId)) {
|
|
986
|
-
setPlanningNestedWorkstreamDetailId(null);
|
|
987
|
-
}
|
|
988
|
-
if (planningSecondaryEditor?.mode === 'edit'
|
|
989
|
-
&& !planningStructure.workstreamById.has(planningSecondaryEditor.targetId)) {
|
|
990
|
-
setPlanningSecondaryEditor(null);
|
|
991
|
-
}
|
|
992
1004
|
}, [
|
|
993
|
-
planningDrawerDetail,
|
|
994
|
-
planningNestedWorkstreamDetailId,
|
|
995
|
-
planningSecondaryEditor,
|
|
996
1005
|
planningStructure.initiativeById,
|
|
997
1006
|
planningStructure.workstreamById,
|
|
998
1007
|
selectedPlanningInitiativeId,
|
|
@@ -1059,6 +1068,29 @@ export function StandaloneLayout(props) {
|
|
|
1059
1068
|
return !!task.dueDate && task.dueDate < todayDateOnly;
|
|
1060
1069
|
}).length;
|
|
1061
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]);
|
|
1062
1094
|
const expiredColumnCandidates = useMemo(() => {
|
|
1063
1095
|
if (groupBy !== 'schedule')
|
|
1064
1096
|
return [];
|
|
@@ -1168,7 +1200,10 @@ export function StandaloneLayout(props) {
|
|
|
1168
1200
|
const [showCreateWorkspaceConfirm, setShowCreateWorkspaceConfirm] = useState(false);
|
|
1169
1201
|
const [showTeamManagementModal, setShowTeamManagementModal] = useState(false);
|
|
1170
1202
|
const [teamManagementTab, setTeamManagementTab] = useState('members');
|
|
1203
|
+
const [teamWorkspaceRole, setTeamWorkspaceRole] = useState(null);
|
|
1171
1204
|
const [teamPlanMode, setTeamPlanMode] = useState('unknown');
|
|
1205
|
+
const [teamManagementAccessLoading, setTeamManagementAccessLoading] = useState(false);
|
|
1206
|
+
const [teamManagementAccessError, setTeamManagementAccessError] = useState(null);
|
|
1172
1207
|
const [teamMgmtError, setTeamMgmtError] = useState(null);
|
|
1173
1208
|
const [teamUsersLoading, setTeamUsersLoading] = useState(false);
|
|
1174
1209
|
const [teamUsers, setTeamUsers] = useState([]);
|
|
@@ -1191,11 +1226,20 @@ export function StandaloneLayout(props) {
|
|
|
1191
1226
|
const [billingNotice, setBillingNotice] = useState(null);
|
|
1192
1227
|
const [billingIntervalChoice, setBillingIntervalChoice] = useState('month');
|
|
1193
1228
|
const [profileDisplayNameDraft, setProfileDisplayNameDraft] = useState('');
|
|
1229
|
+
const [profileAvatarPreviewUrl, setProfileAvatarPreviewUrl] = useState('');
|
|
1230
|
+
const [profileAvatarDraftId, setProfileAvatarDraftId] = useState(null);
|
|
1231
|
+
const [profileAvatarBusy, setProfileAvatarBusy] = useState(false);
|
|
1194
1232
|
const [profileSaveBusy, setProfileSaveBusy] = useState(false);
|
|
1195
1233
|
const [profileSaveError, setProfileSaveError] = useState(null);
|
|
1196
1234
|
const [profileSaveNotice, setProfileSaveNotice] = useState(null);
|
|
1197
1235
|
const TEAM_AUDIT_PAGE_SIZE = 25;
|
|
1198
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
|
+
});
|
|
1199
1243
|
// showSearch state removed - search always visible in actions area
|
|
1200
1244
|
useEffect(() => {
|
|
1201
1245
|
if (!showAccountMenu)
|
|
@@ -1239,20 +1283,29 @@ export function StandaloneLayout(props) {
|
|
|
1239
1283
|
const fallbackId = String(currentWorkspaceId || '').trim();
|
|
1240
1284
|
return fallbackId || 'Workspace';
|
|
1241
1285
|
}, [availableWorkspaces, currentWorkspaceId, projectName, runtimeMode]);
|
|
1242
|
-
const canManageTeamWorkspace = computeCanManageTeamWorkspace(canAccessCloudTeamManagement,
|
|
1243
|
-
const canOpenTeamManagement = computeCanOpenTeamManagement(canAccessCloudTeamManagement,
|
|
1286
|
+
const canManageTeamWorkspace = computeCanManageTeamWorkspace(canAccessCloudTeamManagement, teamWorkspaceRole);
|
|
1287
|
+
const canOpenTeamManagement = computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode);
|
|
1244
1288
|
const authDisplayName = String(authUserDisplayName || '').trim();
|
|
1289
|
+
const authAvatarUrl = String(authUserAvatarUrl || '').trim();
|
|
1245
1290
|
const authEmailLabel = String(authUserEmail || '').trim();
|
|
1246
1291
|
const authIdentityLabel = authDisplayName || authEmailLabel;
|
|
1247
1292
|
const hasAuthIdentity = authIdentityLabel.length > 0;
|
|
1248
1293
|
const hasAuthEmail = authEmailLabel.length > 0;
|
|
1249
1294
|
const accountBadgeLabel = (authIdentityLabel || authEmailLabel || 'U').trim();
|
|
1250
1295
|
const accountBadgeInitial = accountBadgeLabel.charAt(0).toUpperCase() || 'U';
|
|
1296
|
+
const accountAvatarUrl = authAvatarUrl;
|
|
1297
|
+
const profileAvatarDisplayUrl = String(profileAvatarPreviewUrl || authAvatarUrl).trim();
|
|
1251
1298
|
const subscriptionLabel = String(billingStatus?.planName || billingStatus?.planId || '').trim();
|
|
1252
1299
|
const hasSubscription = subscriptionLabel.length > 0;
|
|
1253
1300
|
const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();
|
|
1254
1301
|
const runtimeLabel = runtimeMode === 'cloud' ? 'CLOUD' : 'LOCAL';
|
|
1255
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]);
|
|
1256
1309
|
const billingApiBaseUrl = useMemo(() => {
|
|
1257
1310
|
const preferredBase = runtimeMode === 'local'
|
|
1258
1311
|
? (props.config?.cloudAuthBaseUrl || props.config?.apiBaseUrl || '')
|
|
@@ -1261,10 +1314,10 @@ export function StandaloneLayout(props) {
|
|
|
1261
1314
|
}, [props.config?.apiBaseUrl, props.config?.cloudAuthBaseUrl, runtimeMode]);
|
|
1262
1315
|
const buildBillingApiUrl = useCallback((path) => {
|
|
1263
1316
|
const normalizedPath = String(path || '').trim();
|
|
1264
|
-
if (!
|
|
1317
|
+
if (!accountCapabilityApiBaseUrl)
|
|
1265
1318
|
return normalizedPath;
|
|
1266
|
-
return `${
|
|
1267
|
-
}, [
|
|
1319
|
+
return `${accountCapabilityApiBaseUrl}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`;
|
|
1320
|
+
}, [accountCapabilityApiBaseUrl]);
|
|
1268
1321
|
const connectedEnvironmentLabel = useMemo(() => resolveConnectedEnvironmentLabel(props.config?.cloudEnvironment, [
|
|
1269
1322
|
String(props.config?.cloudBaseUrl || ''),
|
|
1270
1323
|
String(props.config?.cloudAuthBaseUrl || ''),
|
|
@@ -1282,9 +1335,141 @@ export function StandaloneLayout(props) {
|
|
|
1282
1335
|
if (!showEditProfileModal)
|
|
1283
1336
|
return;
|
|
1284
1337
|
setProfileDisplayNameDraft(authDisplayName);
|
|
1338
|
+
setProfileAvatarPreviewUrl(authAvatarUrl);
|
|
1339
|
+
setProfileAvatarDraftId(null);
|
|
1340
|
+
setProfileSaveError(null);
|
|
1341
|
+
setProfileSaveNotice(null);
|
|
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);
|
|
1285
1365
|
setProfileSaveError(null);
|
|
1286
1366
|
setProfileSaveNotice(null);
|
|
1287
|
-
|
|
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]);
|
|
1288
1473
|
const handleSaveProfile = useCallback(async () => {
|
|
1289
1474
|
const trimmedDisplayName = profileDisplayNameDraft.trim();
|
|
1290
1475
|
if (!trimmedDisplayName) {
|
|
@@ -1295,53 +1480,111 @@ export function StandaloneLayout(props) {
|
|
|
1295
1480
|
setProfileSaveBusy(true);
|
|
1296
1481
|
setProfileSaveError(null);
|
|
1297
1482
|
setProfileSaveNotice(null);
|
|
1298
|
-
const result = await updateCurrentUserProfile({
|
|
1483
|
+
const result = await updateCurrentUserProfile({
|
|
1484
|
+
displayName: trimmedDisplayName,
|
|
1485
|
+
avatarDraftId: profileAvatarDraftId,
|
|
1486
|
+
clearAvatar: !profileAvatarDraftId && !profileAvatarPreviewUrl && Boolean(authAvatarUrl)
|
|
1487
|
+
});
|
|
1299
1488
|
if (!result.success) {
|
|
1300
1489
|
setProfileSaveError(result.error || 'Failed to update profile.');
|
|
1301
1490
|
setProfileSaveBusy(false);
|
|
1302
1491
|
return;
|
|
1303
1492
|
}
|
|
1304
1493
|
setProfileSaveNotice('Profile updated.');
|
|
1494
|
+
setProfileAvatarDraftId(null);
|
|
1495
|
+
setProfileAvatarPreviewUrl('');
|
|
1305
1496
|
setProfileSaveBusy(false);
|
|
1306
1497
|
setShowEditProfileModal(false);
|
|
1307
|
-
}, [profileDisplayNameDraft, updateCurrentUserProfile]);
|
|
1498
|
+
}, [authAvatarUrl, profileAvatarDraftId, profileAvatarPreviewUrl, profileDisplayNameDraft, updateCurrentUserProfile]);
|
|
1308
1499
|
const teamAdminHeaders = useCallback(() => {
|
|
1309
1500
|
const headers = {};
|
|
1310
1501
|
const workspaceId = String(teamManagementWorkspaceId || '').trim();
|
|
1311
1502
|
if (workspaceId)
|
|
1312
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
|
+
}
|
|
1313
1510
|
return headers;
|
|
1314
|
-
}, [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]);
|
|
1315
1551
|
const loadBillingStatus = useCallback(async () => {
|
|
1552
|
+
const billingStatusPath = '/api/taskforce/billing/status';
|
|
1553
|
+
const billingStatusUrl = buildBillingApiUrl(billingStatusPath);
|
|
1316
1554
|
if (!isAuthenticated) {
|
|
1317
1555
|
setBillingStatus(null);
|
|
1556
|
+
setBillingError(null);
|
|
1557
|
+
invalidateBillingStatusCache(billingStatusUrl);
|
|
1558
|
+
markAccountSurfaceLoadSettled('billing');
|
|
1318
1559
|
return null;
|
|
1319
1560
|
}
|
|
1561
|
+
const startedAt = getTimingNow();
|
|
1320
1562
|
setBillingLoading(true);
|
|
1321
1563
|
setBillingError(null);
|
|
1322
1564
|
try {
|
|
1323
|
-
const
|
|
1324
|
-
method: 'GET',
|
|
1325
|
-
credentials: 'include'
|
|
1326
|
-
});
|
|
1327
|
-
const payload = await res.json().catch(() => ({}));
|
|
1328
|
-
if (!res.ok) {
|
|
1329
|
-
setBillingStatus(null);
|
|
1330
|
-
setBillingError(String(payload?.error || 'Failed to load billing status.'));
|
|
1331
|
-
return null;
|
|
1332
|
-
}
|
|
1565
|
+
const payload = await loadBillingStatusCached(billingStatusUrl);
|
|
1333
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
|
+
});
|
|
1334
1572
|
return payload;
|
|
1335
1573
|
}
|
|
1336
|
-
catch {
|
|
1574
|
+
catch (error) {
|
|
1337
1575
|
setBillingStatus(null);
|
|
1338
|
-
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
|
+
});
|
|
1339
1581
|
return null;
|
|
1340
1582
|
}
|
|
1341
1583
|
finally {
|
|
1342
1584
|
setBillingLoading(false);
|
|
1585
|
+
markAccountSurfaceLoadSettled('billing');
|
|
1343
1586
|
}
|
|
1344
|
-
}, [buildBillingApiUrl, isAuthenticated]);
|
|
1587
|
+
}, [buildBillingApiUrl, isAuthenticated, markAccountSurfaceLoadSettled]);
|
|
1345
1588
|
const handleStartCheckout = useCallback(async () => {
|
|
1346
1589
|
const params = new URLSearchParams();
|
|
1347
1590
|
params.set('screen', 'plans');
|
|
@@ -1401,6 +1644,7 @@ export function StandaloneLayout(props) {
|
|
|
1401
1644
|
return;
|
|
1402
1645
|
}
|
|
1403
1646
|
setBillingNotice('Subscription updated.');
|
|
1647
|
+
invalidateBillingStatusCache(buildBillingApiUrl('/api/taskforce/billing/status'));
|
|
1404
1648
|
await loadBillingStatus();
|
|
1405
1649
|
}
|
|
1406
1650
|
catch {
|
|
@@ -1410,45 +1654,88 @@ export function StandaloneLayout(props) {
|
|
|
1410
1654
|
setBillingActionBusy(false);
|
|
1411
1655
|
}
|
|
1412
1656
|
}, [billingIntervalChoice, billingStatus?.planVersionId, buildBillingApiUrl, loadBillingStatus]);
|
|
1413
|
-
const
|
|
1414
|
-
if (!
|
|
1657
|
+
const loadTeamManagementAccess = useCallback(async () => {
|
|
1658
|
+
if (!canAccessCloudTeamManagement) {
|
|
1659
|
+
setTeamWorkspaceRole(null);
|
|
1415
1660
|
setTeamPlanMode('unknown');
|
|
1416
|
-
|
|
1417
|
-
|
|
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);
|
|
1418
1669
|
try {
|
|
1419
|
-
const res = await
|
|
1670
|
+
const res = await fetchTaskforceApi('/api/taskforce/account/team-management-access', {
|
|
1420
1671
|
method: 'GET',
|
|
1421
1672
|
credentials: 'include',
|
|
1422
1673
|
headers: teamAdminHeaders()
|
|
1423
|
-
});
|
|
1674
|
+
}, accountCapabilityApiBaseUrl);
|
|
1424
1675
|
const payload = await res.json().catch(() => ({}));
|
|
1425
1676
|
if (!res.ok) {
|
|
1677
|
+
setTeamWorkspaceRole(null);
|
|
1426
1678
|
setTeamPlanMode('unknown');
|
|
1427
|
-
|
|
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 };
|
|
1428
1685
|
}
|
|
1429
|
-
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);
|
|
1430
1694
|
setTeamPlanMode(nextMode);
|
|
1431
|
-
|
|
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 };
|
|
1432
1703
|
}
|
|
1433
|
-
catch {
|
|
1704
|
+
catch (error) {
|
|
1705
|
+
setTeamWorkspaceRole(null);
|
|
1434
1706
|
setTeamPlanMode('unknown');
|
|
1435
|
-
|
|
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 };
|
|
1436
1713
|
}
|
|
1437
|
-
|
|
1714
|
+
finally {
|
|
1715
|
+
setTeamManagementAccessLoading(false);
|
|
1716
|
+
markAccountSurfaceLoadSettled('teamManagement');
|
|
1717
|
+
}
|
|
1718
|
+
}, [accountCapabilityApiBaseUrl, canAccessCloudTeamManagement, markAccountSurfaceLoadSettled, teamAdminHeaders]);
|
|
1438
1719
|
useEffect(() => {
|
|
1439
1720
|
if (!showAccountMenu && !showTeamManagementModal && !showAccountHub)
|
|
1440
1721
|
return;
|
|
1441
|
-
void
|
|
1442
|
-
}, [showAccountHub, showAccountMenu, showTeamManagementModal,
|
|
1722
|
+
void loadTeamManagementAccess();
|
|
1723
|
+
}, [showAccountHub, showAccountMenu, showTeamManagementModal, loadTeamManagementAccess]);
|
|
1443
1724
|
useEffect(() => {
|
|
1444
1725
|
if (!isAuthenticated || !authSessionResolved)
|
|
1445
1726
|
return;
|
|
1446
|
-
if (!
|
|
1727
|
+
if (!canAccessCloudTeamManagement) {
|
|
1728
|
+
setTeamWorkspaceRole(null);
|
|
1447
1729
|
setTeamPlanMode('unknown');
|
|
1448
1730
|
return;
|
|
1449
1731
|
}
|
|
1450
|
-
void
|
|
1451
|
-
}, [authSessionResolved,
|
|
1732
|
+
void loadTeamManagementAccess();
|
|
1733
|
+
}, [authSessionResolved, canAccessCloudTeamManagement, isAuthenticated, loadTeamManagementAccess]);
|
|
1734
|
+
useEffect(() => {
|
|
1735
|
+
if (!isAuthenticated || !authSessionResolved)
|
|
1736
|
+
return;
|
|
1737
|
+
void loadBillingStatus();
|
|
1738
|
+
}, [authSessionResolved, isAuthenticated, loadBillingStatus]);
|
|
1452
1739
|
useEffect(() => {
|
|
1453
1740
|
if (!showAccountHub && !showAccountMenu)
|
|
1454
1741
|
return;
|
|
@@ -1582,12 +1869,12 @@ export function StandaloneLayout(props) {
|
|
|
1582
1869
|
setShowAccountMenu(false);
|
|
1583
1870
|
setShowTeamManagementModal(true);
|
|
1584
1871
|
setTeamManagementTab('members');
|
|
1585
|
-
const
|
|
1586
|
-
if (
|
|
1872
|
+
const access = await loadTeamManagementAccess();
|
|
1873
|
+
if (!access.allowed)
|
|
1587
1874
|
return;
|
|
1588
1875
|
await loadTeamUsers();
|
|
1589
1876
|
await loadTeamAuditPage(0);
|
|
1590
|
-
}, [loadTeamAuditPage,
|
|
1877
|
+
}, [loadTeamAuditPage, loadTeamManagementAccess, loadTeamUsers]);
|
|
1591
1878
|
const executeTeamUserAction = useCallback(async (userId, fn) => {
|
|
1592
1879
|
setTeamMgmtError(null);
|
|
1593
1880
|
setTeamActionBusyUserId(userId);
|
|
@@ -1891,12 +2178,17 @@ export function StandaloneLayout(props) {
|
|
|
1891
2178
|
const activeReferenceMismatchSummaries = useMemo(() => (activeReferenceMismatchEvents.map((event, index) => {
|
|
1892
2179
|
const details = event.details && typeof event.details === 'object' ? event.details : null;
|
|
1893
2180
|
const pathValue = String(details?.path || '').trim();
|
|
2181
|
+
const referenceLabelValue = String(details?.referenceLabel || '').trim();
|
|
2182
|
+
const taskTitleValue = String(details?.taskTitle || details?.existingTaskTitle || '').trim();
|
|
1894
2183
|
const existingRefValue = Number(details?.existingReferenceNumber);
|
|
1895
2184
|
const incomingRefValue = Number(details?.incomingReferenceNumber);
|
|
1896
|
-
const pathLabel = pathValue || `Mismatch ${index + 1}`;
|
|
1897
|
-
const refsLabel = (Number.isFinite(existingRefValue)
|
|
2185
|
+
const pathLabel = pathValue || taskTitleValue || referenceLabelValue || `Mismatch ${index + 1}`;
|
|
2186
|
+
const refsLabel = (Number.isFinite(existingRefValue)
|
|
2187
|
+
|| Number.isFinite(incomingRefValue))
|
|
1898
2188
|
? `Existing ${Number.isFinite(existingRefValue) ? existingRefValue : '?'} vs incoming ${Number.isFinite(incomingRefValue) ? incomingRefValue : '?'}`
|
|
1899
|
-
:
|
|
2189
|
+
: referenceLabelValue
|
|
2190
|
+
? `Both claimed ${referenceLabelValue}`
|
|
2191
|
+
: null;
|
|
1900
2192
|
return {
|
|
1901
2193
|
key: getSyncEventStableKey(event, index),
|
|
1902
2194
|
pathLabel,
|
|
@@ -2255,20 +2547,32 @@ export function StandaloneLayout(props) {
|
|
|
2255
2547
|
// References: isAuthenticated, authIdentityLabel, billingStatus, runtimeLabel,
|
|
2256
2548
|
// connectedEnvironmentLabel, canManageCloudWorkspaces, workspaceActionBusy,
|
|
2257
2549
|
// setWorkspacePanelOpen, canOpenTeamManagement, openTeamManagementModal, and more.
|
|
2258
|
-
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
|
|
2259
2563
|
? 'Checking sign-in status...'
|
|
2260
2564
|
: isAuthenticated
|
|
2261
2565
|
? (_jsx(_Fragment, { children: hasAuthIdentity
|
|
2262
|
-
? (_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: () => {
|
|
2263
2567
|
setShowAccountMenu(false);
|
|
2264
2568
|
openPlansScreen();
|
|
2265
|
-
}, 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 })] })] }) }))
|
|
2266
2570
|
: 'Signed in' }))
|
|
2267
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: () => {
|
|
2268
2572
|
setWorkspaceActionError('');
|
|
2269
2573
|
setShowCreateWorkspaceConfirm(true);
|
|
2270
2574
|
setShowAccountMenu(false);
|
|
2271
|
-
}, 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: () => {
|
|
2272
2576
|
setShowAccountMenu(false);
|
|
2273
2577
|
setShowEditProfileModal(true);
|
|
2274
2578
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Edit Profile"] })), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
@@ -2276,6 +2580,7 @@ export function StandaloneLayout(props) {
|
|
|
2276
2580
|
openPlansScreen();
|
|
2277
2581
|
}, role: "menuitem", children: [_jsx(Layers, { size: 15 }), "Plans"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2278
2582
|
setShowAccountMenu(false);
|
|
2583
|
+
startAccountSurfaceTiming('account_hub');
|
|
2279
2584
|
setShowAccountHub(true);
|
|
2280
2585
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2281
2586
|
setShowAccountMenu(false);
|
|
@@ -2908,7 +3213,7 @@ export function StandaloneLayout(props) {
|
|
|
2908
3213
|
const workspacePrimaryAction = workspaceChromeDefinition.primaryAction
|
|
2909
3214
|
? primaryActionRegistry[workspaceChromeDefinition.primaryAction]
|
|
2910
3215
|
: null;
|
|
2911
|
-
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: {
|
|
2912
3217
|
marginLeft: '6px',
|
|
2913
3218
|
height: '24px',
|
|
2914
3219
|
width: '24px',
|
|
@@ -3041,114 +3346,71 @@ export function StandaloneLayout(props) {
|
|
|
3041
3346
|
return;
|
|
3042
3347
|
}
|
|
3043
3348
|
void handleUnarchive(taskId);
|
|
3044
|
-
}, 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(
|
|
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
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
}));
|
|
3110
|
-
}, children: "Revoke Invite" })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
|
|
3111
|
-
void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}?workspaceId=${encodeURIComponent(teamManagementWorkspaceId)}`, {
|
|
3112
|
-
method: 'DELETE',
|
|
3113
|
-
headers: teamAdminHeaders(),
|
|
3114
|
-
credentials: 'include'
|
|
3115
|
-
}));
|
|
3116
|
-
}, 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: () => {
|
|
3117
|
-
setShowSyncStatusModal(false);
|
|
3118
|
-
}, 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: {
|
|
3119
|
-
borderColor: syncStatusMeta.border,
|
|
3120
|
-
background: syncStatusMeta.background,
|
|
3121
|
-
color: syncStatusMeta.color
|
|
3122
|
-
}, children: syncStatusMeta.label }), _jsxs("span", { className: syncStatusStyles.syncStatusLabel, children: ["Workspace: ", _jsx("code", { children: currentWorkspaceLabel })] })] }) }), _jsx("label", { className: syncStatusStyles.syncToggle, children: _jsxs("span", { className: [
|
|
3123
|
-
syncStatusStyles.syncToggleControl,
|
|
3124
|
-
workspaceCloudSyncEnabled ? syncStatusStyles.syncToggleControlEnabled : syncStatusStyles.syncToggleControlDisabled,
|
|
3125
|
-
syncControlBusy ? syncStatusStyles.syncToggleControlBusy : '',
|
|
3126
|
-
!canManageWorkspaceSync ? syncStatusStyles.syncToggleControlBlocked : ''
|
|
3127
|
-
].filter(Boolean).join(' '), children: [_jsx("input", { className: syncStatusStyles.syncToggleInput, type: "checkbox", role: "switch", "aria-label": "Enable Sync", checked: workspaceCloudSyncEnabled, disabled: !canManageWorkspaceSync || syncControlBusy, onChange: (event) => {
|
|
3128
|
-
void handleWorkspaceSyncToggle(event.target.checked);
|
|
3129
|
-
} }), _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
|
|
3130
|
-
? `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.`
|
|
3131
|
-
: '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: {
|
|
3132
|
-
display: 'flex',
|
|
3133
|
-
flexDirection: 'column',
|
|
3134
|
-
gap: '2px',
|
|
3135
|
-
padding: '8px 10px',
|
|
3136
|
-
borderRadius: '8px',
|
|
3137
|
-
background: 'rgba(15, 23, 42, 0.18)',
|
|
3138
|
-
border: '1px solid rgba(250, 204, 21, 0.16)'
|
|
3139
|
-
}, 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: [
|
|
3140
|
-
syncStatusStyles.syncStatusEventItem,
|
|
3141
|
-
row.tone === 'error' ? syncStatusStyles.syncStatusEventItemError : '',
|
|
3142
|
-
row.tone === 'muted' ? syncStatusStyles.syncStatusEventItemMuted : ''
|
|
3143
|
-
].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
|
|
3144
|
-
? 'Repair is running now.'
|
|
3145
|
-
: workspaceSyncRepairQueued
|
|
3146
|
-
? 'Repair is queued and will start when the current sync finishes.'
|
|
3147
|
-
: workspaceSyncBusy
|
|
3148
|
-
? 'Queue a repair to start automatically when the current sync finishes.'
|
|
3149
|
-
: '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: () => {
|
|
3150
|
-
openSharedLoginRoute('login');
|
|
3151
|
-
}, 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: () => {
|
|
3152
3414
|
const parts = currentBrowsePath.split('/').filter(Boolean);
|
|
3153
3415
|
parts.pop();
|
|
3154
3416
|
fetchFolders(parts.length ? parts.join('/') + '/' : '');
|