@shipfox/client-workspace-settings 21.0.0 → 22.0.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-workspace-settings",
3
3
  "license": "MIT",
4
- "version": "21.0.0",
4
+ "version": "22.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -28,11 +28,11 @@
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@tanstack/react-form": "^1.32.0",
30
30
  "@shipfox/api-workspaces-dto": "12.0.0",
31
+ "@shipfox/client-auth": "22.0.1",
31
32
  "@shipfox/client-api": "6.0.1",
32
- "@shipfox/client-auth": "21.0.0",
33
- "@shipfox/client-shell": "21.0.0",
34
- "@shipfox/react-ui": "2.0.0",
35
- "@shipfox/client-ui": "21.0.0"
33
+ "@shipfox/client-shell": "22.0.1",
34
+ "@shipfox/react-ui": "2.1.0",
35
+ "@shipfox/client-ui": "22.0.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tanstack/react-query": "^5.101.0",
@@ -34,7 +34,6 @@ import {useState} from 'react';
34
34
  import {
35
35
  getInvitationExpiry,
36
36
  getMemberRemovalRestriction,
37
- memberCount,
38
37
  type PendingInvitation,
39
38
  type WorkspaceMember,
40
39
  } from '#core/membership.js';
@@ -74,18 +73,15 @@ function MembersSection({
74
73
  return (
75
74
  <section className="flex flex-col gap-group">
76
75
  <div className="flex flex-col gap-tight">
77
- <Header variant="h3">Members</Header>
78
- <Text size="sm" className="text-foreground-neutral-muted">
79
- {query.isPending
80
- ? 'Loading members…'
81
- : `${memberCount(members)} ${memberCount(members) === 1 ? 'member' : 'members'}`}
82
- </Text>
76
+ <Header variant="h1">Members</Header>
83
77
  </div>
84
78
 
85
- {query.isPending ? <TableSkeleton rows={3} cols={3} /> : null}
79
+ {query.isPending ? <TableSkeleton rows={3} cols={3} label="Loading members" /> : null}
86
80
 
87
81
  {query.isError && query.data === undefined ? (
88
- <QueryLoadError query={query} subject="members" />
82
+ <Panel>
83
+ <QueryLoadError query={query} subject="members" variant="panel" />
84
+ </Panel>
89
85
  ) : null}
90
86
 
91
87
  {members.length > 0 ? (
@@ -213,11 +209,6 @@ function PendingInvitationsSection({
213
209
  <div className="flex items-center justify-between gap-group">
214
210
  <div className="flex flex-col gap-tight">
215
211
  <Header variant="h3">Pending invitations</Header>
216
- <Text size="sm" className="text-foreground-neutral-muted">
217
- {query.isPending
218
- ? 'Loading invitations…'
219
- : `${invitations.length} ${invitations.length === 1 ? 'invitation' : 'invitations'}`}
220
- </Text>
221
212
  </div>
222
213
  <InviteMemberModal
223
214
  open={inviteOpen}
@@ -227,13 +218,19 @@ function PendingInvitationsSection({
227
218
  />
228
219
  </div>
229
220
 
230
- {query.isPending ? <TableSkeleton rows={2} cols={3} /> : null}
221
+ {query.isPending ? <TableSkeleton rows={2} cols={3} label="Loading invitations" /> : null}
231
222
 
232
223
  {query.isError && query.data === undefined ? (
233
- <QueryLoadError query={query} subject="invitations" />
224
+ <Panel>
225
+ <QueryLoadError query={query} subject="invitations" variant="panel" />
226
+ </Panel>
234
227
  ) : null}
235
228
 
236
- {query.data !== undefined && invitations.length === 0 ? <EmptyInvitations /> : null}
229
+ {query.data !== undefined && invitations.length === 0 ? (
230
+ <Panel>
231
+ <EmptyInvitations />
232
+ </Panel>
233
+ ) : null}
237
234
 
238
235
  {invitations.length > 0 ? (
239
236
  <Panel>
@@ -458,22 +455,26 @@ function EmptyInvitations() {
458
455
  icon="mailLine"
459
456
  title="No pending invitations."
460
457
  description="Invite someone above to grow your workspace."
458
+ variant="panel"
461
459
  />
462
460
  );
463
461
  }
464
462
 
465
- function TableSkeleton({rows, cols}: {rows: number; cols: number}) {
463
+ function TableSkeleton({rows, cols, label}: {rows: number; cols: number; label: string}) {
466
464
  return (
467
- <div className="flex flex-col gap-cluster">
465
+ <Panel role="status" aria-label={label} className="divide-y">
468
466
  {Array.from({length: rows}).map((_, rowIdx) => (
469
- // biome-ignore lint/suspicious/noArrayIndexKey: stable placeholder rows
470
- <div key={rowIdx} className="grid grid-cols-3 gap-group">
467
+ <div
468
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable placeholder rows
469
+ key={rowIdx}
470
+ className="grid min-h-44 grid-cols-3 gap-group px-row py-row"
471
+ >
471
472
  {Array.from({length: cols}).map((__, colIdx) => (
472
473
  // biome-ignore lint/suspicious/noArrayIndexKey: stable placeholder cells
473
474
  <Skeleton key={colIdx} className="h-20" />
474
475
  ))}
475
476
  </div>
476
477
  ))}
477
- </div>
478
+ </Panel>
478
479
  );
479
480
  }
@@ -1,5 +1,10 @@
1
- import {useMaybeActiveWorkspace} from '@shipfox/client-shell/runtime';
1
+ import {
2
+ useAuthState,
3
+ useMaybeActiveWorkspace,
4
+ WorkspaceUnavailablePage,
5
+ } from '@shipfox/client-shell/runtime';
2
6
  import {FullPageLoader} from '@shipfox/react-ui/loader';
7
+ import {Navigate} from '@tanstack/react-router';
3
8
  import type {ReactNode} from 'react';
4
9
 
5
10
  interface WorkspaceSettingsShellProps {
@@ -7,8 +12,11 @@ interface WorkspaceSettingsShellProps {
7
12
  }
8
13
 
9
14
  export function WorkspaceSettingsShell({children}: WorkspaceSettingsShellProps) {
15
+ const auth = useAuthState();
10
16
  const workspace = useMaybeActiveWorkspace();
11
- if (!workspace) return <FullPageLoader />;
17
+ if (auth.isLoading) return <FullPageLoader />;
18
+ if (!auth.isAuthenticated) return <Navigate to={'/auth/login' as never} replace />;
19
+ if (!workspace) return <WorkspaceUnavailablePage />;
12
20
 
13
21
  return children(workspace);
14
22
  }
@@ -8,8 +8,9 @@ import {displayNameFieldError, SlugChangeWarning, SlugField} from '@shipfox/clie
8
8
  import {Button} from '@shipfox/react-ui/button';
9
9
  import {Callout} from '@shipfox/react-ui/callout';
10
10
  import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
11
+ import {Panel, PanelBody} from '@shipfox/react-ui/panel';
11
12
  import {toast} from '@shipfox/react-ui/toast';
12
- import {Header, Text} from '@shipfox/react-ui/typography';
13
+ import {Header} from '@shipfox/react-ui/typography';
13
14
  import {useForm} from '@tanstack/react-form';
14
15
  import {useNavigate} from '@tanstack/react-router';
15
16
  import {useState} from 'react';
@@ -95,93 +96,94 @@ function WorkspaceGeneralForm({workspace}: {workspace: ReturnType<typeof useActi
95
96
  return (
96
97
  <>
97
98
  <div className="flex min-w-0 flex-col gap-section">
98
- <header className="flex flex-col gap-inline">
99
- <Header variant="h1">General</Header>
100
- <Text size="sm" className="text-foreground-neutral-muted">
101
- Update the workspace name and the slug used in its URLs.
102
- </Text>
103
- </header>
99
+ <Header variant="h1">General</Header>
104
100
 
105
- {formError ? (
106
- <Callout role="alert" type="error">
107
- {formError}
108
- </Callout>
109
- ) : null}
101
+ <Panel>
102
+ <PanelBody className="gap-group p-panel">
103
+ {formError ? (
104
+ <Callout role="alert" type="error">
105
+ {formError}
106
+ </Callout>
107
+ ) : null}
110
108
 
111
- <form
112
- className="flex max-w-[560px] flex-col gap-group"
113
- noValidate
114
- onSubmit={(event) => {
115
- event.preventDefault();
116
- event.stopPropagation();
117
- void form.handleSubmit();
118
- }}
119
- >
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
139
- label="Workspace name"
140
- id="workspace-settings-name"
141
- error={fieldError(field)}
109
+ <form
110
+ className="flex w-full max-w-[560px] flex-col gap-group"
111
+ noValidate
112
+ onSubmit={(event) => {
113
+ event.preventDefault();
114
+ event.stopPropagation();
115
+ void form.handleSubmit();
116
+ }}
117
+ >
118
+ <form.Field
119
+ name="name"
120
+ validators={{
121
+ onBlur: ({value}) =>
122
+ displayNameFieldError(
123
+ value,
124
+ 'Workspace name',
125
+ createWorkspaceBodySchema.shape.name,
126
+ ),
127
+ onSubmit: ({value}) =>
128
+ displayNameFieldError(
129
+ value,
130
+ 'Workspace name',
131
+ createWorkspaceBodySchema.shape.name,
132
+ ),
133
+ }}
142
134
  >
143
- <FormFieldInput
144
- name="name"
145
- type="text"
146
- value={field.state.value}
147
- onChange={(event) => field.handleChange(event.target.value)}
148
- onBlur={field.handleBlur}
149
- />
150
- </FormField>
151
- )}
152
- </form.Field>
135
+ {(field) => (
136
+ <FormField
137
+ label="Workspace name"
138
+ id="workspace-settings-name"
139
+ error={fieldError(field)}
140
+ >
141
+ <FormFieldInput
142
+ name="name"
143
+ type="text"
144
+ value={field.state.value}
145
+ onChange={(event) => field.handleChange(event.target.value)}
146
+ onBlur={field.handleBlur}
147
+ />
148
+ </FormField>
149
+ )}
150
+ </form.Field>
153
151
 
154
- <form.Field
155
- name="slug"
156
- validators={{
157
- onBlur: createWorkspaceBodySchema.shape.slug,
158
- onSubmit: createWorkspaceBodySchema.shape.slug,
159
- }}
160
- >
161
- {(field) => (
162
- <SlugField
163
- id="workspace-settings-slug"
164
- label="Workspace slug"
152
+ <form.Field
165
153
  name="slug"
166
- value={field.state.value}
167
- onChange={(value) => field.handleChange(value)}
168
- onBlur={field.handleBlur}
169
- error={fieldError(field)}
170
- description={<span className="break-all font-code">/w/{field.state.value}</span>}
171
- placeholder="acme"
172
- className="font-code"
173
- currentSlug={workspace.slug}
174
- checkEnabled
175
- isValid={isSlugValid}
176
- checkAvailability={checkWorkspaceSlugAvailability}
177
- />
178
- )}
179
- </form.Field>
154
+ validators={{
155
+ onBlur: createWorkspaceBodySchema.shape.slug,
156
+ onSubmit: createWorkspaceBodySchema.shape.slug,
157
+ }}
158
+ >
159
+ {(field) => (
160
+ <SlugField
161
+ id="workspace-settings-slug"
162
+ label="Workspace slug"
163
+ name="slug"
164
+ value={field.state.value}
165
+ onChange={(value) => field.handleChange(value)}
166
+ onBlur={field.handleBlur}
167
+ error={fieldError(field)}
168
+ description={
169
+ <span className="break-all font-code">/w/{field.state.value}</span>
170
+ }
171
+ placeholder="acme"
172
+ className="font-code"
173
+ currentSlug={workspace.slug}
174
+ checkEnabled
175
+ isValid={isSlugValid}
176
+ checkAvailability={checkWorkspaceSlugAvailability}
177
+ />
178
+ )}
179
+ </form.Field>
180
180
 
181
- <Button type="submit" isLoading={updateWorkspace.isPending} className="self-start">
182
- Save changes
183
- </Button>
184
- </form>
181
+ <Button type="submit" isLoading={updateWorkspace.isPending} className="self-start">
182
+ Save changes
183
+ </Button>
184
+ </form>
185
+ </PanelBody>
186
+ </Panel>
185
187
  </div>
186
188
 
187
189
  <SlugChangeWarning