@shipfox/client-auth 17.0.0 → 22.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.
Files changed (36) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +34 -0
  3. package/dist/pages/workspace-onboarding-page.d.ts.map +1 -1
  4. package/dist/pages/workspace-onboarding-page.js +100 -235
  5. package/dist/pages/workspace-onboarding-page.js.map +1 -1
  6. package/dist/routes/index.d.ts +3 -0
  7. package/dist/routes/index.d.ts.map +1 -1
  8. package/dist/routes/index.js +3 -0
  9. package/dist/routes/index.js.map +1 -1
  10. package/dist/routes/login.d.ts +3 -0
  11. package/dist/routes/login.js +3 -0
  12. package/dist/routes/login.js.map +1 -1
  13. package/dist/routes/logout.d.ts +3 -0
  14. package/dist/routes/logout.js +3 -0
  15. package/dist/routes/logout.js.map +1 -1
  16. package/dist/routes/reset.d.ts +3 -0
  17. package/dist/routes/reset.js +3 -0
  18. package/dist/routes/reset.js.map +1 -1
  19. package/dist/routes/signup.d.ts +3 -0
  20. package/dist/routes/signup.js +3 -0
  21. package/dist/routes/signup.js.map +1 -1
  22. package/dist/routes/workspace-onboarding.d.ts +3 -0
  23. package/dist/routes/workspace-onboarding.d.ts.map +1 -1
  24. package/dist/routes/workspace-onboarding.js +3 -0
  25. package/dist/routes/workspace-onboarding.js.map +1 -1
  26. package/dist/tsconfig.test.tsbuildinfo +1 -1
  27. package/package.json +5 -5
  28. package/src/pages/workspace-onboarding-page.test.tsx +16 -6
  29. package/src/pages/workspace-onboarding-page.tsx +104 -174
  30. package/src/routes/index.tsx +1 -0
  31. package/src/routes/login.tsx +1 -0
  32. package/src/routes/logout.tsx +5 -1
  33. package/src/routes/reset.tsx +1 -0
  34. package/src/routes/signup.tsx +1 -0
  35. package/src/routes/workspace-onboarding.tsx +1 -0
  36. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-auth",
3
3
  "license": "MIT",
4
- "version": "17.0.0",
4
+ "version": "22.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -39,10 +39,10 @@
39
39
  "@shipfox/api-auth-dto": "12.0.0",
40
40
  "@shipfox/api-workspaces-dto": "12.0.0",
41
41
  "@shipfox/client-api": "6.0.1",
42
- "@shipfox/client-invitations": "17.0.0",
43
- "@shipfox/client-ui": "17.0.0",
44
- "@shipfox/react-ui": "1.2.0",
45
- "@shipfox/client-shell": "17.0.0"
42
+ "@shipfox/client-invitations": "22.0.0",
43
+ "@shipfox/client-shell": "22.0.0",
44
+ "@shipfox/client-ui": "22.0.0",
45
+ "@shipfox/react-ui": "2.1.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@tanstack/react-query": "^5.101.0",
@@ -66,7 +66,7 @@ describe('WorkspaceOnboardingPage', () => {
66
66
  });
67
67
  configureApiClient({fetchImpl});
68
68
 
69
- renderAuthPage(
69
+ const {container} = renderAuthPage(
70
70
  '/',
71
71
  <AuthGuard>
72
72
  <WorkspaceGuard>
@@ -74,7 +74,13 @@ describe('WorkspaceOnboardingPage', () => {
74
74
  </WorkspaceGuard>
75
75
  </AuthGuard>,
76
76
  );
77
- fireEvent.change(await screen.findByLabelText('Workspace name'), {
77
+ const workspaceName = await screen.findByLabelText('Workspace name');
78
+ expect(container.querySelector('[data-slot="panel-header"]')).toHaveAttribute(
79
+ 'data-variant',
80
+ 'plain',
81
+ );
82
+ expect(container.querySelector('[data-slot="panel-body"]')).toBeInTheDocument();
83
+ fireEvent.change(workspaceName, {
78
84
  target: {value: ' Acme '},
79
85
  });
80
86
  fireEvent.click(screen.getByRole('button', {name: 'Create workspace'}));
@@ -111,13 +117,17 @@ describe('WorkspaceOnboardingPage', () => {
111
117
  const slug = screen.getByLabelText('Workspace slug');
112
118
  expect(screen.getByText(`${window.location.origin}/w/acme`)).toBeInTheDocument();
113
119
  fireEvent.change(name, {target: {value: 'Acme Labs'}});
114
- expect(slug).toHaveValue('acme-labs');
115
- expect(screen.getByText(`${window.location.origin}/w/acme-labs`)).toBeInTheDocument();
120
+ await waitFor(() => {
121
+ expect(slug).toHaveValue('acme-labs');
122
+ expect(screen.getByText(`${window.location.origin}/w/acme-labs`)).toBeInTheDocument();
123
+ });
116
124
 
117
125
  fireEvent.change(slug, {target: {value: 'custom-workspace'}});
118
126
  fireEvent.change(name, {target: {value: 'Renamed Labs'}});
119
- expect(slug).toHaveValue('custom-workspace');
120
- expect(screen.getByText(`${window.location.origin}/w/custom-workspace`)).toBeInTheDocument();
127
+ await waitFor(() => {
128
+ expect(slug).toHaveValue('custom-workspace');
129
+ expect(screen.getByText(`${window.location.origin}/w/custom-workspace`)).toBeInTheDocument();
130
+ });
121
131
  });
122
132
 
123
133
  test('checks a manually edited workspace slug for availability', async () => {
@@ -1,11 +1,12 @@
1
1
  import {slugifyName, slugSchema} from '@shipfox/api-common-dto';
2
2
  import {createWorkspaceBodySchema} from '@shipfox/api-workspaces-dto';
3
+ import {FocusedFrame} from '@shipfox/client-shell/runtime';
3
4
  import {displayNameFieldError, SlugField} from '@shipfox/client-ui';
4
5
  import {Button} from '@shipfox/react-ui/button';
5
6
  import {Callout} from '@shipfox/react-ui/callout';
6
- import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@shipfox/react-ui/card';
7
7
  import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
8
8
  import {Icon} from '@shipfox/react-ui/icon';
9
+ import {Panel, PanelBody, PanelHeader, PanelTitle} from '@shipfox/react-ui/panel';
9
10
  import {toast} from '@shipfox/react-ui/toast';
10
11
  import {Text} from '@shipfox/react-ui/typography';
11
12
  import {useForm} from '@tanstack/react-form';
@@ -17,23 +18,6 @@ import {useAuthState} from '#hooks/use-auth-state.js';
17
18
  import {lastWorkspaceIdAtom, rememberLastWorkspaceId} from '#state/last-workspace.js';
18
19
  import {workspaceOnboardingErrorToFormError} from './form-errors.js';
19
20
 
20
- const previewMetrics = [
21
- {label: 'Runs', value: '--'},
22
- {label: 'Passed', value: '--'},
23
- {label: 'Failed', value: '--'},
24
- {label: 'Duration', value: '--'},
25
- ];
26
- const previewBars = [
27
- {id: 'runs-start', height: 32},
28
- {id: 'runs-mid-low', height: 48},
29
- {id: 'runs-dip', height: 28},
30
- {id: 'runs-mid-high', height: 66},
31
- {id: 'runs-mid', height: 54},
32
- {id: 'runs-peak', height: 82},
33
- {id: 'runs-late-low', height: 44},
34
- {id: 'runs-late-high', height: 74},
35
- ];
36
-
37
21
  function isSlugValid(value: string): boolean {
38
22
  return slugSchema.safeParse(value).success;
39
23
  }
@@ -78,8 +62,8 @@ export function WorkspaceOnboardingPage() {
78
62
  });
79
63
 
80
64
  return (
81
- <main className="min-h-screen bg-background-subtle-base px-frame py-frame max-[520px]:px-row">
82
- <div className="mx-auto flex min-h-[calc(100vh-64px)] w-full max-w-[1120px] flex-col gap-section">
65
+ <main className="min-h-screen px-frame py-frame max-[520px]:px-row">
66
+ <FocusedFrame className="flex min-h-[calc(100vh-64px)] flex-col gap-section">
83
67
  <header className="flex items-center justify-between">
84
68
  <div className="flex items-center gap-inline">
85
69
  <div className="flex size-36 items-center justify-center rounded-8 border border-border-neutral-base bg-background-neutral-base shadow-button-neutral">
@@ -91,9 +75,9 @@ export function WorkspaceOnboardingPage() {
91
75
  </div>
92
76
  </header>
93
77
 
94
- <section className="grid flex-1 items-center gap-region lg:grid-cols-[minmax(0,440px)_minmax(0,1fr)]">
78
+ <section className="flex flex-1 items-center">
95
79
  <form
96
- className="relative z-10 w-full"
80
+ className="w-full"
97
81
  noValidate
98
82
  aria-labelledby="workspace-onboarding-title"
99
83
  onSubmit={(event) => {
@@ -102,168 +86,114 @@ export function WorkspaceOnboardingPage() {
102
86
  void form.handleSubmit();
103
87
  }}
104
88
  >
105
- <Card className="gap-section p-panel shadow-button-neutral">
106
- <CardHeader className="gap-inline">
107
- <CardTitle id="workspace-onboarding-title" variant="h1">
89
+ <Panel>
90
+ <PanelHeader variant="plain" className="flex-col items-start gap-inline">
91
+ <PanelTitle id="workspace-onboarding-title" variant="h1">
108
92
  Create your workspace
109
- </CardTitle>
110
- <CardDescription>Give your team a place to collaborate.</CardDescription>
111
- </CardHeader>
93
+ </PanelTitle>
94
+ <Text size="sm" className="text-foreground-neutral-muted">
95
+ Give your team a place to collaborate.
96
+ </Text>
97
+ </PanelHeader>
112
98
 
113
- {formError ? (
114
- <Callout role="alert" type="error">
115
- {formError}
116
- </Callout>
117
- ) : null}
99
+ <PanelBody className="flex flex-col gap-section p-panel">
100
+ {formError ? (
101
+ <Callout role="alert" type="error">
102
+ {formError}
103
+ </Callout>
104
+ ) : null}
118
105
 
119
- <CardContent className="flex flex-col gap-inline">
120
- <form.Field
121
- name="name"
122
- validators={{
123
- onBlur: ({value}) =>
124
- displayNameFieldError(
125
- value,
126
- 'Workspace name',
127
- createWorkspaceBodySchema.shape.name,
128
- ),
129
- onSubmit: ({value}) =>
130
- displayNameFieldError(
131
- value,
132
- 'Workspace name',
133
- createWorkspaceBodySchema.shape.name,
134
- ),
135
- }}
136
- >
137
- {(field) => (
138
- <FormField label="Workspace name" id="workspace-name" error={fieldError(field)}>
139
- <FormFieldInput
140
- autoComplete="organization"
141
- name="name"
142
- placeholder="Acme"
143
- type="text"
106
+ <div className="flex flex-col gap-inline">
107
+ <form.Field
108
+ name="name"
109
+ validators={{
110
+ onBlur: ({value}) =>
111
+ displayNameFieldError(
112
+ value,
113
+ 'Workspace name',
114
+ createWorkspaceBodySchema.shape.name,
115
+ ),
116
+ onSubmit: ({value}) =>
117
+ displayNameFieldError(
118
+ value,
119
+ 'Workspace name',
120
+ createWorkspaceBodySchema.shape.name,
121
+ ),
122
+ }}
123
+ >
124
+ {(field) => (
125
+ <FormField
126
+ label="Workspace name"
127
+ id="workspace-name"
128
+ error={fieldError(field)}
129
+ >
130
+ <FormFieldInput
131
+ autoComplete="organization"
132
+ name="name"
133
+ placeholder="Acme"
134
+ type="text"
135
+ value={field.state.value}
136
+ onChange={(event) => {
137
+ const name = event.target.value;
138
+ field.handleChange(name);
139
+ if (!slugTouched) {
140
+ form.setFieldValue(
141
+ 'slug',
142
+ name ? slugifyName(name, {fallback: 'workspace'}) : '',
143
+ );
144
+ }
145
+ }}
146
+ onBlur={field.handleBlur}
147
+ />
148
+ </FormField>
149
+ )}
150
+ </form.Field>
151
+ <form.Field
152
+ name="slug"
153
+ validators={{
154
+ onBlur: createWorkspaceBodySchema.shape.slug,
155
+ onSubmit: createWorkspaceBodySchema.shape.slug,
156
+ }}
157
+ >
158
+ {(field) => (
159
+ <SlugField
160
+ id="workspace-slug"
161
+ label="Workspace slug"
162
+ name="slug"
144
163
  value={field.state.value}
145
- onChange={(event) => {
146
- const name = event.target.value;
147
- field.handleChange(name);
148
- if (!slugTouched) {
149
- form.setFieldValue(
150
- 'slug',
151
- name ? slugifyName(name, {fallback: 'workspace'}) : '',
152
- );
153
- }
164
+ onChange={(value) => {
165
+ setSlugTouched(true);
166
+ field.handleChange(value);
154
167
  }}
155
168
  onBlur={field.handleBlur}
169
+ error={fieldError(field)}
170
+ description={
171
+ <span className="break-all font-code">
172
+ {`${window.location.origin}/w/${field.state.value || 'acme'}`}
173
+ </span>
174
+ }
175
+ placeholder="acme"
176
+ checkEnabled={slugTouched}
177
+ isValid={isSlugValid}
178
+ checkAvailability={checkWorkspaceSlugAvailability}
156
179
  />
157
- </FormField>
158
- )}
159
- </form.Field>
160
- <form.Field
161
- name="slug"
162
- validators={{
163
- onBlur: createWorkspaceBodySchema.shape.slug,
164
- onSubmit: createWorkspaceBodySchema.shape.slug,
165
- }}
166
- >
167
- {(field) => (
168
- <SlugField
169
- id="workspace-slug"
170
- label="Workspace slug"
171
- name="slug"
172
- value={field.state.value}
173
- onChange={(value) => {
174
- setSlugTouched(true);
175
- field.handleChange(value);
176
- }}
177
- onBlur={field.handleBlur}
178
- error={fieldError(field)}
179
- description={
180
- <span className="break-all font-code">
181
- {`${window.location.origin}/w/${field.state.value || 'acme'}`}
182
- </span>
183
- }
184
- placeholder="acme"
185
- checkEnabled={slugTouched}
186
- isValid={isSlugValid}
187
- checkAvailability={checkWorkspaceSlugAvailability}
188
- />
189
- )}
190
- </form.Field>
191
- </CardContent>
192
-
193
- <Button
194
- className="w-full"
195
- iconRight="chevronRight"
196
- isLoading={createWorkspace.isPending}
197
- type="submit"
198
- >
199
- {createWorkspace.isPending ? 'Creating workspace...' : 'Create workspace'}
200
- </Button>
201
- </Card>
202
- </form>
180
+ )}
181
+ </form.Field>
182
+ </div>
203
183
 
204
- <div className="hidden flex-col gap-group lg:flex" aria-hidden="true">
205
- <div className="grid grid-cols-4 gap-cluster">
206
- {previewMetrics.map((metric) => (
207
- <div
208
- className="rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact shadow-button-neutral"
209
- key={metric.label}
184
+ <Button
185
+ className="w-full"
186
+ iconRight="chevronRight"
187
+ isLoading={createWorkspace.isPending}
188
+ type="submit"
210
189
  >
211
- <Text size="xs" className="text-foreground-neutral-muted">
212
- {metric.label}
213
- </Text>
214
- <Text size="xl" bold>
215
- {metric.value}
216
- </Text>
217
- </div>
218
- ))}
219
- </div>
220
- <div className="grid grid-cols-2 gap-group">
221
- <PreviewPanel title="Performance over time" />
222
- <PreviewPanel title="Duration distribution" bars />
223
- </div>
224
- <div className="flex flex-col gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact shadow-button-neutral">
225
- <Text size="sm" bold>
226
- Jobs breakdown
227
- </Text>
228
- <div className="flex flex-col gap-inline">
229
- {[0, 1, 2, 3].map((row) => (
230
- <div
231
- className="grid grid-cols-[1fr_80px_80px] gap-cluster border-t border-border-neutral-base pt-[10px]"
232
- key={row}
233
- >
234
- <div className="h-12 rounded-full bg-background-neutral-disabled" />
235
- <div className="h-12 rounded-full bg-background-neutral-disabled" />
236
- <div className="h-12 rounded-full bg-background-neutral-disabled" />
237
- </div>
238
- ))}
239
- </div>
240
- </div>
241
- </div>
190
+ {createWorkspace.isPending ? 'Creating workspace...' : 'Create workspace'}
191
+ </Button>
192
+ </PanelBody>
193
+ </Panel>
194
+ </form>
242
195
  </section>
243
- </div>
196
+ </FocusedFrame>
244
197
  </main>
245
198
  );
246
199
  }
247
-
248
- function PreviewPanel({title, bars = false}: {title: string; bars?: boolean}) {
249
- return (
250
- <div className="flex flex-col gap-group rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact shadow-button-neutral">
251
- <Text size="sm" bold>
252
- {title}
253
- </Text>
254
- <div className="flex h-[220px] items-end gap-inline border-b border-l border-border-neutral-base px-row pb-[10px]">
255
- {previewBars.map((bar) => (
256
- <div
257
- className={
258
- bars
259
- ? 'w-full rounded-t-4 bg-background-neutral-disabled'
260
- : 'w-full rounded-full bg-background-neutral-disabled'
261
- }
262
- key={`${title}-${bar.id}`}
263
- style={{height: `${bar.height}%`}}
264
- />
265
- ))}
266
- </div>
267
- </div>
268
- );
269
- }
@@ -4,6 +4,7 @@ import {FullPageLoader} from '@shipfox/react-ui/loader';
4
4
  import {redirect} from '@tanstack/react-router';
5
5
 
6
6
  export default defineRoute({
7
+ staticData: {frame: 'focused'},
7
8
  beforeLoad: ({context}: {context: RouterContext}) => {
8
9
  const auth = context.auth;
9
10
  if (!auth || auth.isLoading) return;
@@ -4,6 +4,7 @@ import {LoginPage} from '#pages/login-page.js';
4
4
  import {validateRedirectSearch} from './inputs.js';
5
5
 
6
6
  export default defineRoute({
7
+ staticData: {frame: 'focused'},
7
8
  validateSearch: validateRedirectSearch,
8
9
  component: () => (
9
10
  <GuestGuard>
@@ -2,4 +2,8 @@ import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {LogoutPage} from '#pages/logout-page.js';
3
3
  import {validateRedirectSearch} from './inputs.js';
4
4
 
5
- export default defineRoute({validateSearch: validateRedirectSearch, component: LogoutPage});
5
+ export default defineRoute({
6
+ staticData: {frame: 'focused'},
7
+ validateSearch: validateRedirectSearch,
8
+ component: LogoutPage,
9
+ });
@@ -4,6 +4,7 @@ import {PasswordResetPage} from '#pages/password-reset-page.js';
4
4
  import {validatePasswordResetSearch} from './inputs.js';
5
5
 
6
6
  export default defineRoute({
7
+ staticData: {frame: 'focused'},
7
8
  validateSearch: validatePasswordResetSearch,
8
9
  component: () => (
9
10
  <GuestGuard>
@@ -4,6 +4,7 @@ import {SignupPage} from '#pages/signup-page.js';
4
4
  import {validateRedirectSearch} from './inputs.js';
5
5
 
6
6
  export default defineRoute({
7
+ staticData: {frame: 'focused'},
7
8
  validateSearch: validateRedirectSearch,
8
9
  component: () => (
9
10
  <GuestGuard>
@@ -4,6 +4,7 @@ import {redirect} from '@tanstack/react-router';
4
4
  import {WorkspaceOnboardingPage} from '#pages/workspace-onboarding-page.js';
5
5
 
6
6
  export default defineRoute({
7
+ staticData: {frame: 'focused'},
7
8
  beforeLoad: ({context, location}: {context: RouterContext; location: {href: string}}) => {
8
9
  const auth = context.auth;
9
10
  if (!auth || auth.isLoading) return;