@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,564 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import '@testing-library/jest-dom/vitest';
|
|
3
|
+
import {modelProviderQueryKeys} from '@shipfox/client-agent';
|
|
4
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
5
|
+
import {
|
|
6
|
+
type IntegrationConnection,
|
|
7
|
+
type IntegrationProvider,
|
|
8
|
+
integrationConnectionsQueryOptions,
|
|
9
|
+
integrationProvidersQueryOptions,
|
|
10
|
+
} from '@shipfox/client-integrations';
|
|
11
|
+
import {provisionerTokenQueryKeys} from '@shipfox/client-runners';
|
|
12
|
+
import {
|
|
13
|
+
type ClientAnalytics,
|
|
14
|
+
ClientAnalyticsProvider,
|
|
15
|
+
clearWorkspaceSetupChecklistDismissal,
|
|
16
|
+
dismissWorkspaceSetupChecklist,
|
|
17
|
+
} from '@shipfox/client-shell/runtime';
|
|
18
|
+
import {listInvitationsQueryKey, listMembersQueryKey} from '@shipfox/client-workspace-settings';
|
|
19
|
+
import {afterEach, beforeEach, describe, expect, test, vi} from '@shipfox/vitest/vi';
|
|
20
|
+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
21
|
+
import {
|
|
22
|
+
createMemoryHistory,
|
|
23
|
+
createRootRoute,
|
|
24
|
+
createRoute,
|
|
25
|
+
createRouter,
|
|
26
|
+
Outlet,
|
|
27
|
+
RouterProvider,
|
|
28
|
+
} from '@tanstack/react-router';
|
|
29
|
+
import {act, cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
|
30
|
+
import {deriveIntegrationReadiness} from '#core/integration-readiness.js';
|
|
31
|
+
import {deriveSetupChecklist} from '#core/setup-checklist.js';
|
|
32
|
+
import {
|
|
33
|
+
SetupChecklistBody,
|
|
34
|
+
type WorkspaceReference,
|
|
35
|
+
WorkspaceSetupChecklist,
|
|
36
|
+
WorkspaceSetupIndicator,
|
|
37
|
+
} from './setup-checklist.js';
|
|
38
|
+
|
|
39
|
+
const WORKSPACE: WorkspaceReference = {id: 'test-workspace', slug: 'acme'};
|
|
40
|
+
const GET_STARTED_BUTTON_RE = /Get started/u;
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const githubProvider: IntegrationProvider = {
|
|
43
|
+
provider: 'github',
|
|
44
|
+
displayName: 'GitHub',
|
|
45
|
+
capabilities: ['source_control'],
|
|
46
|
+
};
|
|
47
|
+
const linearProvider: IntegrationProvider = {
|
|
48
|
+
provider: 'linear',
|
|
49
|
+
displayName: 'Linear',
|
|
50
|
+
capabilities: ['agent_tools'],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function connection(
|
|
54
|
+
provider: 'github' | 'linear',
|
|
55
|
+
lifecycleStatus: IntegrationConnection['lifecycleStatus'],
|
|
56
|
+
): IntegrationConnection {
|
|
57
|
+
return {
|
|
58
|
+
id: `${provider}-connection`,
|
|
59
|
+
workspaceId: WORKSPACE.id,
|
|
60
|
+
provider,
|
|
61
|
+
externalAccountId: `${provider}-account`,
|
|
62
|
+
slug: `${provider}-account`,
|
|
63
|
+
displayName: provider === 'github' ? 'GitHub' : 'Linear',
|
|
64
|
+
lifecycleStatus,
|
|
65
|
+
capabilities: provider === 'github' ? ['source_control'] : ['agent_tools'],
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createQueryClient() {
|
|
72
|
+
return new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pendingResponse(): Promise<Response> {
|
|
76
|
+
return new Promise<Response>((resolve) => {
|
|
77
|
+
void resolve;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function seedQueries(queryClient: QueryClient, toolsConnected = false) {
|
|
82
|
+
queryClient.setQueryData(integrationProvidersQueryOptions().queryKey, [
|
|
83
|
+
githubProvider,
|
|
84
|
+
linearProvider,
|
|
85
|
+
]);
|
|
86
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
87
|
+
connection('github', 'active'),
|
|
88
|
+
...(toolsConnected ? [connection('linear', 'active')] : []),
|
|
89
|
+
]);
|
|
90
|
+
queryClient.setQueryData(provisionerTokenQueryKeys.active(WORKSPACE.id), {
|
|
91
|
+
provisioners: [],
|
|
92
|
+
installationRunners: 'managed' as const,
|
|
93
|
+
});
|
|
94
|
+
queryClient.setQueryData(modelProviderQueryKeys.catalog(), {
|
|
95
|
+
providers: [],
|
|
96
|
+
workspaceProviders: 'enabled' as const,
|
|
97
|
+
managedProviderId: 'managed-default',
|
|
98
|
+
instanceDefaultProviderId: null,
|
|
99
|
+
});
|
|
100
|
+
queryClient.setQueryData(modelProviderQueryKeys.configs(WORKSPACE.id), {
|
|
101
|
+
configs: [],
|
|
102
|
+
defaultHarnessId: null,
|
|
103
|
+
defaultProviderId: null,
|
|
104
|
+
});
|
|
105
|
+
queryClient.setQueryData(listMembersQueryKey(WORKSPACE.id), [
|
|
106
|
+
{
|
|
107
|
+
id: 'member-1',
|
|
108
|
+
userId: 'user-1',
|
|
109
|
+
workspaceId: WORKSPACE.id,
|
|
110
|
+
email: 'you@example.com',
|
|
111
|
+
name: 'You',
|
|
112
|
+
role: 'admin' as const,
|
|
113
|
+
joinedAt: now,
|
|
114
|
+
updatedAt: now,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
queryClient.setQueryData(listInvitationsQueryKey(WORKSPACE.id), []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderWithProviders(
|
|
121
|
+
element: React.ReactElement,
|
|
122
|
+
queryClient: QueryClient,
|
|
123
|
+
analytics: ClientAnalytics,
|
|
124
|
+
) {
|
|
125
|
+
const rootRoute = createRootRoute({component: Outlet});
|
|
126
|
+
const routePaths = [
|
|
127
|
+
'/w/$workspaceSlug',
|
|
128
|
+
'/w/$workspaceSlug/settings/integrations',
|
|
129
|
+
'/w/$workspaceSlug/settings/runners',
|
|
130
|
+
'/w/$workspaceSlug/settings/agents',
|
|
131
|
+
'/w/$workspaceSlug/settings/members',
|
|
132
|
+
];
|
|
133
|
+
const routes = routePaths.map((path) =>
|
|
134
|
+
createRoute({
|
|
135
|
+
getParentRoute: () => rootRoute,
|
|
136
|
+
path,
|
|
137
|
+
component: () => element,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
const router = createRouter({
|
|
141
|
+
routeTree: rootRoute.addChildren(routes),
|
|
142
|
+
history: createMemoryHistory({initialEntries: ['/w/acme']}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return render(
|
|
146
|
+
<ClientAnalyticsProvider analytics={analytics}>
|
|
147
|
+
<QueryClientProvider client={queryClient}>
|
|
148
|
+
<RouterProvider router={router} />
|
|
149
|
+
</QueryClientProvider>
|
|
150
|
+
</ClientAnalyticsProvider>,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('SetupChecklistBody', () => {
|
|
155
|
+
test('renders ordered statuses, purposes, and actions', async () => {
|
|
156
|
+
const checklist = deriveSetupChecklist({
|
|
157
|
+
readiness: deriveIntegrationReadiness({
|
|
158
|
+
providers: [githubProvider, linearProvider],
|
|
159
|
+
connections: [connection('github', 'active')],
|
|
160
|
+
}),
|
|
161
|
+
installationRunners: 'none',
|
|
162
|
+
workspaceRunnerCapacity: false,
|
|
163
|
+
modelProvider: {installationProvided: false, configured: false},
|
|
164
|
+
membership: {memberCount: 1, pendingInvitationCount: 0},
|
|
165
|
+
});
|
|
166
|
+
const queryClient = createQueryClient();
|
|
167
|
+
|
|
168
|
+
renderWithProviders(
|
|
169
|
+
<SetupChecklistBody checklist={checklist} workspaceSlug={WORKSPACE.slug} />,
|
|
170
|
+
queryClient,
|
|
171
|
+
{capture: vi.fn()},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(await screen.findByRole('list', {name: 'Setup steps'})).toBeInTheDocument();
|
|
175
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(7);
|
|
176
|
+
expect(await screen.findByText('Connect your tools')).toBeInTheDocument();
|
|
177
|
+
expect(
|
|
178
|
+
await screen.findByText('Jobs wait in `pending` until a runner is online'),
|
|
179
|
+
).toBeInTheDocument();
|
|
180
|
+
expect(await screen.findByRole('link', {name: 'Connect'})).toHaveAttribute(
|
|
181
|
+
'href',
|
|
182
|
+
`/w/${WORKSPACE.slug}/settings/integrations`,
|
|
183
|
+
);
|
|
184
|
+
expect(await screen.findByRole('link', {name: 'Set up'})).toHaveAttribute(
|
|
185
|
+
'href',
|
|
186
|
+
`/w/${WORKSPACE.slug}/settings/runners`,
|
|
187
|
+
);
|
|
188
|
+
expect(await screen.findByRole('link', {name: 'Configure'})).toHaveAttribute(
|
|
189
|
+
'href',
|
|
190
|
+
`/w/${WORKSPACE.slug}/settings/agents`,
|
|
191
|
+
);
|
|
192
|
+
expect(await screen.findByRole('link', {name: 'Read the quickstart'})).toHaveAttribute(
|
|
193
|
+
'href',
|
|
194
|
+
'https://www.shipfox.io/docs/getting-started',
|
|
195
|
+
);
|
|
196
|
+
expect(await screen.findByRole('link', {name: 'Invite'})).toHaveAttribute(
|
|
197
|
+
'href',
|
|
198
|
+
`/w/${WORKSPACE.slug}/settings/members`,
|
|
199
|
+
);
|
|
200
|
+
expect(screen.queryByText('Next', {exact: true})).not.toBeInTheDocument();
|
|
201
|
+
expect(await screen.findAllByText('next step', {exact: true})).toHaveLength(2);
|
|
202
|
+
expect(
|
|
203
|
+
screen
|
|
204
|
+
.getAllByText('done', {exact: true})
|
|
205
|
+
.every((node) => node.classList.contains('sr-only')),
|
|
206
|
+
).toBe(true);
|
|
207
|
+
expect(
|
|
208
|
+
screen
|
|
209
|
+
.getAllByText('to do', {exact: true})
|
|
210
|
+
.every((node) => node.classList.contains('sr-only')),
|
|
211
|
+
).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('workspace checklist hosts', () => {
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
window.localStorage.clear();
|
|
218
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: vi.fn()});
|
|
219
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
cleanup();
|
|
224
|
+
window.localStorage.clear();
|
|
225
|
+
vi.restoreAllMocks();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('does not subscribe to checklist queries after dismissal', () => {
|
|
229
|
+
const queryClient = createQueryClient();
|
|
230
|
+
const fetchImpl = vi.fn();
|
|
231
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
232
|
+
dismissWorkspaceSetupChecklist(WORKSPACE.id);
|
|
233
|
+
|
|
234
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
235
|
+
capture: vi.fn(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
239
|
+
expect(
|
|
240
|
+
queryClient
|
|
241
|
+
.getQueryCache()
|
|
242
|
+
.find({queryKey: integrationProvidersQueryOptions().queryKey})
|
|
243
|
+
?.getObserversCount() ?? 0,
|
|
244
|
+
).toBe(0);
|
|
245
|
+
clearWorkspaceSetupChecklistDismissal(WORKSPACE.id);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('renders the completion state only after an observed false-to-true transition', async () => {
|
|
249
|
+
const queryClient = createQueryClient();
|
|
250
|
+
seedQueries(queryClient);
|
|
251
|
+
const capture = vi.fn();
|
|
252
|
+
|
|
253
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {capture});
|
|
254
|
+
|
|
255
|
+
expect(screen.queryByText("You're set up")).not.toBeInTheDocument();
|
|
256
|
+
expect(await screen.findByText('Connect your tools')).toBeInTheDocument();
|
|
257
|
+
|
|
258
|
+
act(() => {
|
|
259
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
260
|
+
connection('github', 'active'),
|
|
261
|
+
connection('linear', 'active'),
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(await screen.findByText("You're set up")).toBeInTheDocument();
|
|
266
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_shown', {host: 'panel'});
|
|
267
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_completed', {host: 'panel'});
|
|
268
|
+
|
|
269
|
+
fireEvent.click(screen.getByRole('button', {name: 'Done'}));
|
|
270
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_dismissed', {host: 'panel'});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('does not replay or keep the completion state after a regression', async () => {
|
|
274
|
+
const queryClient = createQueryClient();
|
|
275
|
+
seedQueries(queryClient);
|
|
276
|
+
const capture = vi.fn();
|
|
277
|
+
|
|
278
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {capture});
|
|
279
|
+
|
|
280
|
+
expect(await screen.findByText('Connect your tools')).toBeInTheDocument();
|
|
281
|
+
act(() => {
|
|
282
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
283
|
+
connection('github', 'active'),
|
|
284
|
+
connection('linear', 'active'),
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
expect(await screen.findByText("You're set up")).toBeInTheDocument();
|
|
288
|
+
|
|
289
|
+
act(() => {
|
|
290
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
291
|
+
connection('github', 'active'),
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
await waitFor(() => expect(screen.queryByText("You're set up")).not.toBeInTheDocument());
|
|
295
|
+
|
|
296
|
+
act(() => {
|
|
297
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
298
|
+
connection('github', 'active'),
|
|
299
|
+
connection('linear', 'active'),
|
|
300
|
+
]);
|
|
301
|
+
});
|
|
302
|
+
await waitFor(() => expect(screen.queryByText("You're set up")).not.toBeInTheDocument());
|
|
303
|
+
expect(
|
|
304
|
+
capture.mock.calls.filter(([event]) => event === 'onboarding_checklist_completed'),
|
|
305
|
+
).toHaveLength(1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('captures row clicks with the checklist row id', async () => {
|
|
309
|
+
const queryClient = createQueryClient();
|
|
310
|
+
seedQueries(queryClient);
|
|
311
|
+
const capture = vi.fn();
|
|
312
|
+
|
|
313
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {capture});
|
|
314
|
+
|
|
315
|
+
fireEvent.click(await screen.findByRole('link', {name: 'Connect'}));
|
|
316
|
+
|
|
317
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_row_clicked', {row_id: 'tools'});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('exposes the indicator progress without duplicating its completion label', async () => {
|
|
321
|
+
const queryClient = createQueryClient();
|
|
322
|
+
seedQueries(queryClient);
|
|
323
|
+
|
|
324
|
+
renderWithProviders(<WorkspaceSetupIndicator workspace={WORKSPACE} />, queryClient, {
|
|
325
|
+
capture: vi.fn(),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(
|
|
329
|
+
await screen.findByRole('button', {name: 'Get started, 2 of 3 done'}),
|
|
330
|
+
).toBeInTheDocument();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('shows the indicator completion transition and its dismiss action', async () => {
|
|
334
|
+
const queryClient = createQueryClient();
|
|
335
|
+
seedQueries(queryClient);
|
|
336
|
+
const capture = vi.fn();
|
|
337
|
+
|
|
338
|
+
renderWithProviders(<WorkspaceSetupIndicator workspace={WORKSPACE} />, queryClient, {capture});
|
|
339
|
+
expect(
|
|
340
|
+
await screen.findByRole('button', {name: 'Get started, 2 of 3 done'}),
|
|
341
|
+
).toBeInTheDocument();
|
|
342
|
+
|
|
343
|
+
act(() => {
|
|
344
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
345
|
+
connection('github', 'active'),
|
|
346
|
+
connection('linear', 'active'),
|
|
347
|
+
]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const trigger = await screen.findByRole('button', {name: 'Get started, 3 of 3 done'});
|
|
351
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_completed', {host: 'popover'});
|
|
352
|
+
fireEvent.click(trigger);
|
|
353
|
+
|
|
354
|
+
expect(await screen.findByText("You're set up")).toBeInTheDocument();
|
|
355
|
+
const dialog = screen.getByRole('dialog');
|
|
356
|
+
expect(dialog).toHaveAttribute('aria-labelledby', trigger.id);
|
|
357
|
+
fireEvent.click(screen.getByRole('button', {name: 'Hide setup guide'}));
|
|
358
|
+
|
|
359
|
+
await waitFor(() =>
|
|
360
|
+
expect(screen.queryByRole('button', {name: GET_STARTED_BUTTON_RE})).not.toBeInTheDocument(),
|
|
361
|
+
);
|
|
362
|
+
expect(capture).toHaveBeenCalledWith('onboarding_checklist_dismissed', {host: 'popover'});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('synchronizes dismissal between mounted hosts in the same tab', async () => {
|
|
366
|
+
const queryClient = createQueryClient();
|
|
367
|
+
seedQueries(queryClient);
|
|
368
|
+
const capture = vi.fn();
|
|
369
|
+
|
|
370
|
+
renderWithProviders(
|
|
371
|
+
<>
|
|
372
|
+
<WorkspaceSetupChecklist workspace={WORKSPACE} />
|
|
373
|
+
<WorkspaceSetupIndicator workspace={WORKSPACE} />
|
|
374
|
+
</>,
|
|
375
|
+
queryClient,
|
|
376
|
+
{capture},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(
|
|
380
|
+
await screen.findByRole('button', {name: 'Get started, 2 of 3 done'}),
|
|
381
|
+
).toBeInTheDocument();
|
|
382
|
+
fireEvent.click(screen.getByRole('button', {name: 'Hide setup guide'}));
|
|
383
|
+
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(screen.queryByRole('region', {name: 'Get started'})).not.toBeInTheDocument();
|
|
386
|
+
expect(screen.queryByRole('button', {name: GET_STARTED_BUTTON_RE})).not.toBeInTheDocument();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('keeps a local dismissal when browser storage cannot persist it', async () => {
|
|
391
|
+
const queryClient = createQueryClient();
|
|
392
|
+
seedQueries(queryClient);
|
|
393
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: vi.fn()});
|
|
394
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
395
|
+
throw new Error('storage unavailable');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
399
|
+
capture: vi.fn(),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(await screen.findByRole('button', {name: 'Hide setup guide'})).toBeInTheDocument();
|
|
403
|
+
fireEvent.click(screen.getByRole('button', {name: 'Hide setup guide'}));
|
|
404
|
+
|
|
405
|
+
await waitFor(() => {
|
|
406
|
+
expect(screen.queryByRole('region', {name: 'Get started'})).not.toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('waits for non-base query families before showing completion', async () => {
|
|
411
|
+
const queryClient = createQueryClient();
|
|
412
|
+
const fetchImpl = vi.fn(() => pendingResponse());
|
|
413
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
414
|
+
queryClient.setQueryData(integrationProvidersQueryOptions().queryKey, [
|
|
415
|
+
githubProvider,
|
|
416
|
+
linearProvider,
|
|
417
|
+
]);
|
|
418
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
419
|
+
connection('github', 'active'),
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
423
|
+
capture: vi.fn(),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(await screen.findByText('Connect your tools')).toBeInTheDocument();
|
|
427
|
+
act(() => {
|
|
428
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
429
|
+
connection('github', 'active'),
|
|
430
|
+
connection('linear', 'active'),
|
|
431
|
+
]);
|
|
432
|
+
});
|
|
433
|
+
await waitFor(() => expect(screen.queryByText("You're set up")).not.toBeInTheDocument());
|
|
434
|
+
|
|
435
|
+
act(() => {
|
|
436
|
+
queryClient.setQueryData(provisionerTokenQueryKeys.active(WORKSPACE.id), {
|
|
437
|
+
provisioners: [],
|
|
438
|
+
installationRunners: 'managed' as const,
|
|
439
|
+
});
|
|
440
|
+
queryClient.setQueryData(modelProviderQueryKeys.catalog(), {
|
|
441
|
+
providers: [],
|
|
442
|
+
workspaceProviders: 'enabled' as const,
|
|
443
|
+
managedProviderId: 'managed-default',
|
|
444
|
+
instanceDefaultProviderId: null,
|
|
445
|
+
});
|
|
446
|
+
queryClient.setQueryData(modelProviderQueryKeys.configs(WORKSPACE.id), {
|
|
447
|
+
configs: [],
|
|
448
|
+
defaultHarnessId: null,
|
|
449
|
+
defaultProviderId: null,
|
|
450
|
+
});
|
|
451
|
+
queryClient.setQueryData(listMembersQueryKey(WORKSPACE.id), []);
|
|
452
|
+
queryClient.setQueryData(listInvitationsQueryKey(WORKSPACE.id), []);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
expect(await screen.findByText("You're set up")).toBeInTheDocument();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('keeps the panel skeleton while base queries are pending', async () => {
|
|
459
|
+
const queryClient = createQueryClient();
|
|
460
|
+
configureApiClient({
|
|
461
|
+
baseUrl: 'https://api.example.test',
|
|
462
|
+
fetchImpl: vi.fn(() => pendingResponse()),
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
466
|
+
capture: vi.fn(),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(await screen.findByRole('status', {name: 'Loading setup guide'})).toBeInTheDocument();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('hides rows from failed query families without marking the checklist complete', async () => {
|
|
473
|
+
const queryClient = createQueryClient();
|
|
474
|
+
configureApiClient({
|
|
475
|
+
baseUrl: 'https://api.example.test',
|
|
476
|
+
fetchImpl: vi.fn(() => Promise.reject(new Error('request failed'))),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
480
|
+
capture: vi.fn(),
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(screen.queryByRole('status', {name: 'Loading setup guide'})).not.toBeInTheDocument();
|
|
485
|
+
});
|
|
486
|
+
expect(screen.queryByText('Connect your tools')).not.toBeInTheDocument();
|
|
487
|
+
expect(screen.queryByText('Set up runner capacity')).not.toBeInTheDocument();
|
|
488
|
+
expect(screen.queryByText('Configure a model provider')).not.toBeInTheDocument();
|
|
489
|
+
expect(screen.queryByText("You're set up")).not.toBeInTheDocument();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('does not let a failed optional family block visible completion', async () => {
|
|
493
|
+
const queryClient = createQueryClient();
|
|
494
|
+
configureApiClient({
|
|
495
|
+
baseUrl: 'https://api.example.test',
|
|
496
|
+
fetchImpl: vi.fn(() => Promise.reject(new Error('optional request failed'))),
|
|
497
|
+
});
|
|
498
|
+
queryClient.setQueryData(integrationProvidersQueryOptions().queryKey, [
|
|
499
|
+
githubProvider,
|
|
500
|
+
linearProvider,
|
|
501
|
+
]);
|
|
502
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
503
|
+
connection('github', 'active'),
|
|
504
|
+
connection('linear', 'active'),
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
508
|
+
capture: vi.fn(),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await waitFor(() => {
|
|
512
|
+
expect(screen.queryByRole('status', {name: 'Loading setup guide'})).not.toBeInTheDocument();
|
|
513
|
+
expect(screen.queryByRole('region', {name: 'Get started'})).not.toBeInTheDocument();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('hides the indicator while optional checklist families are unsettled', async () => {
|
|
518
|
+
const queryClient = createQueryClient();
|
|
519
|
+
const fetchImpl = vi.fn(() => pendingResponse());
|
|
520
|
+
const capture = vi.fn();
|
|
521
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
522
|
+
queryClient.setQueryData(integrationProvidersQueryOptions().queryKey, [
|
|
523
|
+
githubProvider,
|
|
524
|
+
linearProvider,
|
|
525
|
+
]);
|
|
526
|
+
queryClient.setQueryData(integrationConnectionsQueryOptions(WORKSPACE.id).queryKey, [
|
|
527
|
+
connection('github', 'active'),
|
|
528
|
+
connection('linear', 'active'),
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
renderWithProviders(<WorkspaceSetupIndicator workspace={WORKSPACE} />, queryClient, {capture});
|
|
532
|
+
|
|
533
|
+
await waitFor(() => {
|
|
534
|
+
expect(screen.queryByRole('button', {name: GET_STARTED_BUTTON_RE})).not.toBeInTheDocument();
|
|
535
|
+
});
|
|
536
|
+
expect(capture).not.toHaveBeenCalledWith('onboarding_checklist_shown', {host: 'popover'});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('does not render an initially complete checklist without a transition', async () => {
|
|
540
|
+
const queryClient = createQueryClient();
|
|
541
|
+
seedQueries(queryClient, true);
|
|
542
|
+
|
|
543
|
+
renderWithProviders(<WorkspaceSetupChecklist workspace={WORKSPACE} />, queryClient, {
|
|
544
|
+
capture: vi.fn(),
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await waitFor(() => {
|
|
548
|
+
expect(screen.queryByRole('region', {name: 'Get started'})).not.toBeInTheDocument();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('does not render an initially complete indicator without a transition', async () => {
|
|
553
|
+
const queryClient = createQueryClient();
|
|
554
|
+
seedQueries(queryClient, true);
|
|
555
|
+
|
|
556
|
+
renderWithProviders(<WorkspaceSetupIndicator workspace={WORKSPACE} />, queryClient, {
|
|
557
|
+
capture: vi.fn(),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await waitFor(() => {
|
|
561
|
+
expect(screen.queryByRole('button', {name: GET_STARTED_BUTTON_RE})).not.toBeInTheDocument();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {SetupChecklistBody} from './setup-checklist-body.js';
|
|
2
|
+
export type {
|
|
3
|
+
SetupChecklistBodyProps,
|
|
4
|
+
WorkspaceReference,
|
|
5
|
+
WorkspaceSetupHostProps,
|
|
6
|
+
} from './setup-checklist-types.js';
|
|
7
|
+
export {WorkspaceSetupChecklist} from './workspace-setup-checklist.js';
|
|
8
|
+
export {WorkspaceSetupIndicator} from './workspace-setup-indicator.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {useClientAnalytics, useMaybeActiveWorkspace} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {Panel, PanelBody} from '@shipfox/react-ui/panel';
|
|
3
|
+
import {useCallback, useState} from 'react';
|
|
4
|
+
import type {SetupChecklistItem} from '#core/setup-checklist.js';
|
|
5
|
+
import {useSetupChecklistQueryState} from '#hooks/api/setup-checklist.js';
|
|
6
|
+
import {useCompletionTransition, useShownAnalytics} from '#hooks/use-checklist-analytics.js';
|
|
7
|
+
import {useChecklistDismissal} from '#hooks/use-checklist-dismissal.js';
|
|
8
|
+
import {SetupChecklistBody} from './setup-checklist-body.js';
|
|
9
|
+
import {
|
|
10
|
+
ChecklistHeader,
|
|
11
|
+
ChecklistSkeleton,
|
|
12
|
+
checklistCountLabel,
|
|
13
|
+
} from './setup-checklist-host-primitives.js';
|
|
14
|
+
import type {WorkspaceReference, WorkspaceSetupHostProps} from './setup-checklist-types.js';
|
|
15
|
+
|
|
16
|
+
export function WorkspaceSetupChecklist(props: WorkspaceSetupHostProps = {}) {
|
|
17
|
+
if (props.workspace) {
|
|
18
|
+
return (
|
|
19
|
+
<WorkspaceSetupChecklistForWorkspace key={props.workspace.id} workspace={props.workspace} />
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <WorkspaceSetupChecklistFromShell />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function WorkspaceSetupChecklistFromShell() {
|
|
27
|
+
const workspace = useMaybeActiveWorkspace();
|
|
28
|
+
return workspace ? (
|
|
29
|
+
<WorkspaceSetupChecklistForWorkspace key={workspace.id} workspace={workspace} />
|
|
30
|
+
) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function WorkspaceSetupChecklistForWorkspace({workspace}: {workspace: WorkspaceReference}) {
|
|
34
|
+
const dismissal = useChecklistDismissal(workspace.id);
|
|
35
|
+
const queryState = useSetupChecklistQueryState(workspace.id, !dismissal.dismissed);
|
|
36
|
+
const [burstPending, setBurstPending] = useState(false);
|
|
37
|
+
const handleCompleted = useCallback((completed: boolean) => {
|
|
38
|
+
if (completed) setBurstPending(true);
|
|
39
|
+
}, []);
|
|
40
|
+
const showCompletion = useCompletionTransition(queryState, 'panel', handleCompleted);
|
|
41
|
+
const analytics = useClientAnalytics();
|
|
42
|
+
const consumeBurst = useCallback(() => setBurstPending(false), []);
|
|
43
|
+
|
|
44
|
+
const dismiss = useCallback(() => {
|
|
45
|
+
dismissal.dismiss();
|
|
46
|
+
analytics.capture('onboarding_checklist_dismissed', {host: 'panel'});
|
|
47
|
+
}, [analytics, dismissal]);
|
|
48
|
+
const handleAction = useCallback(
|
|
49
|
+
(item: SetupChecklistItem) => {
|
|
50
|
+
analytics.capture('onboarding_checklist_row_clicked', {row_id: item.id});
|
|
51
|
+
},
|
|
52
|
+
[analytics],
|
|
53
|
+
);
|
|
54
|
+
const isVisible =
|
|
55
|
+
!dismissal.dismissed &&
|
|
56
|
+
queryState.baseSettled &&
|
|
57
|
+
(!queryState.checklist.complete || showCompletion);
|
|
58
|
+
useShownAnalytics('panel', isVisible);
|
|
59
|
+
|
|
60
|
+
if (dismissal.dismissed) return null;
|
|
61
|
+
|
|
62
|
+
if (queryState.baseSettled && queryState.checklist.complete && !showCompletion) return null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Panel asChild className="w-full">
|
|
66
|
+
<section aria-label="Get started">
|
|
67
|
+
<ChecklistHeader
|
|
68
|
+
count={queryState.baseSettled ? checklistCountLabel(queryState.checklist) : undefined}
|
|
69
|
+
onDismiss={dismiss}
|
|
70
|
+
/>
|
|
71
|
+
<PanelBody>
|
|
72
|
+
{queryState.baseSettled ? (
|
|
73
|
+
queryState.checklist.complete && !showCompletion ? null : (
|
|
74
|
+
<SetupChecklistBody
|
|
75
|
+
checklist={queryState.checklist}
|
|
76
|
+
workspaceSlug={workspace.slug}
|
|
77
|
+
completion={showCompletion}
|
|
78
|
+
showBurst={burstPending}
|
|
79
|
+
onBurstComplete={consumeBurst}
|
|
80
|
+
onAction={handleAction}
|
|
81
|
+
onDone={dismiss}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
) : (
|
|
85
|
+
<ChecklistSkeleton />
|
|
86
|
+
)}
|
|
87
|
+
</PanelBody>
|
|
88
|
+
</section>
|
|
89
|
+
</Panel>
|
|
90
|
+
);
|
|
91
|
+
}
|