@taskforcehq/taskforce 0.3.306 → 0.3.307
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.js +9 -1
- package/dist/components/views/PlanComparisonPage.test.js +19 -0
- package/dist/components/views/PlansPage.js +1 -1
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +12 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +190 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +775 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +21 -0
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +187 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +4 -0
- package/dist/server/routes/authSupport.js +12 -1
- package/dist/server/routes/billing.js +11 -0
- package/dist/server/routes/billing.test.js +3 -0
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +890 -22
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -32,11 +32,11 @@ describe('TaskContextUpload drag and drop', () => {
|
|
|
32
32
|
const renderUpload = (contextFiles = [], taskId, extraProps) => render(_jsx(TaskContextUpload, { taskId: taskId, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption, ...extraProps }));
|
|
33
33
|
const openSection = async (user) => {
|
|
34
34
|
const localUser = user ?? userEvent.setup();
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (!screen.queryByText('Drop files or URLs here')) {
|
|
36
|
+
const showToggle = await screen.findByTitle('Show context documents');
|
|
37
37
|
await localUser.click(showToggle);
|
|
38
38
|
}
|
|
39
|
-
return screen.
|
|
39
|
+
return screen.findByText('Drop files or URLs here');
|
|
40
40
|
};
|
|
41
41
|
beforeEach(() => {
|
|
42
42
|
vi.clearAllMocks();
|
|
@@ -14,12 +14,21 @@ import { Markdown } from "../ui/Markdown";
|
|
|
14
14
|
import { IconMap, resolveColor } from "../../utils/constants";
|
|
15
15
|
import { formatDate } from "../../utils/formatting";
|
|
16
16
|
import { t } from "../../localization";
|
|
17
|
-
import {
|
|
18
|
-
import { getTaskActivityItems, getTaskEventPrimaryChangeField, summarizeTaskChange, summarizeTaskEvent, } from "../../utils/taskActivity";
|
|
17
|
+
import { getTaskActivityItems } from "../../utils/taskActivity";
|
|
19
18
|
import { ensureAssigneeOptionPresent, getAssigneeKind, getAssigneeLabel, } from "../../utils/assignees";
|
|
20
19
|
import { getTaskReferenceLabel } from "../../utils/taskReferences";
|
|
21
20
|
import { getVisibleTaskFormTaxonomies } from "../../utils/taxonomyFormDefaults.js";
|
|
22
21
|
import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel, } from "../../utils/planningReferences";
|
|
22
|
+
const DeferredEntityActivityTimeline = React.lazy(() => (import("../activity/EntityActivityTimeline").then((module) => ({
|
|
23
|
+
default: module.EntityActivityTimeline,
|
|
24
|
+
}))));
|
|
25
|
+
const DeferredTaskContextUpload = React.lazy(() => (import("./TaskContextUpload").then((module) => ({
|
|
26
|
+
default: module.TaskContextUpload,
|
|
27
|
+
}))));
|
|
28
|
+
function ActivityTimelineFallback({ count }) {
|
|
29
|
+
const placeholderCount = Math.max(2, Math.min(count || 3, 4));
|
|
30
|
+
return (_jsx("div", { className: `${formStyles.commentThread} ${formStyles.activityLoadingThread}`.trim(), "aria-busy": "true", "aria-live": "polite", "aria-label": "Loading activity", children: Array.from({ length: placeholderCount }, (_, index) => (_jsxs("div", { className: formStyles.activitySkeletonItem, children: [_jsx("div", { className: formStyles.activitySkeletonMeta }), _jsx("div", { className: formStyles.activitySkeletonBody })] }, index))) }));
|
|
31
|
+
}
|
|
23
32
|
function SortableChecklistRow({ id, item, onToggle, onRemove, }) {
|
|
24
33
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id });
|
|
25
34
|
const style = {
|
|
@@ -31,12 +40,6 @@ function SortableChecklistRow({ id, item, onToggle, onRemove, }) {
|
|
|
31
40
|
? "Mark checklist item incomplete"
|
|
32
41
|
: "Mark checklist item complete", children: item.isCompleted ? _jsx(Check, { size: 14, strokeWidth: 2.1 }) : null }), _jsx("span", { className: `${formStyles.checklistItemText} ${item.isCompleted ? formStyles.checklistItemTextCompleted : ""}`.trim(), children: item.title }), _jsx("button", { type: "button", className: formStyles.checklistRemoveBtn, onClick: onRemove, title: "Remove checklist item", "aria-label": "Remove checklist item", children: _jsx(X, { size: 15, strokeWidth: 1.9 }) })] }));
|
|
33
42
|
}
|
|
34
|
-
function hexToRgba(hex, opacity) {
|
|
35
|
-
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
36
|
-
if (!result)
|
|
37
|
-
return "";
|
|
38
|
-
return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})`;
|
|
39
|
-
}
|
|
40
43
|
export function TaskForm(props) {
|
|
41
44
|
const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = "", checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
|
|
42
45
|
const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask);
|
|
@@ -381,65 +384,12 @@ export function TaskForm(props) {
|
|
|
381
384
|
const frame = window.requestAnimationFrame(scrollToLatest);
|
|
382
385
|
return () => window.cancelAnimationFrame(frame);
|
|
383
386
|
}, [activityCount, commentsEndRef, currentTask?.id, editingTaskId]);
|
|
384
|
-
const activitySidebar = editingTaskId ? (_jsxs("aside", { className: formStyles.activitySidebar, "aria-label": t("taskForm.commentsSectionTitle"), children: [_jsxs("div", { className: formStyles.activitySidebarHeader, children: [_jsx("h2", { className: formStyles.activitySidebarTitle, children: t("taskForm.commentsSectionTitle") }), _jsx("span", { className: formStyles.activitySidebarCount, children: activityCount })] }), _jsxs("div", { className: `${formStyles.commentPanel} ${formStyles.activitySidebarPanel}`.trim(), children: [_jsxs("div", { className: formStyles.
|
|
385
|
-
if (item.type === "comment") {
|
|
386
|
-
const c = item.comment;
|
|
387
|
-
const actorPresentation = resolveActivityActorPresentation({
|
|
388
|
-
actorId: c.author,
|
|
389
|
-
actorType: c.actor?.kind === "ai"
|
|
390
|
-
? "ai"
|
|
391
|
-
: c.actor?.kind === "human"
|
|
392
|
-
? "human"
|
|
393
|
-
: null,
|
|
394
|
-
actorProfile: c.actor,
|
|
395
|
-
});
|
|
396
|
-
const commentKind = actorPresentation.kind === "agent" ? "agent" : "member";
|
|
397
|
-
const { ActorIcon: CommentActorIcon } = actorPresentation;
|
|
398
|
-
const authorColor = actorPresentation.color;
|
|
399
|
-
const authorLabel = actorPresentation.label;
|
|
400
|
-
const commentAccentStyle = authorColor && hexToRgba(authorColor, 0.35)
|
|
401
|
-
? {
|
|
402
|
-
background: `linear-gradient(135deg, ${hexToRgba(authorColor, 0.18)} 0%, ${hexToRgba(authorColor, 0.1)} 100%)`,
|
|
403
|
-
borderColor: hexToRgba(authorColor, 0.35),
|
|
404
|
-
}
|
|
405
|
-
: undefined;
|
|
406
|
-
return (_jsxs("div", { className: `${formStyles.comment} ${commentKind === "agent" ? formStyles.commentAi : formStyles.commentUser}`, children: [_jsxs("div", { className: formStyles.commentHeader, children: [_jsx("span", { className: formStyles.commentAuthor, style: authorColor ? { color: authorColor } : undefined, children: _jsxs(_Fragment, { children: [_jsx(CommentActorIcon, { size: 12, strokeWidth: 1.5 }), " ", authorLabel] }) }), _jsx("span", { className: formStyles.commentTime, children: formatDate(c.timestamp, {
|
|
407
|
-
month: "short",
|
|
408
|
-
day: "numeric",
|
|
409
|
-
hour: "2-digit",
|
|
410
|
-
minute: "2-digit",
|
|
411
|
-
}) })] }), _jsx("div", { className: formStyles.commentText, style: commentAccentStyle, children: _jsx(Markdown, { onTaskIdClick: onOpenTaskById, children: c.text }) })] }, c.id));
|
|
412
|
-
}
|
|
413
|
-
const event = item.event;
|
|
414
|
-
const actorPresentation = resolveActivityActorPresentation({
|
|
415
|
-
actorId: event.actor,
|
|
416
|
-
actorType: event.actorType,
|
|
417
|
-
actorProfile: event.actorProfile,
|
|
418
|
-
fallbackKind: "system",
|
|
419
|
-
});
|
|
420
|
-
const { ActorIcon: EventActorIcon } = actorPresentation;
|
|
421
|
-
const authorColor = actorPresentation.color;
|
|
422
|
-
const authorLabel = actorPresentation.label;
|
|
423
|
-
const eventKind = actorPresentation.kind;
|
|
424
|
-
const changeEntries = Object.entries(event.details?.changes || {});
|
|
425
|
-
const primaryChangeField = getTaskEventPrimaryChangeField(event);
|
|
426
|
-
const displayedChangeEntries = changeEntries.length > 1 && primaryChangeField
|
|
427
|
-
? changeEntries.filter(([field]) => field !== primaryChangeField)
|
|
428
|
-
: changeEntries.length > 1
|
|
429
|
-
? changeEntries
|
|
430
|
-
: [];
|
|
431
|
-
return (_jsxs("div", { className: `${formStyles.comment} ${formStyles.activityEvent} ${eventKind === "system" ? formStyles.activityEventSystem : ""}`, children: [_jsxs("div", { className: `${formStyles.commentHeader} ${formStyles.activityEventHeader}`, children: [_jsx("span", { className: formStyles.commentAuthor, style: authorColor ? { color: authorColor } : undefined, children: _jsxs(_Fragment, { children: [_jsx(EventActorIcon, { size: 12, strokeWidth: 1.5 }), " ", authorLabel] }) }), _jsx("span", { className: formStyles.commentTime, children: formatDate(event.createdAt, {
|
|
432
|
-
month: "short",
|
|
433
|
-
day: "numeric",
|
|
434
|
-
hour: "2-digit",
|
|
435
|
-
minute: "2-digit",
|
|
436
|
-
}) })] }), _jsxs("div", { className: formStyles.activityEventText, children: [_jsx("div", { children: summarizeTaskEvent(event, formatActivityFieldValue) }), displayedChangeEntries.length > 0 && (_jsx("div", { className: formStyles.activityEventChanges, children: displayedChangeEntries.map(([field, change]) => (_jsx("div", { className: formStyles.activityEventChange, children: summarizeTaskChange(field, change, formatActivityFieldValue) }, field))) }))] })] }, item.id));
|
|
437
|
-
}), _jsx("div", { ref: commentsEndRef })] }), _jsxs("div", { className: formStyles.commentInputArea, children: [_jsx("textarea", { className: formStyles.commentInput, placeholder: t("taskForm.commentPlaceholder"), value: newCommentText, onChange: (e) => onNewCommentTextChange(e.target.value), onKeyDown: (e) => {
|
|
387
|
+
const activitySidebar = editingTaskId ? (_jsxs("aside", { className: formStyles.activitySidebar, "aria-label": t("taskForm.commentsSectionTitle"), children: [_jsxs("div", { className: formStyles.activitySidebarHeader, children: [_jsx("h2", { className: formStyles.activitySidebarTitle, children: t("taskForm.commentsSectionTitle") }), _jsx("span", { className: formStyles.activitySidebarCount, children: activityCount })] }), _jsxs("div", { className: `${formStyles.commentPanel} ${formStyles.activitySidebarPanel}`.trim(), children: [_jsx(React.Suspense, { fallback: _jsx(ActivityTimelineFallback, { count: activityCount }), children: _jsx(DeferredEntityActivityTimeline, { activity: currentTask?.activity, comments: comments, formatFieldValue: formatActivityFieldValue, resolveActorPresentation: resolveActivityActorPresentation, onOpenTaskById: onOpenTaskById, emptyMessage: t("taskForm.noComments"), listClassName: formStyles.commentThread, emptyClassName: formStyles.emptyComments, endRef: commentsEndRef }) }), _jsxs("div", { className: formStyles.commentInputArea, children: [_jsx("textarea", { className: formStyles.commentInput, placeholder: t("taskForm.commentPlaceholder"), value: newCommentText, onChange: (e) => onNewCommentTextChange(e.target.value), onKeyDown: (e) => {
|
|
438
388
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
439
389
|
e.preventDefault();
|
|
440
390
|
onAddComment();
|
|
441
391
|
}
|
|
442
|
-
}, rows:
|
|
392
|
+
}, rows: 4 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), "aria-label": "Send comment", title: "Send comment", children: _jsx(Send, { size: 16 }) })] })] })] })) : null;
|
|
443
393
|
return (_jsxs("form", { ref: formRef, onSubmit: onSubmit, className: `${styles.form} ${styles.appScrollbar} tf-scrollbar`, children: [error && _jsx("div", { className: formStyles.error, children: error }), isReadOnly && (_jsx("div", { className: `${formStyles.formNotice} tf-text-helper`, children: isDeletedReadOnly
|
|
444
394
|
? "Deleted tasks are read-only. Restore the task to make changes."
|
|
445
395
|
: "Archived tasks are read-only. Unarchive the task to make changes." })), _jsxs("fieldset", { disabled: isReadOnly, className: `${formStyles.formFieldsetReset} ${editingTaskId ? formStyles.formLayoutWithSidebar : formStyles.formLayout}`.trim(), children: [_jsxs("div", { className: formStyles.formMainColumn, children: [_jsxs("div", { className: styles.field, children: [_jsxs("div", { className: styles.labelRow, children: [_jsx("label", { className: styles.label, children: "Title" }), assignee && (_jsx("span", { title: `Assigned to: ${currentAssigneeLabel}`, className: [
|
|
@@ -560,7 +510,7 @@ export function TaskForm(props) {
|
|
|
560
510
|
.map((entry, entryIndex) => ({
|
|
561
511
|
...entry,
|
|
562
512
|
order: entryIndex,
|
|
563
|
-
}))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [_jsx(
|
|
513
|
+
}))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [_jsx(React.Suspense, { fallback: null, children: _jsx(DeferredTaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption }) }), _jsx("div", { className: `${formStyles.taskFormLifecycle} ${formStyles.taskFormMetaDivider}`, children: _jsx("div", { className: formStyles.taskFormMetaGrid, children: currentTask && (_jsxs(_Fragment, { children: [_jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t("taskForm.createdLabel") }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: createdLabel || t("taskForm.emptyValue") }), _jsx("span", { className: formStyles.taskFormMetaActor, children: createdByLabel
|
|
564
514
|
? `by ${createdByLabel}`
|
|
565
515
|
: t("taskForm.emptyValue") })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t("taskForm.updatedLabel") }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: updatedLabel || t("taskForm.emptyValue") }), _jsx("span", { className: formStyles.taskFormMetaActor, children: updatedByLabel
|
|
566
516
|
? `by ${updatedByLabel}`
|
|
@@ -520,20 +520,12 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
|
|
|
520
520
|
void onAssignTaskToWorkstream?.(activeTaskDataId, overWorkstreamDataId);
|
|
521
521
|
return;
|
|
522
522
|
}
|
|
523
|
-
if (overType === 'planning-task-unlink' && activeTaskDataId) {
|
|
524
|
-
void onAssignTaskToWorkstream?.(activeTaskDataId, null);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
523
|
}
|
|
528
524
|
if (activeType === 'planning-workstream') {
|
|
529
525
|
if (overType === 'planning-initiative-target' && activeWorkstreamDataId && overInitiativeDataId) {
|
|
530
526
|
void onAssignWorkstreamToInitiative?.(activeWorkstreamDataId, overInitiativeDataId);
|
|
531
527
|
return;
|
|
532
528
|
}
|
|
533
|
-
if (overType === 'planning-workstream-unlink' && activeWorkstreamDataId) {
|
|
534
|
-
void onAssignWorkstreamToInitiative?.(activeWorkstreamDataId, null);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
529
|
return;
|
|
538
530
|
}
|
|
539
531
|
const activeId = String(active.id);
|
|
@@ -11,6 +11,7 @@ import { getVisibleTaxonomiesForTasks } from '../../utils/taxonomyLifecycle.js';
|
|
|
11
11
|
import { TaskCard } from './TaskCard';
|
|
12
12
|
import { t } from '../../localization';
|
|
13
13
|
import { DeleteAllDeletedAction, TaskScopeToggle } from '../views/panels/FilterToolbarControls';
|
|
14
|
+
import { normalizeTaskReferenceToken } from '../../utils/taskSearch';
|
|
14
15
|
export const TaskList = forwardRef((props, ref) => {
|
|
15
16
|
const { tasks, archivedTasks, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions = [], workstreams = [], initiatives = [], searchQuery, filterCategories, filterTypes, filterPriorities, filterStatus, filterAssignees = [], filterTaxonomies, sortBy, sortOrder, showArchive, taskScope, collapsedCategories, loadingTasks, filteredTasks, filteredArchive, groupedTasks, filteredDeletedTasks = [], groupedDeletedTasks = {}, copiedId, recentlyChangedTaskIds = [], showTaskCardStatusLabel = true, onSearchChange, onFilterCategoriesChange, onFilterTypesChange, onFilterPrioritiesChange, onFilterStatusChange, onFilterAssigneesChange, onTaxonomyFilterChange, onSortByChange, onSortOrderChange, onShowArchiveChange, onTaskScopeChange, onClearFilters, onToggleCategory, onEditTask, onOpenTaskById, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onBulkArchive, onUnarchive, onDelete, onDeleteAllDeleted, onFetchArchive, onAddTaskToCategory, supplementalTasks = [], taxonomies, } = props;
|
|
16
17
|
const changedTaskIdSet = new Set(recentlyChangedTaskIds);
|
|
@@ -46,8 +47,11 @@ export const TaskList = forwardRef((props, ref) => {
|
|
|
46
47
|
: effectiveTaskScope === 'archived'
|
|
47
48
|
? 'archived'
|
|
48
49
|
: null;
|
|
50
|
+
const searchedTaskReference = normalizeTaskReferenceToken(searchQuery);
|
|
49
51
|
const emptyLabel = searchQuery
|
|
50
|
-
?
|
|
52
|
+
? searchedTaskReference
|
|
53
|
+
? `No task found for ${searchedTaskReference.toUpperCase()}`
|
|
54
|
+
: t('taskList.noMatchingTasks')
|
|
51
55
|
: effectiveTaskScope === 'archived'
|
|
52
56
|
? 'No archived tasks found.'
|
|
53
57
|
: effectiveTaskScope === 'deleted'
|
|
@@ -390,6 +390,10 @@ describe('TaskList Component', () => {
|
|
|
390
390
|
renderComponent({ filteredTasks: [], loadingTasks: false });
|
|
391
391
|
expect(screen.getByText('No active tasks')).toBeInTheDocument();
|
|
392
392
|
});
|
|
393
|
+
it('shows a task-reference-specific empty state for unmatched searches', () => {
|
|
394
|
+
renderComponent({ filteredTasks: [], loadingTasks: false, searchQuery: ' T - 123 ' });
|
|
395
|
+
expect(screen.getByText('No task found for T-123')).toBeInTheDocument();
|
|
396
|
+
});
|
|
393
397
|
it('2.13 Loading state', () => {
|
|
394
398
|
renderComponent({ loadingTasks: true });
|
|
395
399
|
expect(screen.queryByText('No active tasks')).not.toBeInTheDocument();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface HierarchyActionBadgeProps {
|
|
3
|
+
ariaLabel: string;
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function HierarchyActionBadge({ ariaLabel, children, disabled, onClick, title, }: HierarchyActionBadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import styles from '../../Taskforce.module.css';
|
|
3
|
+
export function HierarchyActionBadge({ ariaLabel, children, disabled = false, onClick, title, }) {
|
|
4
|
+
return (_jsx("button", { type: "button", className: `${styles.taskIdBadge} ${styles.taskHierarchyActionBtn}`, onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: children }));
|
|
5
|
+
}
|
|
@@ -4,10 +4,12 @@ interface AppShellHeaderProps {
|
|
|
4
4
|
projectName?: string | null;
|
|
5
5
|
currentWorkspaceId?: string | null;
|
|
6
6
|
brandLabel?: string;
|
|
7
|
+
brandHomeLabel?: string;
|
|
8
|
+
onBrandClick?: () => void;
|
|
7
9
|
runtimeMode?: 'local' | 'cloud';
|
|
8
10
|
theme?: TaskforceTheme;
|
|
9
11
|
meta?: React.ReactNode;
|
|
10
12
|
actions?: React.ReactNode;
|
|
11
13
|
}
|
|
12
|
-
export declare function AppShellHeader({ projectName, currentWorkspaceId, brandLabel, runtimeMode, theme, meta, actions }: AppShellHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function AppShellHeader({ projectName, currentWorkspaceId, brandLabel, brandHomeLabel, onBrandClick, runtimeMode, theme, meta, actions }: AppShellHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
13
15
|
export {};
|
|
@@ -5,19 +5,35 @@ import logoLight from '../../assets/branding/logos/logo_light.svg';
|
|
|
5
5
|
import logoDark from '../../assets/branding/logos/logo_dark.svg';
|
|
6
6
|
import favicon from '../../assets/favicon/favicon.svg';
|
|
7
7
|
import { DEFAULT_TASKFORCE_THEME, isLightThemeFamily } from '../../utils/theme';
|
|
8
|
-
export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = 'TaskForce', runtimeMode = 'local', theme = DEFAULT_TASKFORCE_THEME, meta, actions }) {
|
|
8
|
+
export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = 'TaskForce', brandHomeLabel = 'Go to Kanban', onBrandClick, runtimeMode = 'local', theme = DEFAULT_TASKFORCE_THEME, meta, actions }) {
|
|
9
9
|
const logo = isLightThemeFamily(theme) ? logoDark : logoLight;
|
|
10
10
|
const normalizedProjectName = String(projectName || '').trim();
|
|
11
|
-
const
|
|
11
|
+
const workspaceIdForCopy = String(currentWorkspaceId || '').trim();
|
|
12
|
+
const normalizedWorkspaceId = workspaceIdForCopy.toLowerCase();
|
|
12
13
|
const showProjectName = Boolean(normalizedProjectName
|
|
13
14
|
&& (runtimeMode !== 'cloud' || (normalizedWorkspaceId && normalizedWorkspaceId !== 'default')));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const copyWorkspaceId = () => {
|
|
16
|
+
if (!workspaceIdForCopy || typeof navigator === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
const clipboard = navigator.clipboard;
|
|
19
|
+
if (!clipboard?.writeText)
|
|
20
|
+
return;
|
|
21
|
+
void clipboard.writeText(workspaceIdForCopy).catch(() => undefined);
|
|
22
|
+
};
|
|
23
|
+
const handleProjectNameKeyDown = (event) => {
|
|
24
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
25
|
+
return;
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
copyWorkspaceId();
|
|
28
|
+
};
|
|
29
|
+
const brandContent = (_jsxs(_Fragment, { children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (event) => {
|
|
30
|
+
const target = event.currentTarget;
|
|
31
|
+
if (target.src !== favicon) {
|
|
32
|
+
target.src = favicon;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
target.style.display = 'none';
|
|
36
|
+
}
|
|
37
|
+
} }), _jsx("span", { children: brandLabel }), runtimeMode === 'cloud' && (_jsx("span", { className: styles.brandCloudSuffix, children: "HQ" }))] }));
|
|
38
|
+
return (_jsxs("div", { className: `${styles.header} ${shellStyles.standaloneHeader}`, children: [_jsxs("div", { className: `${styles.headerTitle} ${shellStyles.standaloneTitle}`, children: [onBrandClick ? (_jsx("button", { type: "button", className: styles.brandHomeButton, onClick: onBrandClick, title: brandHomeLabel, "aria-label": brandHomeLabel, children: brandContent })) : (_jsx("span", { className: styles.brandHomeStatic, children: brandContent })), showProjectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: workspaceIdForCopy ? `Workspace ID: ${workspaceIdForCopy}` : undefined, role: workspaceIdForCopy ? 'button' : undefined, tabIndex: workspaceIdForCopy ? 0 : undefined, onClick: copyWorkspaceId, onMouseDown: (event) => event.preventDefault(), onKeyDown: handleProjectNameKeyDown, children: normalizedProjectName })] })), meta] }), _jsx("div", { className: styles.headerActions, style: { gap: '16px' }, children: actions })] }));
|
|
23
39
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { AppShellHeader } from './AppShellHeader';
|
|
5
6
|
import logoDark from '../../assets/branding/logos/logo_dark.svg';
|
|
6
7
|
describe('AppShellHeader', () => {
|
|
@@ -18,6 +19,22 @@ describe('AppShellHeader', () => {
|
|
|
18
19
|
render(_jsx(AppShellHeader, { runtimeMode: "local", theme: "dawn" }));
|
|
19
20
|
expect(screen.getByAltText('Taskforce Logo')).toHaveAttribute('src', logoDark);
|
|
20
21
|
});
|
|
22
|
+
it('calls the brand click handler from the logo button', async () => {
|
|
23
|
+
const onBrandClick = vi.fn();
|
|
24
|
+
render(_jsx(AppShellHeader, { runtimeMode: "local", onBrandClick: onBrandClick }));
|
|
25
|
+
await userEvent.click(screen.getByRole('button', { name: 'Go to Kanban' }));
|
|
26
|
+
expect(onBrandClick).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
it('copies the workspace id from the project badge', async () => {
|
|
29
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
30
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
31
|
+
configurable: true,
|
|
32
|
+
value: { writeText }
|
|
33
|
+
});
|
|
34
|
+
render(_jsx(AppShellHeader, { runtimeMode: "local", projectName: "Roadmap", currentWorkspaceId: "workspace-alpha" }));
|
|
35
|
+
await userEvent.click(screen.getByRole('button', { name: 'Roadmap' }));
|
|
36
|
+
expect(writeText).toHaveBeenCalledWith('workspace-alpha');
|
|
37
|
+
});
|
|
21
38
|
it('hides a stale project label while cloud runtime is still on the default workspace', () => {
|
|
22
39
|
render(_jsx(AppShellHeader, { runtimeMode: "cloud", projectName: "Old Workspace", currentWorkspaceId: "default" }));
|
|
23
40
|
expect(screen.queryByText('Old Workspace')).not.toBeInTheDocument();
|
|
@@ -106,6 +106,7 @@ function getFeatureDisplayParts(feature, seatLimit) {
|
|
|
106
106
|
export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
|
|
107
107
|
const [plans, setPlans] = useState([]);
|
|
108
108
|
const [pricingSource, setPricingSource] = useState(null);
|
|
109
|
+
const [pricingPageCopy, setPricingPageCopy] = useState({ pageTitle: null, pageDescription: null });
|
|
109
110
|
const [loading, setLoading] = useState(true);
|
|
110
111
|
const [error, setError] = useState(null);
|
|
111
112
|
const [retryNonce, setRetryNonce] = useState(0);
|
|
@@ -124,6 +125,10 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
124
125
|
if (active) {
|
|
125
126
|
setPlans(Array.isArray(body?.plans) ? body.plans : []);
|
|
126
127
|
setPricingSource(body?.pricingSource && typeof body.pricingSource === 'object' ? body.pricingSource : null);
|
|
128
|
+
setPricingPageCopy({
|
|
129
|
+
pageTitle: typeof body?.pageTitle === 'string' ? (body.pageTitle.trim() || null) : null,
|
|
130
|
+
pageDescription: typeof body?.pageDescription === 'string' ? (body.pageDescription.trim() || null) : null
|
|
131
|
+
});
|
|
127
132
|
}
|
|
128
133
|
return;
|
|
129
134
|
}
|
|
@@ -134,6 +139,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
134
139
|
const message = err instanceof Error ? err.message : 'Failed to load pricing';
|
|
135
140
|
setError(message || 'Failed to load pricing');
|
|
136
141
|
setPricingSource(null);
|
|
142
|
+
setPricingPageCopy({ pageTitle: null, pageDescription: null });
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
145
|
finally {
|
|
@@ -156,6 +162,8 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
156
162
|
return String(a.displayName || a.planId).localeCompare(String(b.displayName || b.planId));
|
|
157
163
|
}), [plans]);
|
|
158
164
|
const pricingSourceLabel = useMemo(() => formatPricingSource(pricingSource || fallbackPricingSource), [fallbackPricingSource, pricingSource]);
|
|
165
|
+
const resolvedHeading = String(pricingPageCopy.pageTitle || heading).trim() || heading;
|
|
166
|
+
const resolvedSubtitle = String(pricingPageCopy.pageDescription || subtitle || '').trim();
|
|
159
167
|
const planSelectionEnabled = authSessionResolved;
|
|
160
168
|
const contentClassName = `${styles.plansWrapper} ${shellOwnsScroll ? styles.shellOwnsScroll : 'tf-scrollbar'} ${variant === 'site' ? styles.siteVariant : ''} ${chrome ? styles.withChrome : ''}`.trim();
|
|
161
169
|
const wrapPage = (content) => {
|
|
@@ -170,7 +178,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
170
178
|
if (error) {
|
|
171
179
|
return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.emptyState, children: [_jsx("h2", { children: "Connecting to plan pricing..." }), _jsx("p", { children: error }), _jsx("button", { className: styles.backBtn, onClick: () => setRetryNonce((value) => value + 1), children: "Retry" })] }) }) }) }));
|
|
172
180
|
}
|
|
173
|
-
return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children:
|
|
181
|
+
return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: resolvedHeading }), resolvedSubtitle && _jsx("p", { className: styles.subtitle, children: resolvedSubtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
|
|
174
182
|
? styles.successBanner
|
|
175
183
|
: (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
|
|
176
184
|
const monthPrice = plan.pricing.month;
|
|
@@ -9,6 +9,8 @@ describe('PlanComparisonPage', () => {
|
|
|
9
9
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
10
10
|
ok: true,
|
|
11
11
|
json: async () => ({
|
|
12
|
+
pageTitle: 'Subscription Plans',
|
|
13
|
+
pageDescription: 'Manage your subscription and explore available features. Your current plan is highlighted below.',
|
|
12
14
|
pricingSource: {
|
|
13
15
|
environment: 'production',
|
|
14
16
|
runtimeMode: 'cloud'
|
|
@@ -66,6 +68,23 @@ describe('PlanComparisonPage', () => {
|
|
|
66
68
|
expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
|
|
67
69
|
expect(screen.getByText('For small collaborative teams.')).toBeInTheDocument();
|
|
68
70
|
});
|
|
71
|
+
it('prefers pricing payload header copy when provided', async () => {
|
|
72
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
73
|
+
ok: true,
|
|
74
|
+
json: async () => ({
|
|
75
|
+
pageTitle: 'Choose Your Taskforce Plan',
|
|
76
|
+
pageDescription: 'Compare plans and pick the right fit for your team.',
|
|
77
|
+
pricingSource: {
|
|
78
|
+
environment: 'production',
|
|
79
|
+
runtimeMode: 'cloud'
|
|
80
|
+
},
|
|
81
|
+
plans: []
|
|
82
|
+
})
|
|
83
|
+
})));
|
|
84
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", subtitle: "Fallback subtitle", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
85
|
+
expect(await screen.findByRole('heading', { name: 'Choose Your Taskforce Plan' })).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText('Compare plans and pick the right fit for your team.')).toBeInTheDocument();
|
|
87
|
+
});
|
|
69
88
|
it('falls back to provided pricing source details when the pricing response omits them', async () => {
|
|
70
89
|
globalThis.__DEBUG_MODE__ = true;
|
|
71
90
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
@@ -570,6 +570,6 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
570
570
|
navigate('/');
|
|
571
571
|
}, [closeEmbeddedPlansScreen, embedded, handleContinueToTaskforce, hasResolvedActiveEntitlement, navigate, onContinueToTaskforce]);
|
|
572
572
|
const headerActions = embedded ? null : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, disabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), children: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : 'Back' }), isAuthenticated && accountProfileSummary?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })), !isAuthenticated && (_jsxs("button", { className: styles.navAction, onClick: () => navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`), children: [_jsx(User, { size: 16, style: { marginRight: '8px', verticalAlign: 'text-bottom' } }), "Sign In"] }))] }));
|
|
573
|
-
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, actions: headerActions }));
|
|
573
|
+
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, onBrandClick: handleBack, actions: headerActions }));
|
|
574
574
|
return (_jsx(PlanComparisonPage, { chrome: chrome, heading: "Subscription Plans", subtitle: "Manage your subscription and explore available features. Your current plan is highlighted below.", isAuthenticated: isAuthenticated, authSessionResolved: authSessionResolved, currentPlanId: String(accountProfileSummary?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(accountProfileSummary?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(accountProfileSummary?.entitlementState || '').trim() || null, markCurrentPlan: lifecyclePresentation.markCurrentPlan, canManageCurrentPlan: Boolean(accountProfileSummary?.stripeCustomerId), pricingEndpoint: pricingEndpoint, statusBanner: feedback, onBack: handleBack, backLabel: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : (embedded ? 'Back to Board' : 'Back to App'), backDisabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), topActions: null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: false, showHeaderBarWithChrome: false, currentPlanCardActionMode: "manage", fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
|
|
575
575
|
}
|