@taskforcehq/taskforce 0.3.308 → 0.3.311
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 +59 -135
- package/dist/components/context/ContextAttachmentManager.js +4 -3
- package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
- package/dist/components/ui/DisclosureSectionHeader.js +8 -0
- package/dist/components/views/PlanComparisonPage.js +1 -1
- package/dist/components/views/PlanComparisonPage.test.js +1 -0
- package/dist/components/views/StandaloneLayout.js +21 -4
- package/dist/components/views/panels/PlanningDrawer.js +2 -1
- package/dist/components/views/panels/PlanningDrawer.test.js +2 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +3 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +18 -12
- package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
- package/dist/components/views/standalone/modals/HelpModal.js +4 -2
- package/dist/components/views/standalone/modals/HelpModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/HelpModal.test.js +23 -0
- package/dist/core/PlanEntitlementService.d.ts +2 -0
- package/dist/core/PlanEntitlementService.js +57 -4
- package/dist/core/PlanVersionPolicy.test.js +32 -0
- package/dist/localization/locales/en-US.js +1 -1
- package/dist/localization/locales/es-419.js +1 -1
- package/dist/localization/locales/pt-BR.js +1 -1
- package/dist/mcp/runtime.js +49 -14
- package/dist/mcp/runtime.test.js +91 -0
- package/dist/server/routes/billing.js +0 -1
- package/dist/server/routes/billing.test.js +60 -0
- package/dist/ui/assets/AgentsModule-D8u9P5dZ.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-CsUauFA2.js} +1 -1
- package/dist/ui/assets/ContextAttachmentManager-DbQvDHFz.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-BmTkN1A8.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-BXHTM-Nh.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-D33RyXx0.js} +1 -1
- package/dist/ui/assets/PlansPage-D0RXlT9_.js +1 -0
- package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
- package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-Vsg67lpR.js} +1 -1
- package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DshLw7_d.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CLM-1BnF.js} +1 -1
- package/dist/ui/assets/documentReferences-BPWp6ce0.js +1 -0
- package/dist/ui/assets/{index-BvAbqx5Q.css → index-DOR9PRqt.css} +1 -1
- package/dist/ui/assets/index-zvbo2Jse.js +5 -0
- package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/supportContact.d.ts +9 -0
- package/dist/utils/supportContact.js +32 -0
- package/dist/utils/supportContact.test.d.ts +1 -0
- package/dist/utils/supportContact.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
- package/dist/ui/assets/index-MAHKvFqp.js +0 -4
- /package/dist/ui/agent-logos/{ChatGPT.png → chatgpt.png} +0 -0
package/README.md
CHANGED
|
@@ -1,182 +1,106 @@
|
|
|
1
1
|
# Taskforce
|
|
2
2
|
|
|
3
|
-
Taskforce is a task
|
|
3
|
+
Taskforce is a shared task and planning workspace for humans collaborating with AI.
|
|
4
4
|
|
|
5
|
-
It
|
|
5
|
+
It brings tasks, planning, document review, image review, and AI access into one place so people and AI collaborators can work from the same source of truth.
|
|
6
6
|
|
|
7
7
|
Taskforce is currently in beta and under active development.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Install Taskforce
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- Tasks with title, description, status, priority, type, category, assignee, due date, scheduled date, and checklist support
|
|
13
|
-
- Custom workspace taxonomy for categories, types, priorities, approaches, and complexity
|
|
14
|
-
- Task comments and task attachments
|
|
15
|
-
- Schedule board for planning work by day
|
|
16
|
-
- Archive and restore flows for completed or cancelled work
|
|
17
|
-
|
|
18
|
-
### Planning model
|
|
19
|
-
- Initiatives, workstreams, tasks, and checklists
|
|
20
|
-
- Workstream and initiative assignment for tasks
|
|
21
|
-
- Initiative templates for reusable planning structures such as launch, migration, or incident work
|
|
22
|
-
|
|
23
|
-
### AI collaboration
|
|
24
|
-
- Built-in MCP support for local and cloud usage
|
|
25
|
-
- AI profile resolution and task ownership for agent collaborators
|
|
26
|
-
- MCP tools for reading and updating tasks, planning entities, documents, image reviews, schedules, and attachments
|
|
27
|
-
- Local MCP configuration for local clients such as Codex, Claude Code CLI, and Gemini CLI
|
|
28
|
-
- Cloud MCP token flow for remote clients
|
|
11
|
+
Install Taskforce once, then run it from any workspace:
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- Image Notes for visual review with saved annotation sessions and markers
|
|
33
|
-
- MCP helper flows for document review and image review
|
|
34
|
-
|
|
35
|
-
### Runtime and deployment model
|
|
36
|
-
- Local runtime and cloud runtime support
|
|
37
|
-
- Separate app, site, and admin surfaces in the repo
|
|
38
|
-
- Auth, billing, and account flows for the hosted product
|
|
39
|
-
- Workspace sync and cloud/local runtime routing infrastructure
|
|
40
|
-
|
|
41
|
-
## What is intentionally not part of the product right now
|
|
42
|
-
|
|
43
|
-
These exist in the codebase in some form, but should not be treated as current product surface area.
|
|
44
|
-
|
|
45
|
-
- **Workflow editor**: currently disabled and should not be presented as an active feature
|
|
46
|
-
- **Embedded widget/in-app overlay version**: not part of the product right now, not tested recently, and likely to be deprecated
|
|
47
|
-
|
|
48
|
-
## Product surfaces
|
|
49
|
-
|
|
50
|
-
- `apps/app` - the main Taskforce application
|
|
51
|
-
- `apps/site` - the public website / marketing surface
|
|
52
|
-
- `apps/admin` - the admin surface
|
|
53
|
-
|
|
54
|
-
## Repository structure
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
apps/
|
|
58
|
-
app/ Main product app
|
|
59
|
-
site/ Public website
|
|
60
|
-
admin/ Admin surface
|
|
61
|
-
src/
|
|
62
|
-
components/ UI components and product modules
|
|
63
|
-
core/ Core application logic
|
|
64
|
-
server/ Server routes and runtime logic
|
|
65
|
-
mcp/ MCP runtime and tool registry
|
|
66
|
-
sync/ Sync model and transfer logic
|
|
67
|
-
storage/ Persistence and asset storage
|
|
68
|
-
tests/
|
|
69
|
-
e2e/ Playwright realtime and interactive browser scenarios
|
|
70
|
-
soak/ Playwright soak and endurance scenarios
|
|
13
|
+
```bash
|
|
14
|
+
npm install --global @taskforcehq/taskforce
|
|
71
15
|
```
|
|
72
16
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## Getting started
|
|
76
|
-
|
|
77
|
-
### Prerequisites
|
|
78
|
-
- Node.js 20+
|
|
79
|
-
- npm
|
|
80
|
-
|
|
81
|
-
### Install dependencies
|
|
17
|
+
Then open Taskforce from the workspace you want to manage:
|
|
82
18
|
|
|
83
19
|
```bash
|
|
84
|
-
|
|
20
|
+
taskforce open
|
|
85
21
|
```
|
|
86
22
|
|
|
87
|
-
|
|
23
|
+
You can also install with Homebrew:
|
|
88
24
|
|
|
89
25
|
```bash
|
|
90
|
-
|
|
26
|
+
brew tap taskforcehq/tap
|
|
27
|
+
brew install taskforce
|
|
91
28
|
```
|
|
92
29
|
|
|
93
|
-
|
|
30
|
+
The npm package page may show `npm i @taskforcehq/taskforce`, but most users should include `--global` so the `taskforce` command is available from the terminal.
|
|
94
31
|
|
|
95
|
-
|
|
32
|
+
## Get Started
|
|
33
|
+
|
|
34
|
+
Go to the folder you want Taskforce to manage, then run:
|
|
96
35
|
|
|
97
36
|
```bash
|
|
98
|
-
|
|
37
|
+
taskforce open
|
|
99
38
|
```
|
|
100
39
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Taskforce includes built-in MCP support so AI clients can operate directly against the workspace.
|
|
40
|
+
Taskforce opens in your browser and stores workspace data in that folder, under `.taskforce/`.
|
|
104
41
|
|
|
105
|
-
|
|
106
|
-
Use Local MCP for local stdio-based clients. This is the intended path for tools such as:
|
|
107
|
-
- Codex
|
|
108
|
-
- Claude Code CLI
|
|
109
|
-
- Gemini CLI
|
|
42
|
+
To confirm which workspace Taskforce is using:
|
|
110
43
|
|
|
111
|
-
|
|
44
|
+
```bash
|
|
45
|
+
taskforce status
|
|
46
|
+
```
|
|
112
47
|
|
|
113
|
-
|
|
114
|
-
Use Cloud MCP for hosted remote clients. Cloud MCP uses token-based access and the hosted MCP endpoint.
|
|
48
|
+
To connect a local AI client through MCP, start the MCP server from the same workspace:
|
|
115
49
|
|
|
116
|
-
|
|
50
|
+
```bash
|
|
51
|
+
taskforce mcp
|
|
52
|
+
```
|
|
117
53
|
|
|
118
|
-
|
|
119
|
-
Taskforce is designed so people and AI agents can work from the same task graph, rather than forcing humans to maintain one system and agents to operate from another.
|
|
54
|
+
## What Taskforce Includes
|
|
120
55
|
|
|
121
|
-
###
|
|
122
|
-
Taskforce supports a planning hierarchy that can represent real work:
|
|
123
|
-
- Initiative
|
|
124
|
-
- Workstream
|
|
125
|
-
- Task
|
|
126
|
-
- Checklist
|
|
56
|
+
### Task Management
|
|
127
57
|
|
|
128
|
-
|
|
58
|
+
- Tasks with title, description, status, priority, type, category, assignee, due date, scheduled date, and checklist support
|
|
59
|
+
- Custom workspace taxonomy for categories, types, priorities, approaches, and complexity
|
|
60
|
+
- Task comments and task attachments
|
|
61
|
+
- Schedule board for planning work by day
|
|
62
|
+
- Archive and restore flows for completed or cancelled work
|
|
129
63
|
|
|
130
|
-
###
|
|
131
|
-
Taskforce includes first-class review surfaces for:
|
|
132
|
-
- documents, via document review sessions and comments
|
|
133
|
-
- images, via Image Notes sessions and markers
|
|
64
|
+
### Planning
|
|
134
65
|
|
|
135
|
-
|
|
66
|
+
- Initiatives, workstreams, tasks, and checklists
|
|
67
|
+
- Workstream and initiative assignment for tasks
|
|
68
|
+
- Initiative templates for reusable planning structures such as launches, migrations, and incident response
|
|
136
69
|
|
|
137
|
-
###
|
|
138
|
-
Taskforce exposes product state through MCP so agents can:
|
|
139
|
-
- read tasks and planning structures
|
|
140
|
-
- update status and ownership
|
|
141
|
-
- create and comment on review sessions
|
|
142
|
-
- work with schedules and attachments
|
|
70
|
+
### AI Collaboration
|
|
143
71
|
|
|
144
|
-
|
|
72
|
+
- Built-in MCP access for AI clients
|
|
73
|
+
- AI profiles and task ownership for agent collaborators
|
|
74
|
+
- Tools for agents to read and update tasks, planning entities, documents, image reviews, schedules, and attachments
|
|
75
|
+
- Local MCP support for clients such as Codex, Claude Code CLI, and Gemini CLI
|
|
76
|
+
- Cloud MCP access for hosted remote clients
|
|
145
77
|
|
|
146
|
-
|
|
147
|
-
- a production-ready embedded widget product
|
|
148
|
-
- a workflow editor product
|
|
149
|
-
- a generic developer overlay tool
|
|
150
|
-
- something centered only on AI-developer pairs
|
|
78
|
+
### Document And Image Review
|
|
151
79
|
|
|
152
|
-
|
|
80
|
+
- Document review sessions with threaded comments
|
|
81
|
+
- Image Notes for visual review with saved annotation sessions and markers
|
|
82
|
+
- AI-assisted document and image review workflows
|
|
153
83
|
|
|
154
|
-
|
|
84
|
+
### Sync And Hosted Workspaces
|
|
155
85
|
|
|
156
|
-
|
|
86
|
+
- Local workspace data for private, machine-local use
|
|
87
|
+
- Hosted workspaces for cloud-backed collaboration
|
|
88
|
+
- Workspace sync between local and cloud environments
|
|
89
|
+
- Account, auth, and billing flows for hosted usage
|
|
157
90
|
|
|
158
|
-
##
|
|
91
|
+
## MCP
|
|
159
92
|
|
|
160
|
-
|
|
161
|
-
Useful local checks include:
|
|
93
|
+
Taskforce includes built-in MCP support so AI clients can work directly with your workspace.
|
|
162
94
|
|
|
163
|
-
|
|
164
|
-
npm run build
|
|
165
|
-
npm run test
|
|
166
|
-
```
|
|
95
|
+
Local MCP is for AI clients running on the same machine as your workspace. It does not require a cloud token.
|
|
167
96
|
|
|
168
|
-
|
|
97
|
+
Cloud MCP is for hosted remote clients and uses token-based access.
|
|
169
98
|
|
|
170
|
-
|
|
171
|
-
- `docs/runbooks/sync-soak-runbook.md`
|
|
172
|
-
- `docs/reference/operations/external-qa-workflow.md`
|
|
99
|
+
## Requirements
|
|
173
100
|
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
- `docs/reference/platform/runtime-modes-contract.md`
|
|
177
|
-
- `docs/reference/operations/external-qa-workflow.md`
|
|
178
|
-
- `docs/runbooks/sync-soak-runbook.md`
|
|
101
|
+
- Node.js 20+
|
|
102
|
+
- npm, or Homebrew on macOS
|
|
179
103
|
|
|
180
104
|
## Status
|
|
181
105
|
|
|
182
|
-
Taskforce is in active beta development.
|
|
106
|
+
Taskforce is in active beta development. This README describes the user-facing capabilities that are ready to try today.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { AlertTriangle, Copy, Download, File, FileUp, Loader2, PencilLine, Trash2
|
|
3
|
+
import { AlertTriangle, Copy, Download, File, FileUp, Loader2, PencilLine, Trash2 } from 'lucide-react';
|
|
4
4
|
import styles from '../../Taskforce.module.css';
|
|
5
|
+
import { DisclosureSectionHeader } from '../ui/DisclosureSectionHeader';
|
|
5
6
|
import { dispatchOpenMarkdownDocument } from '../../utils/contextFiles';
|
|
6
7
|
import { canOpenAnnotatedAttachment, dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
|
|
7
8
|
import { dispatchTaskforceContextAssetsMutated } from '../../utils/contextAssetEvents';
|
|
@@ -460,7 +461,7 @@ export function ContextAttachmentManager(props) {
|
|
|
460
461
|
const path = typeof item === 'string' ? item : item.path;
|
|
461
462
|
return !isImagePath(path);
|
|
462
463
|
});
|
|
463
|
-
return (_jsxs("div", { className: styles.specialistSection, children: [
|
|
464
|
+
return (_jsxs("div", { className: styles.specialistSection, children: [_jsx(DisclosureSectionHeader, { label: "Context Documents", count: attachments.length, expanded: showContextUpload, expandedTitle: "Hide context documents", collapsedTitle: "Show context documents", onToggle: () => setShowContextUpload(!showContextUpload) }), showContextUpload && (_jsxs(_Fragment, { children: [_jsxs("p", { className: styles.settingsHint, children: ["Upload files into ", ownerTypeLabel, " context storage, or add an external http(s) URL."] }), _jsxs("div", { className: styles.contextUploadPanel, children: [_jsxs("div", { className: styles.contextUploadActions, children: [_jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => inputRef.current?.click(), disabled: uploading, children: [uploading ? _jsx(Loader2, { size: 12, className: styles.spinner }) : _jsx(FileUp, { size: 12 }), uploading ? 'Uploading...' : 'Upload File'] }), _jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => {
|
|
464
465
|
void handlePasteImage();
|
|
465
466
|
}, disabled: uploading, children: [_jsx(Copy, { size: 12 }), "Paste Image from Clipboard"] })] }), _jsx("input", { ref: inputRef, type: "file", accept: ACCEPTED_CONTEXT_FILE_TYPES, multiple: true, className: styles.hiddenInput, onChange: (e) => {
|
|
466
467
|
void handleFilesSelected(e.target.files);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface DisclosureSectionHeaderProps {
|
|
2
|
+
label: string;
|
|
3
|
+
count?: number;
|
|
4
|
+
showZeroCount?: boolean;
|
|
5
|
+
expanded: boolean;
|
|
6
|
+
expandedTitle: string;
|
|
7
|
+
collapsedTitle: string;
|
|
8
|
+
onToggle: () => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function DisclosureSectionHeader({ label, count, showZeroCount, expanded, expandedTitle, collapsedTitle, onToggle, className, }: DisclosureSectionHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
|
+
import styles from './DisclosureSectionHeader.module.css';
|
|
4
|
+
export function DisclosureSectionHeader({ label, count, showZeroCount = false, expanded, expandedTitle, collapsedTitle, onToggle, className = '', }) {
|
|
5
|
+
const shouldShowCount = typeof count === 'number' && (showZeroCount || count > 0);
|
|
6
|
+
const Icon = expanded ? ChevronDown : ChevronRight;
|
|
7
|
+
return (_jsxs("button", { type: "button", className: `${styles.header} ${className}`.trim(), onClick: onToggle, "aria-expanded": expanded, title: expanded ? expandedTitle : collapsedTitle, children: [_jsxs("span", { className: styles.title, children: [label, shouldShowCount ? ` (${count})` : ''] }), _jsx(Icon, { className: styles.icon, size: 14, "aria-hidden": "true" })] }));
|
|
8
|
+
}
|
|
@@ -386,5 +386,5 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', theme =
|
|
|
386
386
|
isCurrentPlan: false
|
|
387
387
|
});
|
|
388
388
|
}, disabled: !planVersionId || !planSelectionEnabled, children: hasMultipleBillingOptions ? 'Start Yearly' : 'Get Started' })) : null] }))] }, plan.planId));
|
|
389
|
-
}) }), _jsxs("div", { className: styles.pricingNotes, children: [_jsx("p", { children: "Prices are listed in USD. Taxes may apply." }), _jsx("p", { children: "Local install is free on every plan. Cloud limits apply only to hosted workspaces." })] }), _jsxs("section", { className: `${styles.foundingBeta} ${styles.cloudCapacityNote}`, "aria-labelledby": "cloud-capacity-heading", children: [_jsx("h2", { id: "cloud-capacity-heading", children: "Need more cloud capacity?" }), _jsx("p", { children: "Pro users can request expanded limits for cloud workspaces, AI agents, and storage." })] }), _jsxs("div", { className: `${styles.planCard} ${styles.planNoticeCard} ${styles.teamComingSoonCard}`, children: [_jsx("h2", { className: styles.planName, children: "Team workspaces are coming soon." }), _jsx("p", { className: styles.planDescription, children: "Shared workspaces, collaborators, team agent rosters, admin controls, and larger cloud limits are in development." })] })] }))] }) }) }));
|
|
389
|
+
}) }), _jsxs("div", { className: styles.pricingNotes, children: [_jsx("p", { children: "Prices are listed in USD. Taxes may apply." }), _jsx("p", { children: "Local install is free on every plan. Cloud limits apply only to hosted workspaces." })] }), _jsxs("section", { className: `${styles.foundingBeta} ${styles.cloudCapacityNote}`, "aria-labelledby": "cloud-capacity-heading", children: [_jsx("h2", { id: "cloud-capacity-heading", children: "Need more cloud capacity?" }), _jsx("p", { children: "Pro users can request expanded limits for cloud workspaces, AI agents, and storage." }), _jsxs("p", { children: ["Contact ", _jsx("a", { href: "mailto:support@taskforcehq.ai", children: "support@taskforcehq.ai" }), " to discuss capacity needs."] })] }), _jsxs("div", { className: `${styles.planCard} ${styles.planNoticeCard} ${styles.teamComingSoonCard}`, children: [_jsx("h2", { className: styles.planName, children: "Team workspaces are coming soon." }), _jsx("p", { className: styles.planDescription, children: "Shared workspaces, collaborators, team agent rosters, admin controls, and larger cloud limits are in development." })] })] }))] }) }) }));
|
|
390
390
|
}
|
|
@@ -72,6 +72,7 @@ describe('PlanComparisonPage', () => {
|
|
|
72
72
|
expect(screen.getByText(/Early members get access while the product is still forming/)).toBeInTheDocument();
|
|
73
73
|
expect(screen.getByText('Prices are listed in USD. Taxes may apply.')).toBeInTheDocument();
|
|
74
74
|
expect(screen.getByText('Local install is free on every plan. Cloud limits apply only to hosted workspaces.')).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByRole('link', { name: 'support@taskforcehq.ai' })).toHaveAttribute('href', 'mailto:support@taskforcehq.ai');
|
|
75
76
|
const foundingBeta = screen.getByRole('heading', { name: 'Founding beta' }).closest('section');
|
|
76
77
|
const firstPlanCard = container.querySelector('[data-testid^="plan-card-"]');
|
|
77
78
|
const planCards = container.querySelectorAll('[data-testid^="plan-card-"]');
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import React, { useMemo, useCallback, useState, useEffect, useRef, 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, Users, ClockAlert, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, Bot, Image, Workflow, Plug } from 'lucide-react';
|
|
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, ClockAlert, Mail, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, Bot, Image, Workflow, Plug } 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';
|
|
@@ -41,6 +41,7 @@ import { resolveAnnotatedAttachmentDetail } from '../../utils/annotatedAttachmen
|
|
|
41
41
|
import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from '../../utils/accountProfileSummaryCache';
|
|
42
42
|
import { prepareAvatarUploadFile } from '../../utils/avatarUpload';
|
|
43
43
|
import { resolveCommercialLifecyclePresentation } from '../../utils/commercialLifecyclePresentation';
|
|
44
|
+
import { buildSupportMailtoHref } from '../../utils/supportContact';
|
|
44
45
|
import { AppShellHeader } from './AppShellHeader';
|
|
45
46
|
import { getTaskforceCloudEnvironmentConfig, parseTaskforceCloudEnvironment, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
|
|
46
47
|
import { logBootstrapDebug, persistRemoteUiState } from '../../hooks/workspace/workspaceLocalState';
|
|
@@ -1382,6 +1383,15 @@ export function StandaloneLayout(props) {
|
|
|
1382
1383
|
const authIdentityLabel = authDisplayName || authEmailLabel;
|
|
1383
1384
|
const hasAuthIdentity = authIdentityLabel.length > 0;
|
|
1384
1385
|
const hasAuthEmail = authEmailLabel.length > 0;
|
|
1386
|
+
const supportMailtoHref = useMemo(() => buildSupportMailtoHref({
|
|
1387
|
+
workspaceName: currentWorkspaceLabel,
|
|
1388
|
+
workspaceId: currentWorkspaceId,
|
|
1389
|
+
accountLabel: authIdentityLabel || authEmailLabel,
|
|
1390
|
+
runtimeMode
|
|
1391
|
+
}), [authEmailLabel, authIdentityLabel, currentWorkspaceId, currentWorkspaceLabel, runtimeMode]);
|
|
1392
|
+
const openSupportEmail = useCallback(() => {
|
|
1393
|
+
window.location.href = supportMailtoHref;
|
|
1394
|
+
}, [supportMailtoHref]);
|
|
1385
1395
|
const accountBadgeLabel = (authIdentityLabel || authEmailLabel || '').trim();
|
|
1386
1396
|
const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;
|
|
1387
1397
|
const accountBadgeInitial = shouldShowResolvedAccountInitial ? accountBadgeLabel.charAt(0).toUpperCase() : '';
|
|
@@ -2428,7 +2438,10 @@ export function StandaloneLayout(props) {
|
|
|
2428
2438
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2429
2439
|
setShowAccountMenu(false);
|
|
2430
2440
|
setShowHelpModal(true);
|
|
2431
|
-
}, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help &
|
|
2441
|
+
}, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help & Support"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2442
|
+
setShowAccountMenu(false);
|
|
2443
|
+
openSupportEmail();
|
|
2444
|
+
}, role: "menuitem", children: [_jsx(Mail, { size: 15 }), "Contact Support"] }), runtimeMode === 'local' && !isAuthenticated && !isAuthBootstrapPending && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
|
|
2432
2445
|
openSharedLoginRoute('login');
|
|
2433
2446
|
}, role: "menuitem", children: [_jsx(User, { size: 15 }), "Sign In / Register"] })), isAuthenticated && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: async () => {
|
|
2434
2447
|
setShowAccountMenu(false);
|
|
@@ -3315,15 +3328,19 @@ export function StandaloneLayout(props) {
|
|
|
3315
3328
|
}, 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: {
|
|
3316
3329
|
...settingsModel,
|
|
3317
3330
|
onOpenCloudAuth: openSharedLoginRoute
|
|
3318
|
-
}, onSectionChange: setSettingsSection }) }) }), _jsx(ScheduleWarningModal, { prompt: scheduleWarningPrompt, theme: currentTheme, onClose: cancelScheduleWarning, onConfirm: confirmScheduleWarning }), _jsx(SyncEnableWarningModal, { isOpen: showSyncEnableWarning, theme: currentTheme, onClose: cancelSyncEnableWarning, onConfirm: confirmSyncEnableWarning }), _jsx(HelpModal, { isOpen: showHelpModal, theme: currentTheme, onClose: () => setShowHelpModal(false), onOpenSettings: () => {
|
|
3331
|
+
}, onSectionChange: setSettingsSection }) }) }), _jsx(ScheduleWarningModal, { prompt: scheduleWarningPrompt, theme: currentTheme, onClose: cancelScheduleWarning, onConfirm: confirmScheduleWarning }), _jsx(SyncEnableWarningModal, { isOpen: showSyncEnableWarning, theme: currentTheme, onClose: cancelSyncEnableWarning, onConfirm: confirmSyncEnableWarning }), _jsx(HelpModal, { isOpen: showHelpModal, theme: currentTheme, onClose: () => setShowHelpModal(false), supportMailtoHref: supportMailtoHref, onOpenSettings: () => {
|
|
3319
3332
|
setShowHelpModal(false);
|
|
3320
3333
|
setActiveTab('settings');
|
|
3321
3334
|
setSettingsSection('general');
|
|
3335
|
+
}, onOpenAccountHub: () => {
|
|
3336
|
+
setShowHelpModal(false);
|
|
3337
|
+
startAccountSurfaceTiming('account_hub');
|
|
3338
|
+
setShowAccountHub(true);
|
|
3322
3339
|
} }), _jsx(EditProfileModal, { isOpen: showEditProfileModal, theme: currentTheme, onClose: closeEditProfileModal, onSave: () => { void handleSaveProfile(); }, displayName: profileDisplayNameDraft, email: authEmailLabel, avatarDisplayUrl: profileAvatarDisplayUrl, accountBadgeInitial: accountBadgeInitial, saveBusy: profileSaveBusy, avatarBusy: profileAvatarBusy, saveError: profileSaveError, saveNotice: profileSaveNotice, onDisplayNameChange: setProfileDisplayNameDraft, onOpenAvatarManager: () => {
|
|
3323
3340
|
setProfileSaveError(null);
|
|
3324
3341
|
setProfileSaveNotice(null);
|
|
3325
3342
|
setShowAvatarPhotoManager(true);
|
|
3326
|
-
} }), _jsx(AvatarImageManagerModal, { isOpen: showAvatarPhotoManager, theme: currentTheme, title: "Edit Profile Photo", currentImageUrl: profileAvatarDisplayUrl, fallbackInitial: accountBadgeInitial, accept: PROFILE_AVATAR_ACCEPT, busy: profileAvatarBusy, hasPendingImage: Boolean(profileAvatarDraftId), canRemove: Boolean(profileAvatarDisplayUrl), error: profileSaveError, notice: profileSaveNotice, onClose: () => setShowAvatarPhotoManager(false), onApplyImage: handleProfileAvatarSelected, onRemoveImage: handleRemoveProfileAvatar, onDiscardPendingImage: handleDiscardPendingProfileAvatar }), _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, accountProfileSummary: accountProfileSummary, currentWorkspaceId: currentWorkspaceId, canOpenTeamManagement: canOpenTeamManagement, canManageWorkspaceSync: canManageWorkspaceSync, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncStatusLabel: syncStatusMeta.label, workspaceSyncError: workspaceSyncError, syncControlBusy: syncControlBusy, cloudAuthEnabled: cloudAuthConfigured, availableAuthProviders: availableAuthProviders, onFetchLoginMethods: fetchLoginMethods, onUnlinkLoginMethod: unlinkLoginMethod, onAddPassword: addPasswordToAccount, onLinkProvider: (provider) => beginOAuthLink(provider), onClose: () => setShowAccountHub(false), onOpenWorkspaceAudit: () => {
|
|
3343
|
+
} }), _jsx(AvatarImageManagerModal, { isOpen: showAvatarPhotoManager, theme: currentTheme, title: "Edit Profile Photo", currentImageUrl: profileAvatarDisplayUrl, fallbackInitial: accountBadgeInitial, accept: PROFILE_AVATAR_ACCEPT, busy: profileAvatarBusy, hasPendingImage: Boolean(profileAvatarDraftId), canRemove: Boolean(profileAvatarDisplayUrl), error: profileSaveError, notice: profileSaveNotice, onClose: () => setShowAvatarPhotoManager(false), onApplyImage: handleProfileAvatarSelected, onRemoveImage: handleRemoveProfileAvatar, onDiscardPendingImage: handleDiscardPendingProfileAvatar }), _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, accountProfileSummary: accountProfileSummary, currentWorkspaceId: currentWorkspaceId, canOpenTeamManagement: canOpenTeamManagement, canManageWorkspaceSync: canManageWorkspaceSync, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncStatusLabel: syncStatusMeta.label, workspaceSyncError: workspaceSyncError, syncControlBusy: syncControlBusy, supportMailtoHref: supportMailtoHref, cloudAuthEnabled: cloudAuthConfigured, availableAuthProviders: availableAuthProviders, onFetchLoginMethods: fetchLoginMethods, onUnlinkLoginMethod: unlinkLoginMethod, onAddPassword: addPasswordToAccount, onLinkProvider: (provider) => beginOAuthLink(provider), onClose: () => setShowAccountHub(false), onOpenWorkspaceAudit: () => {
|
|
3327
3344
|
setShowAccountHub(false);
|
|
3328
3345
|
void openTeamManagementModal();
|
|
3329
3346
|
setTeamManagementTab('audit');
|
|
@@ -7,6 +7,7 @@ import styles from '../../../Taskforce.module.css';
|
|
|
7
7
|
import panelStyles from './PlanningDrawer.module.css';
|
|
8
8
|
import { ReferenceBadgeButton } from '../../ui/ReferenceBadgeButton';
|
|
9
9
|
import { HierarchyActionBadge } from '../../ui/HierarchyActionBadge';
|
|
10
|
+
import { DisclosureSectionHeader } from '../../ui/DisclosureSectionHeader';
|
|
10
11
|
import { IconMap, getStatusDefinition, resolveColor } from '../../../utils/constants';
|
|
11
12
|
import { formatInitiativeReference, formatWorkstreamReference, } from '../../../utils/planningReferences.js';
|
|
12
13
|
import { getEntityActivityItems } from '../../../utils/taskActivity';
|
|
@@ -363,7 +364,7 @@ function PlanningDrawerDetailView({ detail, currentWorkspaceId, onOpenNestedWork
|
|
|
363
364
|
event.preventDefault();
|
|
364
365
|
handleOpenTask();
|
|
365
366
|
}, children: [_jsx("span", { className: panelStyles.listItemTitle, children: task.title }), _jsx("span", { className: panelStyles.listItemMeta, children: taskMeta })] })] }) }, task.id));
|
|
366
|
-
}) : (_jsxs("div", { className: panelStyles.sectionNote, children: ["No tasks are linked to this workstream yet. Use the ", _jsx("code", { children: "+" }), " action above to add the first one."] })))] }), _jsx("div", { className: panelStyles.listCard, children: _jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredContextAttachmentManager, { ownerType: detail.type, ownerId: detail.item.id, ownerReferenceLabel: detailReferenceLabel, workspaceId: currentWorkspaceId, attachments: detail.item.attachments || [], onAddAttachment: (file) => onAddPlanningContextFile?.(detail.type, detail.item.id, file), onRemoveAttachment: (index) => onRemovePlanningContextFile?.(detail.type, detail.item.id, index), onUpdateAttachmentCaption: (index, caption) => (onUpdatePlanningContextCaption?.(detail.type, detail.item.id, index, caption)) }) }) }), _jsxs("div", { className: panelStyles.listCard, children: [
|
|
367
|
+
}) : (_jsxs("div", { className: panelStyles.sectionNote, children: ["No tasks are linked to this workstream yet. Use the ", _jsx("code", { children: "+" }), " action above to add the first one."] })))] }), _jsx("div", { className: panelStyles.listCard, children: _jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredContextAttachmentManager, { ownerType: detail.type, ownerId: detail.item.id, ownerReferenceLabel: detailReferenceLabel, workspaceId: currentWorkspaceId, attachments: detail.item.attachments || [], onAddAttachment: (file) => onAddPlanningContextFile?.(detail.type, detail.item.id, file), onRemoveAttachment: (index) => onRemovePlanningContextFile?.(detail.type, detail.item.id, index), onUpdateAttachmentCaption: (index, caption) => (onUpdatePlanningContextCaption?.(detail.type, detail.item.id, index, caption)) }) }) }), _jsxs("div", { className: panelStyles.listCard, children: [_jsx(DisclosureSectionHeader, { label: "Recent Activity", count: activityCount, showZeroCount: true, expanded: showRecentActivity, expandedTitle: "Hide recent activity", collapsedTitle: "Show recent activity", onToggle: () => setShowRecentActivity((current) => !current) }), showRecentActivity ? (_jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredEntityActivityTimeline, { activity: detail.item.activity, comments: detail.item.comments, formatFieldValue: formatActivityFieldValue, resolveActorPresentation: resolveActivityActorPresentation, onOpenTaskById: onOpenTaskById, emptyMessage: `No ${detail.type} activity yet.`, listClassName: panelStyles.activityTimelineList, emptyClassName: panelStyles.activityTimelineEmpty }) })) : null] })] }));
|
|
367
368
|
}
|
|
368
369
|
function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner, draftInitiativeId, assigneeOptions, draftInitiativeSummary, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onCancel, onSubmit, isSubmitting, onAssignInitiativeToWorkstream, }) {
|
|
369
370
|
const entityLabel = editor.entityType === 'initiative' ? 'Initiative' : 'Workstream';
|
|
@@ -490,10 +490,10 @@ describe('PlanningDrawer context attachments', () => {
|
|
|
490
490
|
detail: { type: 'workstream', item: workstreamSummary },
|
|
491
491
|
});
|
|
492
492
|
const contextHeading = await screen.findByText('Context Documents (1)');
|
|
493
|
-
const activityHeading = screen.getByText('Recent
|
|
493
|
+
const activityHeading = screen.getByText('Recent Activity (1)');
|
|
494
494
|
expect(Boolean(contextHeading.compareDocumentPosition(activityHeading) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
495
495
|
expect(await screen.findByText('Task added: Investigate bootstrap timeouts')).toBeInTheDocument();
|
|
496
|
-
await user.click(screen.getByRole('button', { name: 'Recent
|
|
496
|
+
await user.click(screen.getByRole('button', { name: 'Recent Activity (1)' }));
|
|
497
497
|
expect(screen.queryByText('Task added: Investigate bootstrap timeouts')).not.toBeInTheDocument();
|
|
498
498
|
});
|
|
499
499
|
it('wires workstream context caption and remove actions through planning callbacks', async () => {
|
|
@@ -56,6 +56,7 @@ interface AccountHubModalProps {
|
|
|
56
56
|
onStartCheckout: () => void;
|
|
57
57
|
onToggleWorkspaceSync: (enabled: boolean) => void;
|
|
58
58
|
onOpenHelp: () => void;
|
|
59
|
+
supportMailtoHref: string;
|
|
59
60
|
}
|
|
60
|
-
export declare function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, }: AccountHubModalProps): import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
export declare function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, supportMailtoHref, }: AccountHubModalProps): import("react/jsx-runtime").JSX.Element;
|
|
61
62
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Mail } from 'lucide-react';
|
|
3
4
|
import { Modal } from '../../../ui/Modal';
|
|
4
5
|
import modalStyles from '../../../ui/Modal.module.css';
|
|
5
6
|
import accountStyles from '../../StandaloneAccount.module.css';
|
|
@@ -8,7 +9,7 @@ import managementStyles from '../../StandaloneManagement.module.css';
|
|
|
8
9
|
import styles from '../../../../Taskforce.module.css';
|
|
9
10
|
import { resolveCommercialLifecyclePresentation } from '../../../../utils/commercialLifecyclePresentation';
|
|
10
11
|
import { getPasswordPolicyGuidance, validatePasswordPolicy } from '../../../../shared/passwordPolicy';
|
|
11
|
-
export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, }) {
|
|
12
|
+
export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, supportMailtoHref, }) {
|
|
12
13
|
const lifecyclePresentation = resolveCommercialLifecyclePresentation(accountProfileSummary);
|
|
13
14
|
const addPasswordGuidance = getPasswordPolicyGuidance();
|
|
14
15
|
const billingRecoveryMessage = lifecyclePresentation.message;
|
|
@@ -104,5 +105,5 @@ export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi
|
|
|
104
105
|
? (workspaceCloudSyncEnabled
|
|
105
106
|
? `Enabled · ${syncStatusLabel}`
|
|
106
107
|
: 'Disabled')
|
|
107
|
-
: 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: `${authStyles.loginError} ${managementStyles.errorText}`, children: workspaceSyncError })), _jsx("div", { className: managementStyles.actionRowEnd, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || syncControlBusy, onClick: () => onToggleWorkspaceSync(!workspaceCloudSyncEnabled), children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: onOpenHelp, children: "Help &
|
|
108
|
+
: 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: `${authStyles.loginError} ${managementStyles.errorText}`, children: workspaceSyncError })), _jsx("div", { className: managementStyles.actionRowEnd, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || syncControlBusy, onClick: () => onToggleWorkspaceSync(!workspaceCloudSyncEnabled), children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: onOpenHelp, children: "Help & Support" }), _jsxs("a", { className: `${styles.cancelBtn} ${managementStyles.supportEmailBtn}`, href: supportMailtoHref, children: [_jsx(Mail, { size: 15 }), "Contact Support"] }), _jsx("button", { className: styles.submitBtn, onClick: onClose, children: "Close" })] })] }) }));
|
|
108
109
|
}
|
|
@@ -16,7 +16,7 @@ describe('AccountHubModal', () => {
|
|
|
16
16
|
message: 'Your latest checkout expired. Choose a plan to continue.',
|
|
17
17
|
billingConflictCode: 'CHECKOUT_SESSION_EXPIRED',
|
|
18
18
|
planSelectionRequired: true
|
|
19
|
-
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: onStartCheckout, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
19
|
+
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: onStartCheckout, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
20
20
|
expect(screen.getByText('Your previous checkout expired. Choose a plan to continue.')).toBeInTheDocument();
|
|
21
21
|
expect(screen.getByText('Pro Plan')).toBeInTheDocument();
|
|
22
22
|
const cta = screen.getByRole('button', { name: 'Choose Plan' });
|
|
@@ -30,21 +30,27 @@ describe('AccountHubModal', () => {
|
|
|
30
30
|
planName: 'Pro Plan',
|
|
31
31
|
entitlementState: 'suspended',
|
|
32
32
|
stripeCustomerId: 'cus_123'
|
|
33
|
-
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: onManageBilling, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
33
|
+
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: onManageBilling, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
34
34
|
expect(screen.getByText('Your access is suspended because billing needs attention. Update billing to restore access.')).toBeInTheDocument();
|
|
35
35
|
fireEvent.click(screen.getAllByRole('button', { name: 'Manage Billing' })[1]);
|
|
36
36
|
expect(onManageBilling).toHaveBeenCalledTimes(1);
|
|
37
37
|
});
|
|
38
38
|
it('shows trialing accounts as active without blocked recovery copy', () => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-04-20T12:00:00.000Z').getTime());
|
|
40
|
+
try {
|
|
41
|
+
render(_jsx(AccountHubModal, { isOpen: true, theme: "dark", runtimeMode: "cloud", authRequiredForApi: true, isAuthenticated: true, hasAuthIdentity: true, authIdentityLabel: "owner@example.com", billingLoading: false, billingError: null, billingActionError: null, billingNotice: null, billingActionBusy: false, billingIntervalChoice: "month", accountProfileSummary: {
|
|
42
|
+
planId: 'starter',
|
|
43
|
+
planName: 'Starter',
|
|
44
|
+
entitlementState: 'trialing',
|
|
45
|
+
stripeCustomerId: 'cus_123',
|
|
46
|
+
trialEnd: '2026-05-01T12:00:00.000Z'
|
|
47
|
+
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
48
|
+
expect(screen.getByText(/Your trial is active through/)).toBeInTheDocument();
|
|
49
|
+
expect(screen.queryByText('Your access is suspended because billing needs attention. Update billing to restore access.')).not.toBeInTheDocument();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
nowSpy.mockRestore();
|
|
53
|
+
}
|
|
48
54
|
});
|
|
49
55
|
it('shows an updating message instead of an expired active-trial date', () => {
|
|
50
56
|
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-04-20T12:00:00.000Z').getTime());
|
|
@@ -55,7 +61,7 @@ describe('AccountHubModal', () => {
|
|
|
55
61
|
entitlementState: 'trialing',
|
|
56
62
|
stripeCustomerId: 'cus_123',
|
|
57
63
|
trialEnd: '2026-04-12T16:09:09.000Z'
|
|
58
|
-
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
64
|
+
}, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
|
|
59
65
|
expect(screen.getByText('Your trial end date has passed. Billing status is updating.')).toBeInTheDocument();
|
|
60
66
|
expect(screen.queryByText(/Your trial is active through/)).not.toBeInTheDocument();
|
|
61
67
|
}
|
|
@@ -4,6 +4,8 @@ interface HelpModalProps {
|
|
|
4
4
|
theme: ModalThemeName;
|
|
5
5
|
onClose: () => void;
|
|
6
6
|
onOpenSettings: () => void;
|
|
7
|
+
onOpenAccountHub?: () => void;
|
|
8
|
+
supportMailtoHref: string;
|
|
7
9
|
}
|
|
8
|
-
export declare function HelpModal({ isOpen, theme, onClose, onOpenSettings, }: HelpModalProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function HelpModal({ isOpen, theme, onClose, onOpenSettings, onOpenAccountHub, supportMailtoHref, }: HelpModalProps): import("react/jsx-runtime").JSX.Element;
|
|
9
11
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Mail, Settings, UserCircle, Wrench } from 'lucide-react';
|
|
2
3
|
import { Modal } from '../../../ui/Modal';
|
|
3
4
|
import modalStyles from '../../../ui/Modal.module.css';
|
|
4
5
|
import managementStyles from '../../StandaloneManagement.module.css';
|
|
5
6
|
import styles from '../../../../Taskforce.module.css';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { TASKFORCE_SUPPORT_EMAIL } from '../../../../utils/supportContact';
|
|
8
|
+
export function HelpModal({ isOpen, theme, onClose, onOpenSettings, onOpenAccountHub, supportMailtoHref, }) {
|
|
9
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: "Help & Support", size: "md", theme: theme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${managementStyles.modalBody}`, children: [_jsxs("div", { className: managementStyles.helpIntro, children: [_jsx("p", { className: "tf-text-body", children: "Need a hand with Taskforce? Email support with what you were trying to do, what happened, and any workspace context that would help us reproduce it." }), _jsxs("a", { className: `${styles.submitBtn} ${managementStyles.supportEmailBtn}`, href: supportMailtoHref, children: [_jsx(Mail, { size: 16 }), "Email Support"] }), _jsxs("p", { className: "tf-text-helper", children: ["Opens your email client to ", TASKFORCE_SUPPORT_EMAIL, "."] })] }), _jsxs("div", { className: `${managementStyles.supportPanel} tf-surface-panel`, children: [_jsxs("div", { className: managementStyles.supportPanelHeader, children: [_jsx(Wrench, { size: 16 }), _jsxs("div", { children: [_jsx("h3", { className: "tf-heading-card", children: "Before you email" }), _jsx("p", { className: "tf-text-secondary", children: "These are the fastest places to check for common workspace, account, and setup issues." })] })] }), _jsxs("div", { className: managementStyles.supportRouteList, children: [_jsxs("button", { className: `${styles.cancelBtn} ${managementStyles.supportRouteBtn}`, onClick: onOpenSettings, children: [_jsx(Settings, { size: 15 }), "Open Settings"] }), onOpenAccountHub && (_jsxs("button", { className: `${styles.cancelBtn} ${managementStyles.supportRouteBtn}`, onClick: onOpenAccountHub, children: [_jsx(UserCircle, { size: 15 }), "Open Account Hub"] }))] })] }), _jsx("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: _jsx("button", { className: styles.cancelBtn, onClick: onClose, children: "Close" }) })] }) }));
|
|
8
10
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { TASKFORCE_SUPPORT_EMAIL } from '../../../../utils/supportContact';
|
|
5
|
+
import { HelpModal } from './HelpModal';
|
|
6
|
+
vi.mock('../../../ui/Modal', () => ({
|
|
7
|
+
Modal: ({ isOpen, children, title }) => (isOpen ? _jsxs("div", { children: [_jsx("h1", { children: title }), children] }) : null)
|
|
8
|
+
}));
|
|
9
|
+
describe('HelpModal', () => {
|
|
10
|
+
it('presents support actions instead of tutorial steps', () => {
|
|
11
|
+
const onOpenSettings = vi.fn();
|
|
12
|
+
const onOpenAccountHub = vi.fn();
|
|
13
|
+
render(_jsx(HelpModal, { isOpen: true, theme: "dark", onClose: () => { }, onOpenSettings: onOpenSettings, onOpenAccountHub: onOpenAccountHub, supportMailtoHref: `mailto:${TASKFORCE_SUPPORT_EMAIL}` }));
|
|
14
|
+
expect(screen.getByRole('heading', { name: 'Help & Support' })).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByRole('link', { name: 'Email Support' })).toHaveAttribute('href', `mailto:${TASKFORCE_SUPPORT_EMAIL}`);
|
|
16
|
+
expect(screen.queryByText('Close Tutorial')).not.toBeInTheDocument();
|
|
17
|
+
expect(screen.queryByText('Capture work')).not.toBeInTheDocument();
|
|
18
|
+
fireEvent.click(screen.getByRole('button', { name: 'Open Settings' }));
|
|
19
|
+
expect(onOpenSettings).toHaveBeenCalledTimes(1);
|
|
20
|
+
fireEvent.click(screen.getByRole('button', { name: 'Open Account Hub' }));
|
|
21
|
+
expect(onOpenAccountHub).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
});
|