@shipfox/client-onboarding 24.0.0 → 25.0.0
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-type.log +0 -1
- package/CHANGELOG.md +18 -0
- package/README.md +26 -7
- package/dist/components/setup-checklist-body.d.ts +3 -0
- package/dist/components/setup-checklist-body.d.ts.map +1 -0
- package/dist/components/setup-checklist-body.js +25 -0
- package/dist/components/setup-checklist-body.js.map +1 -0
- package/dist/components/setup-checklist-completion.d.ts +6 -0
- package/dist/components/setup-checklist-completion.d.ts.map +1 -0
- package/dist/components/setup-checklist-completion.js +156 -0
- package/dist/components/setup-checklist-completion.js.map +1 -0
- package/dist/components/setup-checklist-confetti.d.ts +13 -0
- package/dist/components/setup-checklist-confetti.d.ts.map +1 -0
- package/dist/components/setup-checklist-confetti.js +24 -0
- package/dist/components/setup-checklist-confetti.js.map +1 -0
- package/dist/components/setup-checklist-host-primitives.d.ts +11 -0
- package/dist/components/setup-checklist-host-primitives.d.ts.map +1 -0
- package/dist/components/setup-checklist-host-primitives.js +71 -0
- package/dist/components/setup-checklist-host-primitives.js.map +1 -0
- package/dist/components/setup-checklist-row.d.ts +7 -0
- package/dist/components/setup-checklist-row.d.ts.map +1 -0
- package/dist/components/setup-checklist-row.js +177 -0
- package/dist/components/setup-checklist-row.js.map +1 -0
- package/dist/components/setup-checklist-types.d.ts +19 -0
- package/dist/components/setup-checklist-types.d.ts.map +1 -0
- package/dist/components/setup-checklist-types.js +3 -0
- package/dist/components/setup-checklist-types.js.map +1 -0
- package/dist/components/setup-checklist.d.ts +5 -0
- package/dist/components/setup-checklist.d.ts.map +1 -0
- package/dist/components/setup-checklist.js +5 -0
- package/dist/components/setup-checklist.js.map +1 -0
- package/dist/components/setup-checklist.stories.d.ts +19 -0
- package/dist/components/setup-checklist.stories.d.ts.map +1 -0
- package/dist/components/setup-checklist.stories.js +305 -0
- package/dist/components/setup-checklist.stories.js.map +1 -0
- package/dist/components/workspace-setup-checklist.d.ts +3 -0
- package/dist/components/workspace-setup-checklist.d.ts.map +1 -0
- package/dist/components/workspace-setup-checklist.js +80 -0
- package/dist/components/workspace-setup-checklist.js.map +1 -0
- package/dist/components/workspace-setup-indicator.d.ts +3 -0
- package/dist/components/workspace-setup-indicator.d.ts.map +1 -0
- package/dist/components/workspace-setup-indicator.js +133 -0
- package/dist/components/workspace-setup-indicator.js.map +1 -0
- package/dist/core/setup-checklist.d.ts +2 -0
- package/dist/core/setup-checklist.d.ts.map +1 -1
- package/dist/core/setup-checklist.js +11 -6
- package/dist/core/setup-checklist.js.map +1 -1
- package/dist/feature.d.ts +6 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +19 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/setup-checklist.d.ts +13 -0
- package/dist/hooks/api/setup-checklist.d.ts.map +1 -0
- package/dist/hooks/api/setup-checklist.js +103 -0
- package/dist/hooks/api/setup-checklist.js.map +1 -0
- package/dist/hooks/use-checklist-analytics.d.ts +6 -0
- package/dist/hooks/use-checklist-analytics.d.ts.map +1 -0
- package/dist/hooks/use-checklist-analytics.js +51 -0
- package/dist/hooks/use-checklist-analytics.js.map +1 -0
- package/dist/hooks/use-checklist-dismissal.d.ts +5 -0
- package/dist/hooks/use-checklist-dismissal.d.ts.map +1 -0
- package/dist/hooks/use-checklist-dismissal.js +31 -0
- package/dist/hooks/use-checklist-dismissal.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -5
- package/src/components/setup-checklist-body.tsx +35 -0
- package/src/components/setup-checklist-completion.test.tsx +124 -0
- package/src/components/setup-checklist-completion.tsx +165 -0
- package/src/components/setup-checklist-confetti.test.ts +19 -0
- package/src/components/setup-checklist-confetti.ts +39 -0
- package/src/components/setup-checklist-host-primitives.tsx +65 -0
- package/src/components/setup-checklist-row.tsx +189 -0
- package/src/components/setup-checklist-types.ts +21 -0
- package/src/components/setup-checklist.stories.tsx +281 -0
- package/src/components/setup-checklist.test.tsx +564 -0
- package/src/components/setup-checklist.tsx +8 -0
- package/src/components/workspace-setup-checklist.tsx +91 -0
- package/src/components/workspace-setup-indicator.tsx +126 -0
- package/src/core/setup-checklist.test.ts +2 -2
- package/src/core/setup-checklist.ts +19 -10
- package/src/feature.ts +16 -0
- package/src/hooks/api/setup-checklist.ts +144 -0
- package/src/hooks/use-checklist-analytics.ts +60 -0
- package/src/hooks/use-checklist-dismissal.ts +30 -0
- package/src/index.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {useClientAnalytics, useMaybeActiveWorkspace} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
3
|
+
import {Icon} from '@shipfox/react-ui/icon';
|
|
4
|
+
import {Panel, PanelBody} from '@shipfox/react-ui/panel';
|
|
5
|
+
import {Popover, PopoverContent, PopoverTrigger} from '@shipfox/react-ui/popover';
|
|
6
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
7
|
+
import {useCallback, useId, useState} from 'react';
|
|
8
|
+
import type {SetupChecklistItem} from '#core/setup-checklist.js';
|
|
9
|
+
import {useSetupChecklistQueryState} from '#hooks/api/setup-checklist.js';
|
|
10
|
+
import {useCompletionTransition, useShownAnalytics} from '#hooks/use-checklist-analytics.js';
|
|
11
|
+
import {useChecklistDismissal} from '#hooks/use-checklist-dismissal.js';
|
|
12
|
+
import {SetupChecklistBody} from './setup-checklist-body.js';
|
|
13
|
+
import {ChecklistDismissAction, checklistCountLabel} from './setup-checklist-host-primitives.js';
|
|
14
|
+
import type {WorkspaceReference, WorkspaceSetupHostProps} from './setup-checklist-types.js';
|
|
15
|
+
|
|
16
|
+
export function WorkspaceSetupIndicator(props: WorkspaceSetupHostProps = {}) {
|
|
17
|
+
if (props.workspace) {
|
|
18
|
+
return (
|
|
19
|
+
<WorkspaceSetupIndicatorForWorkspace key={props.workspace.id} workspace={props.workspace} />
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <WorkspaceSetupIndicatorFromShell />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function WorkspaceSetupIndicatorFromShell() {
|
|
27
|
+
const workspace = useMaybeActiveWorkspace();
|
|
28
|
+
return workspace ? (
|
|
29
|
+
<WorkspaceSetupIndicatorForWorkspace key={workspace.id} workspace={workspace} />
|
|
30
|
+
) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function WorkspaceSetupIndicatorForWorkspace({workspace}: {workspace: WorkspaceReference}) {
|
|
34
|
+
const dismissal = useChecklistDismissal(workspace.id);
|
|
35
|
+
const queryState = useSetupChecklistQueryState(workspace.id, !dismissal.dismissed);
|
|
36
|
+
const analytics = useClientAnalytics();
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const [pulseKey, setPulseKey] = useState(0);
|
|
39
|
+
const [burstPending, setBurstPending] = useState(false);
|
|
40
|
+
const handleCompleted = useCallback(
|
|
41
|
+
(completed: boolean) => {
|
|
42
|
+
if (!completed) return;
|
|
43
|
+
setBurstPending(true);
|
|
44
|
+
if (!open) setPulseKey((key) => key + 1);
|
|
45
|
+
},
|
|
46
|
+
[open],
|
|
47
|
+
);
|
|
48
|
+
const showCompletion = useCompletionTransition(queryState, 'popover', handleCompleted);
|
|
49
|
+
const consumeBurst = useCallback(() => setBurstPending(false), []);
|
|
50
|
+
const triggerId = useId();
|
|
51
|
+
|
|
52
|
+
const dismiss = useCallback(() => {
|
|
53
|
+
dismissal.dismiss();
|
|
54
|
+
setOpen(false);
|
|
55
|
+
analytics.capture('onboarding_checklist_dismissed', {host: 'popover'});
|
|
56
|
+
}, [analytics, dismissal]);
|
|
57
|
+
const handleAction = useCallback(
|
|
58
|
+
(item: SetupChecklistItem) => {
|
|
59
|
+
analytics.capture('onboarding_checklist_row_clicked', {row_id: item.id});
|
|
60
|
+
setOpen(false);
|
|
61
|
+
},
|
|
62
|
+
[analytics],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const indicatorReady = queryState.completionReady || queryState.checklist.openCount > 0;
|
|
66
|
+
const isVisible =
|
|
67
|
+
!dismissal.dismissed &&
|
|
68
|
+
queryState.baseSettled &&
|
|
69
|
+
indicatorReady &&
|
|
70
|
+
(!queryState.checklist.complete || showCompletion);
|
|
71
|
+
useShownAnalytics('popover', isVisible);
|
|
72
|
+
|
|
73
|
+
if (dismissal.dismissed || !queryState.baseSettled || !indicatorReady) return null;
|
|
74
|
+
if (queryState.checklist.complete && !showCompletion) return null;
|
|
75
|
+
|
|
76
|
+
const countLabel = checklistCountLabel(queryState.checklist);
|
|
77
|
+
const ariaLabel = `Get started, ${countLabel}`;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Popover modal={false} open={open} onOpenChange={setOpen}>
|
|
81
|
+
<PopoverTrigger asChild>
|
|
82
|
+
<Button
|
|
83
|
+
type="button"
|
|
84
|
+
variant="transparent"
|
|
85
|
+
size="sm"
|
|
86
|
+
id={triggerId}
|
|
87
|
+
aria-label={ariaLabel}
|
|
88
|
+
className="gap-inline"
|
|
89
|
+
>
|
|
90
|
+
<span
|
|
91
|
+
key={pulseKey}
|
|
92
|
+
className={cn(
|
|
93
|
+
'inline-flex items-center',
|
|
94
|
+
pulseKey > 0 && 'motion-safe:animate-[pulse_1s_ease-in-out_1]',
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<Icon name="circleDottedLine" size={16} aria-hidden="true" />
|
|
98
|
+
</span>
|
|
99
|
+
<span>Get started · {countLabel}</span>
|
|
100
|
+
</Button>
|
|
101
|
+
</PopoverTrigger>
|
|
102
|
+
<PopoverContent
|
|
103
|
+
align="end"
|
|
104
|
+
className="w-[360px] max-w-[calc(100vw-32px)] p-0"
|
|
105
|
+
aria-labelledby={triggerId}
|
|
106
|
+
>
|
|
107
|
+
<Panel asChild>
|
|
108
|
+
<section aria-label="Get started">
|
|
109
|
+
<PanelBody>
|
|
110
|
+
<SetupChecklistBody
|
|
111
|
+
checklist={queryState.checklist}
|
|
112
|
+
workspaceSlug={workspace.slug}
|
|
113
|
+
completion={showCompletion}
|
|
114
|
+
showBurst={burstPending}
|
|
115
|
+
onBurstComplete={consumeBurst}
|
|
116
|
+
onAction={handleAction}
|
|
117
|
+
onDone={dismiss}
|
|
118
|
+
/>
|
|
119
|
+
</PanelBody>
|
|
120
|
+
<ChecklistDismissAction onDismiss={dismiss} />
|
|
121
|
+
</section>
|
|
122
|
+
</Panel>
|
|
123
|
+
</PopoverContent>
|
|
124
|
+
</Popover>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -277,6 +277,7 @@ describe('deriveSetupChecklist', () => {
|
|
|
277
277
|
expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
|
|
278
278
|
status: 'done',
|
|
279
279
|
title: 'Connect your tools',
|
|
280
|
+
attention: false,
|
|
280
281
|
});
|
|
281
282
|
});
|
|
282
283
|
|
|
@@ -284,8 +285,7 @@ describe('deriveSetupChecklist', () => {
|
|
|
284
285
|
const checklist = deriveSetupChecklist(input());
|
|
285
286
|
|
|
286
287
|
expect(checklist.items.find((item) => item.id === 'tools')).toMatchObject({
|
|
287
|
-
purpose:
|
|
288
|
-
'Connect issue tracking, messaging, observability, or any Shipfox integration so workflows can react to events and agents can use them',
|
|
288
|
+
purpose: 'Connect issue tracking, messaging, observability, or any Shipfox integration',
|
|
289
289
|
action: {href: '/settings/integrations'},
|
|
290
290
|
});
|
|
291
291
|
});
|
|
@@ -22,6 +22,8 @@ export interface SetupChecklistItem {
|
|
|
22
22
|
status: SetupChecklistItemStatus;
|
|
23
23
|
/** Tracked items count toward completion; pointers never do. */
|
|
24
24
|
tracked: boolean;
|
|
25
|
+
/** The row's underlying integration has a connection that needs attention. */
|
|
26
|
+
attention?: boolean;
|
|
25
27
|
/** One line of purpose, rendered for open and pointer rows. */
|
|
26
28
|
purpose?: string;
|
|
27
29
|
/** Primary action, a link to the settings page where the work happens. */
|
|
@@ -49,7 +51,7 @@ export interface SetupChecklistInput {
|
|
|
49
51
|
|
|
50
52
|
const TOOLS_TITLE = 'Connect your tools';
|
|
51
53
|
const TOOLS_PURPOSE =
|
|
52
|
-
'Connect issue tracking, messaging, observability, or any Shipfox integration
|
|
54
|
+
'Connect issue tracking, messaging, observability, or any Shipfox integration';
|
|
53
55
|
const RUNNER_PURPOSE = 'Jobs wait in `pending` until a runner is online';
|
|
54
56
|
const MODEL_PROVIDER_PURPOSE = 'Agent steps need a model provider to run';
|
|
55
57
|
const FIRST_WORKFLOW_PURPOSE =
|
|
@@ -69,6 +71,8 @@ export function deriveSetupChecklist({
|
|
|
69
71
|
modelProvider,
|
|
70
72
|
membership,
|
|
71
73
|
}: SetupChecklistInput): SetupChecklist {
|
|
74
|
+
const toolsAttention =
|
|
75
|
+
!readiness.hasToolIntegration && attentionToolProviders(readiness).length > 0;
|
|
72
76
|
const items: SetupChecklistItem[] = [
|
|
73
77
|
{id: 'source-control', title: 'Connect source control', status: 'done', tracked: true},
|
|
74
78
|
{id: 'project', title: 'Create a project', status: 'done', tracked: true},
|
|
@@ -77,6 +81,7 @@ export function deriveSetupChecklist({
|
|
|
77
81
|
title: toolsTitle(readiness),
|
|
78
82
|
status: readiness.hasToolIntegration ? 'done' : 'open',
|
|
79
83
|
tracked: true,
|
|
84
|
+
attention: toolsAttention,
|
|
80
85
|
purpose: TOOLS_PURPOSE,
|
|
81
86
|
action: {label: 'Connect', href: '/settings/integrations'},
|
|
82
87
|
},
|
|
@@ -137,20 +142,24 @@ export function deriveSetupChecklist({
|
|
|
137
142
|
|
|
138
143
|
function toolsTitle(readiness: WorkspaceIntegrationReadiness): string {
|
|
139
144
|
if (!readiness.hasToolIntegration) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(provider) => provider !== undefined && !provider.capabilities.includes('source_control'),
|
|
144
|
-
);
|
|
145
|
-
if (attentionToolProviders.length === 1) {
|
|
146
|
-
const provider = attentionToolProviders[0];
|
|
145
|
+
const providers = attentionToolProviders(readiness);
|
|
146
|
+
if (providers.length === 1) {
|
|
147
|
+
const provider = providers[0];
|
|
147
148
|
if (provider !== undefined) {
|
|
148
149
|
return `${provider.displayName || provider.provider} needs attention`;
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
|
-
if (
|
|
152
|
-
return `${
|
|
152
|
+
if (providers.length > 1) {
|
|
153
|
+
return `${providers.length} integrations need attention`;
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
return TOOLS_TITLE;
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
function attentionToolProviders(readiness: WorkspaceIntegrationReadiness) {
|
|
160
|
+
return readiness.attentionProviders
|
|
161
|
+
.map((providerKey) => readiness.providers.find(({provider}) => provider === providerKey))
|
|
162
|
+
.filter(
|
|
163
|
+
(provider) => provider !== undefined && !provider.capabilities.includes('source_control'),
|
|
164
|
+
);
|
|
165
|
+
}
|
package/src/feature.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {defineClientFeature} from '@shipfox/client-shell';
|
|
2
|
+
import {lazy} from 'react';
|
|
3
|
+
|
|
4
|
+
export const onboardingFeature = defineClientFeature({
|
|
5
|
+
id: 'shipfox.onboarding',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const WorkspaceSetupChecklist = lazy(async () => {
|
|
9
|
+
const module = await import('./components/setup-checklist.js');
|
|
10
|
+
return {default: module.WorkspaceSetupChecklist};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const WorkspaceSetupIndicator = lazy(async () => {
|
|
14
|
+
const module = await import('./components/setup-checklist.js');
|
|
15
|
+
return {default: module.WorkspaceSetupIndicator};
|
|
16
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
modelProviderCatalogQueryOptions,
|
|
3
|
+
modelProviderConfigsQueryOptions,
|
|
4
|
+
} from '@shipfox/client-agent';
|
|
5
|
+
import {
|
|
6
|
+
integrationConnectionsQueryOptions,
|
|
7
|
+
integrationProvidersQueryOptions,
|
|
8
|
+
} from '@shipfox/client-integrations';
|
|
9
|
+
import {
|
|
10
|
+
activeProvisionersQueryOptions,
|
|
11
|
+
installationRunnersStatusQueryOptions,
|
|
12
|
+
} from '@shipfox/client-runners';
|
|
13
|
+
import {
|
|
14
|
+
listInvitationsQueryOptions,
|
|
15
|
+
listMembersQueryOptions,
|
|
16
|
+
} from '@shipfox/client-workspace-settings';
|
|
17
|
+
import {useQuery} from '@tanstack/react-query';
|
|
18
|
+
import {deriveIntegrationReadiness} from '#core/integration-readiness.js';
|
|
19
|
+
import {
|
|
20
|
+
deriveSetupChecklist,
|
|
21
|
+
type SetupChecklist,
|
|
22
|
+
type SetupChecklistItemId,
|
|
23
|
+
} from '#core/setup-checklist.js';
|
|
24
|
+
|
|
25
|
+
const CHECKLIST_STALE_TIME_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
export interface ChecklistQueryState {
|
|
28
|
+
checklist: SetupChecklist;
|
|
29
|
+
baseSettled: boolean;
|
|
30
|
+
completionReady: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Composes the server state needed by both checklist hosts. Keeping this hook
|
|
35
|
+
* in the API adapter boundary gives each query family one shared cache policy
|
|
36
|
+
* while allowing dismissed hosts to remain unsubscribed.
|
|
37
|
+
*/
|
|
38
|
+
export function useSetupChecklistQueryState(
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
subscribed: boolean,
|
|
41
|
+
): ChecklistQueryState {
|
|
42
|
+
const queryEnabled = subscribed && Boolean(workspaceId);
|
|
43
|
+
const queryPolicy = {
|
|
44
|
+
enabled: queryEnabled,
|
|
45
|
+
subscribed: queryEnabled,
|
|
46
|
+
refetchInterval: false,
|
|
47
|
+
retry: false,
|
|
48
|
+
staleTime: CHECKLIST_STALE_TIME_MS,
|
|
49
|
+
refetchOnWindowFocus: false,
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
const providersQuery = useQuery({
|
|
53
|
+
...integrationProvidersQueryOptions(),
|
|
54
|
+
...queryPolicy,
|
|
55
|
+
});
|
|
56
|
+
const connectionsQuery = useQuery({
|
|
57
|
+
...integrationConnectionsQueryOptions(workspaceId),
|
|
58
|
+
...queryPolicy,
|
|
59
|
+
});
|
|
60
|
+
const activeProvisionersQuery = useQuery({
|
|
61
|
+
...activeProvisionersQueryOptions(workspaceId),
|
|
62
|
+
...queryPolicy,
|
|
63
|
+
});
|
|
64
|
+
const runnersStatusQuery = useQuery({
|
|
65
|
+
...installationRunnersStatusQueryOptions(workspaceId),
|
|
66
|
+
...queryPolicy,
|
|
67
|
+
});
|
|
68
|
+
const catalogQuery = useQuery({
|
|
69
|
+
...modelProviderCatalogQueryOptions(),
|
|
70
|
+
...queryPolicy,
|
|
71
|
+
});
|
|
72
|
+
const configsQuery = useQuery({
|
|
73
|
+
...modelProviderConfigsQueryOptions(workspaceId),
|
|
74
|
+
...queryPolicy,
|
|
75
|
+
});
|
|
76
|
+
const membersQuery = useQuery({
|
|
77
|
+
...listMembersQueryOptions(workspaceId),
|
|
78
|
+
...queryPolicy,
|
|
79
|
+
});
|
|
80
|
+
const invitationsQuery = useQuery({
|
|
81
|
+
...listInvitationsQueryOptions(workspaceId),
|
|
82
|
+
...queryPolicy,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const baseSettled = isSettled(providersQuery) && isSettled(connectionsQuery);
|
|
86
|
+
const runnerSettled = isSettled(activeProvisionersQuery) && isSettled(runnersStatusQuery);
|
|
87
|
+
const runnerReady = activeProvisionersQuery.isSuccess && runnersStatusQuery.isSuccess;
|
|
88
|
+
const catalogInstallationProvided =
|
|
89
|
+
catalogQuery.data !== undefined &&
|
|
90
|
+
(catalogQuery.data.managedProviderId !== null ||
|
|
91
|
+
catalogQuery.data.instanceDefaultProviderId !== null);
|
|
92
|
+
const modelReady = catalogQuery.isSuccess && configsQuery.isSuccess;
|
|
93
|
+
const membersReady = membersQuery.isSuccess && invitationsQuery.isSuccess;
|
|
94
|
+
const modelSettled = isSettled(catalogQuery) && isSettled(configsQuery);
|
|
95
|
+
const membersSettled = isSettled(membersQuery) && isSettled(invitationsQuery);
|
|
96
|
+
const completionReady =
|
|
97
|
+
providersQuery.isSuccess &&
|
|
98
|
+
connectionsQuery.isSuccess &&
|
|
99
|
+
runnerSettled &&
|
|
100
|
+
modelSettled &&
|
|
101
|
+
membersSettled;
|
|
102
|
+
|
|
103
|
+
const rawChecklist = deriveSetupChecklist({
|
|
104
|
+
readiness: deriveIntegrationReadiness({
|
|
105
|
+
providers: providersQuery.data ?? [],
|
|
106
|
+
connections: connectionsQuery.data ?? [],
|
|
107
|
+
}),
|
|
108
|
+
installationRunners: runnersStatusQuery.data ?? 'managed',
|
|
109
|
+
workspaceRunnerCapacity: (activeProvisionersQuery.data?.length ?? 0) > 0,
|
|
110
|
+
modelProvider: {
|
|
111
|
+
installationProvided: catalogQuery.isSuccess ? catalogInstallationProvided : true,
|
|
112
|
+
configured: (configsQuery.data?.configs.length ?? 0) > 0,
|
|
113
|
+
},
|
|
114
|
+
membership: {
|
|
115
|
+
memberCount: membersQuery.data?.length ?? 0,
|
|
116
|
+
pendingInvitationCount: invitationsQuery.data?.length ?? 0,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const hiddenRows = new Set<SetupChecklistItemId>();
|
|
121
|
+
if (!providersQuery.isSuccess || !connectionsQuery.isSuccess) hiddenRows.add('tools');
|
|
122
|
+
if (!runnerReady) hiddenRows.add('runner');
|
|
123
|
+
if (!modelReady) hiddenRows.add('model-provider');
|
|
124
|
+
if (!membersReady) hiddenRows.add('teammates');
|
|
125
|
+
|
|
126
|
+
const items = rawChecklist.items.filter((item) => !hiddenRows.has(item.id));
|
|
127
|
+
const trackedItems = items.filter((item) => item.tracked);
|
|
128
|
+
const openCount = trackedItems.filter((item) => item.status === 'open').length;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
baseSettled,
|
|
132
|
+
completionReady,
|
|
133
|
+
checklist: {
|
|
134
|
+
items,
|
|
135
|
+
openCount,
|
|
136
|
+
trackedCount: trackedItems.length,
|
|
137
|
+
complete: completionReady && openCount === 0,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isSettled(query: {isError: boolean; isSuccess: boolean}) {
|
|
143
|
+
return query.isSuccess || query.isError;
|
|
144
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {useClientAnalytics} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {useEffect, useRef, useState} from 'react';
|
|
3
|
+
import type {ChecklistQueryState} from '#hooks/api/setup-checklist.js';
|
|
4
|
+
|
|
5
|
+
type ChecklistHost = 'panel' | 'popover';
|
|
6
|
+
|
|
7
|
+
export function useCompletionTransition(
|
|
8
|
+
queryState: ChecklistQueryState,
|
|
9
|
+
host: ChecklistHost,
|
|
10
|
+
onCompleted?: (completed: boolean) => void,
|
|
11
|
+
) {
|
|
12
|
+
const analytics = useClientAnalytics();
|
|
13
|
+
const observedIncomplete = useRef(false);
|
|
14
|
+
const completionHandled = useRef(false);
|
|
15
|
+
const [showCompletion, setShowCompletion] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (
|
|
19
|
+
queryState.baseSettled &&
|
|
20
|
+
!queryState.checklist.complete &&
|
|
21
|
+
queryState.checklist.openCount > 0
|
|
22
|
+
) {
|
|
23
|
+
observedIncomplete.current = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!queryState.completionReady) return;
|
|
27
|
+
|
|
28
|
+
if (!queryState.checklist.complete) {
|
|
29
|
+
setShowCompletion(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!observedIncomplete.current || completionHandled.current) return;
|
|
34
|
+
completionHandled.current = true;
|
|
35
|
+
setShowCompletion(true);
|
|
36
|
+
onCompleted?.(true);
|
|
37
|
+
analytics.capture('onboarding_checklist_completed', {host});
|
|
38
|
+
}, [
|
|
39
|
+
analytics,
|
|
40
|
+
host,
|
|
41
|
+
onCompleted,
|
|
42
|
+
queryState.baseSettled,
|
|
43
|
+
queryState.checklist.complete,
|
|
44
|
+
queryState.checklist.openCount,
|
|
45
|
+
queryState.completionReady,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
return showCompletion;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useShownAnalytics(host: ChecklistHost, visible: boolean) {
|
|
52
|
+
const analytics = useClientAnalytics();
|
|
53
|
+
const shown = useRef(false);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!visible || shown.current) return;
|
|
57
|
+
shown.current = true;
|
|
58
|
+
analytics.capture('onboarding_checklist_shown', {host});
|
|
59
|
+
}, [analytics, host, visible]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dismissWorkspaceSetupChecklist,
|
|
3
|
+
isWorkspaceSetupChecklistDismissed,
|
|
4
|
+
WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT,
|
|
5
|
+
} from '@shipfox/client-shell/runtime';
|
|
6
|
+
import {useCallback, useEffect, useState} from 'react';
|
|
7
|
+
|
|
8
|
+
export function useChecklistDismissal(workspaceId: string) {
|
|
9
|
+
const [dismissed, setDismissed] = useState(() => isWorkspaceSetupChecklistDismissed(workspaceId));
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const refresh = () => setDismissed(isWorkspaceSetupChecklistDismissed(workspaceId));
|
|
13
|
+
refresh();
|
|
14
|
+
window.addEventListener('storage', refresh);
|
|
15
|
+
window.addEventListener('focus', refresh);
|
|
16
|
+
window.addEventListener(WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT, refresh);
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener('storage', refresh);
|
|
19
|
+
window.removeEventListener('focus', refresh);
|
|
20
|
+
window.removeEventListener(WORKSPACE_SETUP_CHECKLIST_DISMISSAL_EVENT, refresh);
|
|
21
|
+
};
|
|
22
|
+
}, [workspaceId]);
|
|
23
|
+
|
|
24
|
+
const dismiss = useCallback(() => {
|
|
25
|
+
dismissWorkspaceSetupChecklist(workspaceId);
|
|
26
|
+
setDismissed(true);
|
|
27
|
+
}, [workspaceId]);
|
|
28
|
+
|
|
29
|
+
return {dismissed, dismiss};
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,14 @@ export {
|
|
|
13
13
|
type SetupChecklistItemId,
|
|
14
14
|
type SetupChecklistItemStatus,
|
|
15
15
|
} from '#core/setup-checklist.js';
|
|
16
|
+
export {
|
|
17
|
+
SetupChecklistBody,
|
|
18
|
+
type SetupChecklistBodyProps,
|
|
19
|
+
type WorkspaceReference,
|
|
20
|
+
WorkspaceSetupChecklist,
|
|
21
|
+
type WorkspaceSetupHostProps,
|
|
22
|
+
WorkspaceSetupIndicator,
|
|
23
|
+
} from './components/setup-checklist.js';
|
|
16
24
|
export {
|
|
17
25
|
loadWorkspaceSetupRoute,
|
|
18
26
|
type WorkspaceSetupRouteOptions,
|