@sanity/workflow-studio 0.26.0 → 0.28.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/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # @sanity/workflow-studio
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6584554: **BREAKING:** assignment surfaces now read the project's role catalog, so a role
8
+ with no current holders is assignable and carries its project title.
9
+
10
+ Previously the only role vocabulary was the roles loaded members happened to
11
+ hold. A role the project defines but nobody has yet could not be offered at all,
12
+ and a role named by a stored value rendered as its bare machine name because no
13
+ membership record carried a title for it. Both are fixed by reading
14
+ `GET /projects/{projectId}/roles`. That read is gated on
15
+ `sanity-project-roles: read`, which the default user-facing project roles carry
16
+ down to viewer, so it is not expected to need an administrator.
17
+
18
+ The catalog also becomes the bound on what a picker offers, which subtracts as
19
+ well as adds: a role a member holds that the catalog does not list stops being
20
+ offerable, though it still labels correctly wherever it is already assigned. On
21
+ a complete catalog nothing is lost, since every held role is in it.
22
+
23
+ Three API changes, all in `@sanity/workflow-components`:
24
+ - `ProjectMembersState` gains a required `roles` field, the project's assignable
25
+ roles. Anything constructing this state by hand must supply it; `roles: []`
26
+ preserves the previous behaviour, since resolution falls back to the roles
27
+ members hold. `MemberPicker` does not take it — it lists members only, and its
28
+ props were narrowed to the three fields it reads.
29
+ - `memberRoleFor` and `roleLabelFor` take a `RoleVocabulary` (`{members, roles}`)
30
+ in place of a bare member array. The state either adapter returns satisfies it,
31
+ so a caller holding one can pass it straight through.
32
+ - `AssigneeBadge` and `AssigneeBadges` take `vocabulary` in place of `members`,
33
+ for the same reason.
34
+
35
+ Not migrating leaves a type error at each call site rather than a silent
36
+ behaviour change.
37
+
38
+ Hosts using `@sanity/workflow-studio` or `@sanity/workflow-sdk` get the catalog
39
+ without changes: both adapters fetch it beside the member list, each behind a
40
+ per-client, per-project cache, so it costs one extra request per project per page
41
+ load however many surfaces read it. A failed roles read is deliberately
42
+ non-fatal — it degrades to the previous member-derived behaviour rather than
43
+ taking down the member list, and therefore every assignment control, with it. It
44
+ is reported once on the console with its cause, because nothing downstream can
45
+ tell an unreadable catalog from a project that defines no assignable roles. That
46
+ empty result is cached too, so a project whose catalog cannot be read stays on
47
+ the member-derived behaviour until the page reloads.
48
+
49
+ **Docs impact:** Update the assignment concepts and any Studio plugin guidance
50
+ that describes which roles can be assigned, to say the catalog is the source,
51
+ that a role with no holders is offered, and that a held role missing from the
52
+ catalog is not. The `@sanity/workflow-components` README covers the new
53
+ `RoleVocabulary` shape and the picker's behaviour.
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [1e4a5da]
58
+ - Updated dependencies [6584554]
59
+ - Updated dependencies [f4405b4]
60
+ - Updated dependencies [5050b06]
61
+ - Updated dependencies [a044ba5]
62
+ - @sanity/workflow-engine@0.28.0
63
+ - @sanity/workflow-sdk@0.28.0
64
+ - @sanity/workflow-react@0.28.0
65
+
66
+ ## 0.27.0
67
+
68
+ ### Minor Changes
69
+
70
+ - 780cf93: **BREAKING:** `ProjectMember.roles` is now `readonly {name: string; title?: string}[]` instead of `readonly string[]`, and `AssigneeStack`'s `roles` prop takes the same records rather than name strings. This affects integrations that build or read `ProjectMember` rows directly, or render `AssigneeStack` themselves. Read `role.name` wherever a role name was read before, and pass role records rather than name strings into `projectMemberRow` and `AssigneeStack`. Until migrated, TypeScript fails at those sites. Integrations using `useProjectMembers` from `@sanity/workflow-sdk` or the Studio plugin's own equivalent get the new shape without changes, because the adapters own the projection.
71
+
72
+ A role now reads as its project title rather than its machine name — `Administrator`, not `administrator` — wherever one is shown: picker role rows, assignee badges, the instance-snapshot pills, and the hover hint naming a collapsed assignee cluster. A square role avatar takes its letters from the title too, so `Blueprints Deployer` reads as `BD` where `blueprints-deployer` could only ever give one letter; its colour still derives from the machine name, so retitling a project role keeps the role's colour. A role whose membership record carries no title — or a title that is only whitespace, which counts as none — and a role named by an assignee that no current member holds, both still read as the machine name. `roleLabel`, `roleLabelFor`, and `memberRoleFor` are exported for callers rendering their own role labels.
73
+
74
+ Both people-pickers now say who a person is, so choosing one no longer requires knowing the org chart. Search matches role names and titles as well as display name and email, so typing a role narrows the list to its holders. Members are also listed by display name rather than in the order the host supplied them, which was the sequence people joined the project — expect the list order to change, and on a large project to become usable. And hovering a row previews the account behind the name — a larger avatar badged with the person's identity provider, their email, and the roles they hold. Marks exist for Google, GitHub, and `saml-`-prefixed deployments; any other provider badges nothing rather than showing an empty circle. That is what tells two members sharing a display name apart, which no role label could. Rows share one tooltip delay group, so the wait to open is paid once and moving along the list swaps the preview rather than waiting again at each row.
75
+
76
+ `MemberAvatar` takes an optional `loginProvider` to badge the provider mark onto its corner. `ProjectMember` gains optional `loginProvider` and `isCurrentUser`; both are populated by the adapters, so integrations using `useProjectMembers` get them without changes.
77
+
78
+ Neither picker ranks or groups its member rows by the roles those members hold, and neither claims who is eligible for an activity. (The roles-and-members picker still lists selectable roles above the members, as it did before.) An activity has no role gate; only an action does, and a manually fired action's `roles` is folded into its `filter` at desugar, so the accepted set is not readable from a deployed definition. A picker has nothing to derive eligibility from, and a ranked list would assert something no engine or Content Lake check backs.
79
+
80
+ Stored values are unchanged. An assignee still persists the machine role name, and titles are display only.
81
+
82
+ **Docs impact:** Update the `@sanity/workflow-components` member-selection reference for the `ProjectMember.roles` and `AssigneeStack` `roles` shapes and the `roleLabel` / `roleLabelFor` / `memberRoleFor` exports (done in this package's README). Any concept or guide page showing a role in Studio UI should show titles rather than machine names, and the assignment guide should state that a picker never restricts or ranks who may be assigned. Add the `ProjectMember.roles` migration to the release notes.
83
+
84
+ ### Patch Changes
85
+
86
+ - Updated dependencies [780cf93]
87
+ - Updated dependencies [b7c8bed]
88
+ - @sanity/workflow-engine@0.27.0
89
+ - @sanity/workflow-sdk@0.27.0
90
+ - @sanity/workflow-react@0.27.0
91
+
3
92
  ## 0.26.0
4
93
 
5
94
  ### Patch Changes
package/README.md CHANGED
@@ -33,10 +33,16 @@ Studio source context (`useSource` / `useClient`).
33
33
  errors surface reactively through `error`.
34
34
  - `useStudioProjectUsers` — the shared Studio project-user directory used by
35
35
  actor resolution and member pickers. It returns the upstream membership and
36
- profile records without an Editorial Workflows projection, plus the engine's
37
- `ProjectUserDirectory` integration. Concurrent loads are coalesced, the
38
- settled successful directory is reused for the client/project lifetime, and
39
- failed loads retry after a 30-second backoff. A directory that has not
36
+ profile records without an Editorial Workflows projection, the project's
37
+ assignable `roles`, and the engine's `ProjectUserDirectory` integration. The
38
+ role catalog is read alongside the memberships and is the only account of a
39
+ role nobody currently holds; it is filtered to roles a person can be assigned,
40
+ and it is empty when the read is unavailable, which leaves role labels to the
41
+ roles members hold and is reported once on the console with its cause.
42
+ Concurrent loads are coalesced, the settled successful directory is reused for
43
+ the client/project lifetime, and a failed membership load retries after a
44
+ 30-second backoff. That backoff does not cover the catalog: an unavailable one
45
+ is held empty for the client's lifetime rather than re-read. A directory that has not
40
46
  answered reports `loading`, including before its fetch starts (the fetch
41
47
  begins in a consumer's effect), so an empty `users` with `loading: false`
42
48
  and no `error` is a confirmed "no members" rather than a directory nobody
package/dist/index.cjs CHANGED
@@ -194,11 +194,11 @@ function isRecord(value) {
194
194
  }
195
195
 
196
196
  function isRole(value) {
197
- return isRecord(value) && typeof value.name == "string";
197
+ return isRecord(value) && typeof value.name == "string" && (value.title === void 0 || typeof value.title == "string");
198
198
  }
199
199
 
200
200
  function isMembership(value) {
201
- return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
201
+ return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.isCurrentUser === void 0 || typeof value.isCurrentUser == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
202
202
  }
203
203
 
204
204
  async function fetchProjectUsers(client, projectId) {
@@ -226,6 +226,7 @@ const projectUsersByClient = /* @__PURE__ */ new WeakMap;
226
226
  function createProjectUsersStore(client, projectId) {
227
227
  let snapshot = {
228
228
  users: [],
229
+ roles: [],
229
230
  loading: !0,
230
231
  error: void 0
231
232
  }, pending, retryAt = 0, usersById = /* @__PURE__ */ new Map, usersByEmail = /* @__PURE__ */ new Map;
@@ -238,18 +239,23 @@ function createProjectUsersStore(client, projectId) {
238
239
  ...snapshot,
239
240
  loading: !0,
240
241
  error: void 0
241
- }), pending = fetchProjectUsers(client, projectId).then(users => (usersById = new Map(users.flatMap(user => {
242
+ }), pending = Promise.all([ fetchProjectUsers(client, projectId), projectUsers.loadProjectRolesOrNone({
243
+ client: client,
244
+ projectId: projectId
245
+ }) ]).then(([users, roles]) => (usersById = new Map(users.flatMap(user => {
242
246
  const entries = [ [ user.membership.id, user ] ], globalId = user.profile?.sanityUserId;
243
247
  return globalId !== void 0 && globalId !== user.membership.id && entries.push([ globalId, user ]),
244
248
  entries;
245
249
  })), usersByEmail = new Map(users.flatMap(user => user.profile?.email ? [ [ user.profile.email.toLowerCase(), user ] ] : [])),
246
250
  retryAt = 1 / 0, setSnapshot({
247
251
  users: users,
252
+ roles: roles,
248
253
  loading: !1,
249
254
  error: void 0
250
255
  }), pending = void 0, users), error => {
251
256
  throw retryAt = Date.now() + PROJECT_USERS_RETRY_MS, setSnapshot({
252
257
  users: [],
258
+ roles: [],
253
259
  loading: !1,
254
260
  error: error
255
261
  }), pending = void 0, error;
@@ -316,6 +322,7 @@ function studioProjectUserDirectory(client, projectId) {
316
322
 
317
323
  const missingProjectSnapshot = {
318
324
  users: [],
325
+ roles: [],
319
326
  loading: !1,
320
327
  error: new Error("No projectId on the Studio client")
321
328
  }, noopSubscribe = () => () => {};
package/dist/index.d.cts CHANGED
@@ -6,6 +6,7 @@ import type { GdrUri } from "@sanity/workflow-engine";
6
6
  import type { InstancesQueryFilter } from "@sanity/workflow-engine";
7
7
  import { MissingHandlerPolicy } from "@sanity/workflow-engine";
8
8
  import { MutationGuardDoc } from "@sanity/workflow-react";
9
+ import { ProjectRole } from "@sanity/workflow-sdk/project-users";
9
10
  import { ProjectUserDirectory } from "@sanity/workflow-engine";
10
11
  import { ProjectUserProfile } from "@sanity/workflow-sdk/project-users";
11
12
  import { ResourceClientResolver } from "@sanity/workflow-engine";
@@ -39,8 +40,10 @@ export declare interface StudioObserverOptions {
39
40
  export declare interface StudioProjectMembership {
40
41
  readonly id: string;
41
42
  readonly isRobot?: boolean;
43
+ readonly isCurrentUser?: boolean;
42
44
  readonly roles?: readonly {
43
45
  readonly name: string;
46
+ readonly title?: string;
44
47
  }[];
45
48
  readonly [key: string]: unknown;
46
49
  }
@@ -59,6 +62,9 @@ export declare function studioProjectUserDirectory(
59
62
 
60
63
  export declare interface StudioProjectUsersState {
61
64
  readonly users: readonly StudioProjectUser[];
65
+ /** The project's assignable roles, whether or not anyone holds them. Empty
66
+ * when the read is unavailable — see {@link loadProjectRolesOrNone}. */
67
+ readonly roles: readonly ProjectRole[];
62
68
  /** True while the directory has not answered, including before its load
63
69
  * starts — the fetch begins in a consumer's effect or the first directory
64
70
  * lookup, one commit after the first render reads this state. An empty
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { GdrUri } from "@sanity/workflow-engine";
6
6
  import type { InstancesQueryFilter } from "@sanity/workflow-engine";
7
7
  import { MissingHandlerPolicy } from "@sanity/workflow-engine";
8
8
  import { MutationGuardDoc } from "@sanity/workflow-react";
9
+ import { ProjectRole } from "@sanity/workflow-sdk/project-users";
9
10
  import { ProjectUserDirectory } from "@sanity/workflow-engine";
10
11
  import { ProjectUserProfile } from "@sanity/workflow-sdk/project-users";
11
12
  import { ResourceClientResolver } from "@sanity/workflow-engine";
@@ -39,8 +40,10 @@ export declare interface StudioObserverOptions {
39
40
  export declare interface StudioProjectMembership {
40
41
  readonly id: string;
41
42
  readonly isRobot?: boolean;
43
+ readonly isCurrentUser?: boolean;
42
44
  readonly roles?: readonly {
43
45
  readonly name: string;
46
+ readonly title?: string;
44
47
  }[];
45
48
  readonly [key: string]: unknown;
46
49
  }
@@ -59,6 +62,9 @@ export declare function studioProjectUserDirectory(
59
62
 
60
63
  export declare interface StudioProjectUsersState {
61
64
  readonly users: readonly StudioProjectUser[];
65
+ /** The project's assignable roles, whether or not anyone holds them. Empty
66
+ * when the read is unavailable — see {@link loadProjectRolesOrNone}. */
67
+ readonly roles: readonly ProjectRole[];
62
68
  /** True while the directory has not answered, including before its load
63
69
  * starts — the fetch begins in a consumer's effect or the first directory
64
70
  * lookup, one commit after the first render reads this state. An empty
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { useSource, useClient } from "sanity";
12
12
 
13
13
  import { useWorkflowTelemetry, useWorkflowSession as useWorkflowSession$1, useDocumentWorkflows as useDocumentWorkflows$1, useWorkflowInstances as useWorkflowInstances$1 } from "@sanity/workflow-react";
14
14
 
15
- import { loadProjectUserProfiles } from "@sanity/workflow-sdk/project-users";
15
+ import { loadProjectRolesOrNone, loadProjectUserProfiles } from "@sanity/workflow-sdk/project-users";
16
16
 
17
17
  function makeStudioObserver(sdk, options) {
18
18
  const {client: client, engineResource: engineResource} = options;
@@ -202,11 +202,11 @@ function isRecord(value) {
202
202
  }
203
203
 
204
204
  function isRole(value) {
205
- return isRecord(value) && typeof value.name == "string";
205
+ return isRecord(value) && typeof value.name == "string" && (value.title === void 0 || typeof value.title == "string");
206
206
  }
207
207
 
208
208
  function isMembership(value) {
209
- return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
209
+ return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.isCurrentUser === void 0 || typeof value.isCurrentUser == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
210
210
  }
211
211
 
212
212
  async function fetchProjectUsers(client, projectId) {
@@ -234,6 +234,7 @@ const projectUsersByClient = /* @__PURE__ */ new WeakMap;
234
234
  function createProjectUsersStore(client, projectId) {
235
235
  let snapshot = {
236
236
  users: [],
237
+ roles: [],
237
238
  loading: !0,
238
239
  error: void 0
239
240
  }, pending, retryAt = 0, usersById = /* @__PURE__ */ new Map, usersByEmail = /* @__PURE__ */ new Map;
@@ -246,18 +247,23 @@ function createProjectUsersStore(client, projectId) {
246
247
  ...snapshot,
247
248
  loading: !0,
248
249
  error: void 0
249
- }), pending = fetchProjectUsers(client, projectId).then(users => (usersById = new Map(users.flatMap(user => {
250
+ }), pending = Promise.all([ fetchProjectUsers(client, projectId), loadProjectRolesOrNone({
251
+ client: client,
252
+ projectId: projectId
253
+ }) ]).then(([users, roles]) => (usersById = new Map(users.flatMap(user => {
250
254
  const entries = [ [ user.membership.id, user ] ], globalId = user.profile?.sanityUserId;
251
255
  return globalId !== void 0 && globalId !== user.membership.id && entries.push([ globalId, user ]),
252
256
  entries;
253
257
  })), usersByEmail = new Map(users.flatMap(user => user.profile?.email ? [ [ user.profile.email.toLowerCase(), user ] ] : [])),
254
258
  retryAt = 1 / 0, setSnapshot({
255
259
  users: users,
260
+ roles: roles,
256
261
  loading: !1,
257
262
  error: void 0
258
263
  }), pending = void 0, users), error => {
259
264
  throw retryAt = Date.now() + PROJECT_USERS_RETRY_MS, setSnapshot({
260
265
  users: [],
266
+ roles: [],
261
267
  loading: !1,
262
268
  error: error
263
269
  }), pending = void 0, error;
@@ -324,6 +330,7 @@ function studioProjectUserDirectory(client, projectId) {
324
330
 
325
331
  const missingProjectSnapshot = {
326
332
  users: [],
333
+ roles: [],
327
334
  loading: !1,
328
335
  error: new Error("No projectId on the Studio client")
329
336
  }, noopSubscribe = () => () => {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/workflow-studio",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "React adapter that drives the @sanity/workflow-engine reactive session in Sanity Studio through the App SDK store.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -50,17 +50,17 @@
50
50
  "react-dom": "^19.2.7",
51
51
  "sanity": "^6",
52
52
  "vitest": "^4.1.8",
53
- "@sanity/workflow-engine": "0.26.0",
54
- "@sanity/workflow-sdk": "0.26.0",
55
- "@sanity/workflow-react": "0.26.0"
53
+ "@sanity/workflow-engine": "0.28.0",
54
+ "@sanity/workflow-react": "0.28.0",
55
+ "@sanity/workflow-sdk": "0.28.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@sanity/sdk": "^2.12.0",
59
59
  "react": "^19.2.7",
60
60
  "sanity": "^6",
61
- "@sanity/workflow-react": "0.26.0",
62
- "@sanity/workflow-engine": "0.26.0",
63
- "@sanity/workflow-sdk": "0.26.0"
61
+ "@sanity/workflow-engine": "0.28.0",
62
+ "@sanity/workflow-react": "0.28.0",
63
+ "@sanity/workflow-sdk": "0.28.0"
64
64
  },
65
65
  "engines": {
66
66
  "node": ">=20"