New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@sanity/workflow-components

Package Overview
Dependencies
Maintainers
2
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sanity/workflow-components

Reusable React components for Workflows member, assignee, and date fields.

latest
Source
npmnpm
Version
0.37.0
Version published
Weekly downloads
4.6K
168.44%
Maintainers
2
Weekly downloads
 
Created
Source

@sanity/workflow-components

Reusable React controls for Workflows member, assignee, and date fields. The package uses @sanity/ui but has no dependency on Sanity Studio or the App SDK, so the same components can be used in either integration.

Render the components inside a Sanity UI ThemeProvider, with @sanity/ui/styles.css loaded: Sanity Studio loads it, and an App SDK or other React host imports it once at its entry. Member controls take account-global user ids (sanityUserId) — the one identity namespace @sanity/workflow-engine stores and compares. The integration owns the join from its host's project-scoped member records to these rows — for example useProjectMembers in @sanity/workflow-sdk; the Studio plugin ships its own equivalent — built on this package's projectMemberRow.

Member selection

Supply the project directory explicitly. App SDK integrations can pass the result of useProjectMembers(projectId) from @sanity/workflow-sdk; Studio integrations can adapt their existing project-member query to the same shape.

import {MemberPicker, type ProjectMember} from '@sanity/workflow-components'

const members: ProjectMember[] = [
  {
    id: 'gAccountUserId',
    displayName: 'Ada Lovelace',
    email: 'ada@example.com',
    roles: [{name: 'editor', title: 'Editor'}],
  },
]

export function OwnerPicker() {
  return (
    <MemberPicker
      error={undefined}
      loading={false}
      members={members}
      onSelect={(member) => console.log(member?.id)}
      selectedIds={new Set()}
      unassignRow
    />
  )
}

MemberPicker reports a ProjectMember or null for the optional unassigned row. AssigneePicker is a controlled multi-value input: value is the current engine Assignee array and onChange receives the complete next array after a role or member is toggled. ActorBadge, ActorBadges, MemberAvatar, and MemberAvatarGroup provide matching read-only presentation. The badges take an AssigneeVocabulary: the ProjectMembersState you already hold, optionally with robots. A role renders as a badge; a user renders as ActorName, a face beside the name.

AssigneeStack presents a scope's whole assignment as one overlapping cluster for a dense list row: members render as round avatars, roles as square ones through RoleAvatar, which initials a role from its label and colours it from its name. actors take resolved actors and render after members as ActorAvatar draws them, and glyphs render after roles as icon faces. The cluster shows at most three slots, the overflow count included, and earlier entries collapse first: members, then actors, then roles, then glyphs. Both take display data only — resolving user ids to people, and naming everyone in full once the cluster abbreviates, stay with the integration.

Neither avatar renders a tooltip or carries an accessible name of its own, so a host rendering RoleAvatar outside AssigneeStack owns naming it.

Showing who acted

A workflow stores people as IDs: actor stamps on history and notes, actor field values, and user assignees. actorDisplayFor(directory, id) turns one into an ActorDisplay against an ActorDirectory of members, optional robots, loading, and error: the ProjectMembersState you already hold satisfies it. It never returns the ID as the name:

kindWhenname
personA member with a resolved profileThe profile name
robotA robot-token member listed in robots with a token labelThe token's label
automationA robot member without a label, or an unlisted p- robot or system ID, even when error is setAutomation
unknownAnyone else: a departed member, or a profile that did not loadUnknown user
unavailableerror is set and the directory does not list the ID, except robot and system IDsCouldn't load
pendingloading is true and the directory does not list the IDEmpty

The ID matches a member by its account-global id or by its membershipId, so stamps that carry a project membership ID resolve too. ActorName renders the face and name on one line with ActorHintContent on hover: the member preview for a person, otherwise the label, what it is, and the stored ID. That hover is the only place the ID appears. ActorFace is the face with the same hover, and ActorAvatar the face alone. ActorNames renders several actors as inline comma-separated text for a sentence or a shared hover, with distinguishActors appending the sign-in provider to a name two people share. A pending actor has no hover; its name is a name-sized ActorNameSkeleton and its face an empty avatar, so the line keeps its size when the name arrives.

Roles on a member

A ProjectMember's roles carry both spellings a host supplies. name is the machine name: the only spelling the engine stores in an assignee value and compares against an actor's roles. title is the project's human label, present when the host's membership records include one. Every surface showing a role reads the title, falling back to the name when there is no title, so a record without one reads as its machine name rather than as a blank label.

ProjectMembersState.roles is the project's own catalog of assignable roles, supplied by the host alongside the members. It is the authority on a role's title and the only account of a role nobody currently holds, which no membership record can carry. A RoleVocabulary — the catalog plus the members — is what role resolution takes.

roleLabel resolves the name/title pair. memberRoleFor and roleLabelFor resolve the other direction, from a name stored in an assignee value back to the role record or straight to its label. The catalog answers first, and any name it does not account for falls through to the roles members hold — whether the catalog is still loading, could not be read, or simply omits that name — so a label degrades to a worse title rather than to none. A name in neither resolves to itself.

roleMemberCount reports the distinct people who can fulfill any supplied role under a workflow definition's aliases. Pass members, requiredRoles, and the definition's normalized roleAliases; the result includes alias-only fulfillers and deduplicates them by account-global user id. It does not expand or rewrite a member's literal roles, which remain the source for profile display and search.

Without eligibleRoles, AssigneePicker offers the whole catalog, so a role with no holders is assignable. It always merges in any name the current value carries that the catalog does not account for — a role deleted since it was assigned would otherwise have no row, and so no way to be seen or removed. Those value-derived rows sort in by label like every other one, and stay listed for the picker's lifetime once present, so clearing one does not delete the row under the pointer; a host that renders the picker in a popover gets them dropped on the next open, since closing unmounts the content. A host mounting it inline should remount it (or change its key) to get the same reset. MemberPicker takes no catalog: it lists members only.

Both pickers match role names and titles in their search, so typing a role narrows the list to its holders, and both list members by display name. A host's own member order is the sequence people joined the project, which on a large project puts whoever joined first at the top rather than anyone findable.

A picker row shows a name and nothing else, because a name is not enough to tell two members apart — the account behind it lives in MemberPreview, which the row previews beside itself on hover: a larger avatar badged with the person's identity provider, their email, and the roles they hold. Rows share one tooltip delay group, so the wait is paid once and moving along the list swaps the preview instead of making the reader wait again at every row.

The preview belongs to the pointer and does not follow the keyboard-active row. Driving it from both reads as broken from either: with the keyboard holding one row, moving the mouse onto another leaves the first preview up through the hover delay, so the reader watches the wrong card and then a jump. The active row has its own highlight.

MemberAvatar takes an optional loginProvider to badge the mark onto its corner; leave it off in a dense list, where the mark is larger than the corner it would sit in.

Marks exist for Google, GitHub, and any saml--prefixed deployment, which reads as an SSO wordmark. Any other provider badges nothing at all — no mark and no circle — so an unrecognised value costs a reader nothing rather than showing an empty badge.

Beyond the value-derived rows described above, neither picker reorders or re-sections its list in response to the value: selecting a catalogued role or a member decides what is checkmarked and nothing else. AssigneePicker can opt into field-value eligibility with eligibleRoles and roleAliases. It then offers users who fulfil at least one listed role and only the listed collective roles. The picker names the active eligible roles and distinguishes an empty eligible set from a search with no matches. A current value that no longer qualifies stays visible so an editor can remove or replace it. This is distinct from action permission and field editability; callers should pass eligibility only from an assignment field's declared roles facet.

Dates

DatePicker emits local JavaScript Date values after complete calendar or time gestures. Storage remains the caller's responsibility, so the control works with an engine field edit, an action parameter, or local application state.

import {ClearableDatePicker} from '@sanity/workflow-components'
;<ClearableDatePicker
  clearLabel="Clear due date"
  onClear={() => setDueDate(undefined)}
  onPick={setDueDate}
  value={dueDate}
/>

Set selectTime when time-of-day is part of the workflow field.

Workflow stamps and status

AgoStamp takes an ISO timestamp in at and a caller-owned now: Date. It shows a compact age, with the absolute local timestamp in a tooltip. Update now to refresh it; it does not start its own timer. Invalid timestamps show an em dash, and future timestamps show "just now".

ActivityStatusIcon displays active, done, failed, and skipped activity states. Unknown strings use a muted empty icon. Supply an accessible label in the surrounding UI when the icon is the only status indicator. StartIncompleteBadge marks an instance whose start has not finished.

Overview and For me tables are exported by @sanity/workflow-studio-plugin. They require Studio providers and remain part of the Studio integration. Exports whose names start with _ are unsupported integration internals.

Keywords

react

FAQs

Package last updated on 26 Oct 2026

Related posts