sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.4

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 (50) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +73 -12
  3. package/lib/{src/index.d.ts → index.d.ts} +3 -3
  4. package/lib/index.esm.js +1800 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +1813 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +51 -40
  9. package/src/actions/AssignWorkflow.tsx +48 -0
  10. package/src/actions/BeginWorkflow.tsx +68 -0
  11. package/src/actions/CompleteWorkflow.tsx +41 -0
  12. package/src/actions/RequestReviewAction.js +1 -7
  13. package/src/actions/UpdateWorkflow.tsx +142 -0
  14. package/src/badges/AssigneesBadge.tsx +52 -0
  15. package/src/badges/{index.tsx → StateBadge.tsx} +4 -8
  16. package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
  17. package/src/components/DocumentCard/CompleteButton.tsx +53 -0
  18. package/src/components/DocumentCard/EditButton.tsx +3 -2
  19. package/src/components/DocumentCard/Field.tsx +38 -0
  20. package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
  21. package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
  22. package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +156 -50
  25. package/src/components/DocumentList.tsx +122 -0
  26. package/src/components/Filters.tsx +168 -0
  27. package/src/components/FloatingCard.tsx +29 -0
  28. package/src/components/StateTitle/Status.tsx +27 -0
  29. package/src/components/StateTitle/index.tsx +73 -0
  30. package/src/components/UserAssignment.tsx +57 -75
  31. package/src/components/UserAssignmentInput.tsx +27 -0
  32. package/src/components/UserDisplay.tsx +57 -0
  33. package/src/components/Validators.tsx +229 -0
  34. package/src/components/WorkflowTool.tsx +302 -163
  35. package/src/constants/index.ts +31 -0
  36. package/src/helpers/arraysContainMatchingString.ts +6 -0
  37. package/src/helpers/filterItemsAndSort.ts +41 -0
  38. package/src/helpers/initialRank.ts +13 -0
  39. package/src/hooks/useWorkflowDocuments.tsx +62 -70
  40. package/src/hooks/useWorkflowMetadata.tsx +0 -1
  41. package/src/index.ts +38 -58
  42. package/src/schema/workflow/workflow.metadata.ts +68 -0
  43. package/src/tools/index.ts +15 -0
  44. package/src/types/index.ts +27 -6
  45. package/src/actions/DemoteAction.tsx +0 -62
  46. package/src/actions/PromoteAction.tsx +0 -62
  47. package/src/components/Mutate.tsx +0 -54
  48. package/src/components/StateTimeline.tsx +0 -98
  49. package/src/components/UserSelectInput.tsx +0 -43
  50. package/src/schema/workflow/metadata.ts +0 -38
@@ -0,0 +1,68 @@
1
+ import {useCallback, useState} from 'react'
2
+ import {SplitVerticalIcon} from '@sanity/icons'
3
+ import {DocumentActionProps, useClient} from 'sanity'
4
+ import {useToast} from '@sanity/ui'
5
+ import {LexoRank} from 'lexorank'
6
+
7
+ import {API_VERSION} from '../constants'
8
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
9
+ import {State} from '../types'
10
+
11
+ export function BeginWorkflow(props: DocumentActionProps, states: State[]) {
12
+ const {id, draft} = props
13
+ const {data, loading, error} = useWorkflowMetadata(id, states)
14
+ const client = useClient({apiVersion: API_VERSION})
15
+ const toast = useToast()
16
+ const [beginning, setBeginning] = useState(false)
17
+ const [complete, setComplete] = useState(false)
18
+
19
+ if (error) {
20
+ console.error(error)
21
+ }
22
+
23
+ const handle = useCallback(async () => {
24
+ setBeginning(true)
25
+ const lowestOrderFirstState = await client.fetch(
26
+ `*[_type == "workflow.metadata" && state == $state]|order(orderRank)[0].orderRank`,
27
+ {state: states[0].id}
28
+ )
29
+ client
30
+ .createIfNotExists(
31
+ {
32
+ _id: `workflow-metadata.${id}`,
33
+ _type: `workflow.metadata`,
34
+ documentId: id,
35
+ state: states[0].id,
36
+ orderRank: lowestOrderFirstState
37
+ ? LexoRank.parse(lowestOrderFirstState).genNext().toString()
38
+ : LexoRank.min().toString(),
39
+ },
40
+ // Faster!
41
+ {visibility: 'async'}
42
+ )
43
+ .then(() => {
44
+ toast.push({
45
+ status: 'success',
46
+ title: 'Workflow started',
47
+ description: `Document is now "${states[0].title}"`,
48
+ })
49
+ setBeginning(false)
50
+ // Optimistically remove action
51
+ setComplete(true)
52
+ })
53
+ }, [id, states, client, toast])
54
+
55
+ if (!draft || complete || data.metadata) {
56
+ return null
57
+ }
58
+
59
+ return {
60
+ icon: SplitVerticalIcon,
61
+ type: 'dialog',
62
+ disabled: data?.metadata || loading || error || beginning || complete,
63
+ label: beginning ? `Beginning...` : `Begin Workflow`,
64
+ onHandle: () => {
65
+ handle()
66
+ },
67
+ }
68
+ }
@@ -0,0 +1,41 @@
1
+ import {useCallback} from 'react'
2
+ import {CheckmarkIcon} from '@sanity/icons'
3
+ import {DocumentActionProps, useClient} from 'sanity'
4
+
5
+ import {API_VERSION} from '../constants'
6
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
7
+ import {State} from '../types'
8
+
9
+ export function CompleteWorkflow(props: DocumentActionProps, states: State[]) {
10
+ const {id} = props
11
+ const {data, loading, error} = useWorkflowMetadata(id, states)
12
+ const client = useClient({apiVersion: API_VERSION})
13
+
14
+ if (error) {
15
+ console.error(error)
16
+ }
17
+
18
+ const handle = useCallback(() => {
19
+ client.delete(`workflow-metadata.${id}`)
20
+ }, [id, client])
21
+
22
+ const isLastState = data?.state?.id === states[states.length - 1].id
23
+
24
+ if (!data.metadata) {
25
+ return null
26
+ }
27
+
28
+ return {
29
+ icon: CheckmarkIcon,
30
+ type: 'dialog',
31
+ disabled: loading || error || !isLastState,
32
+ label: `Complete Workflow`,
33
+ title: isLastState
34
+ ? `Removes the document from the Workflow process`
35
+ : `Cannot remove from workflow until in the last state`,
36
+ onHandle: () => {
37
+ handle()
38
+ },
39
+ color: 'positive',
40
+ }
41
+ }
@@ -38,13 +38,7 @@ export function RequestReviewAction(props) {
38
38
  return {
39
39
  dialog: showWizardDialog && {
40
40
  type: 'popover',
41
- content: (
42
- <RequestReviewWizard
43
- metadata={metadata.data}
44
- onClose={onClose}
45
- onSend={onSend}
46
- />
47
- ),
41
+ content: <RequestReviewWizard metadata={metadata.data} onClose={onClose} onSend={onSend} />,
48
42
  onClose: props.onComplete,
49
43
  },
50
44
  disabled: showWizardDialog,
@@ -0,0 +1,142 @@
1
+ // import {useState} from 'react'
2
+ import {ArrowRightIcon, ArrowLeftIcon} from '@sanity/icons'
3
+ import {useToast} from '@sanity/ui'
4
+ import {useCurrentUser, useValidationStatus} from 'sanity'
5
+ import {DocumentActionProps, useClient} from 'sanity'
6
+
7
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
8
+ import {API_VERSION} from '../constants'
9
+ import {State} from '../types'
10
+ import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
11
+
12
+ export function UpdateWorkflow(
13
+ props: DocumentActionProps,
14
+ allStates: State[],
15
+ actionState: State
16
+ ) {
17
+ const {id, type} = props
18
+ const {validation, isValidating} = useValidationStatus(id, type)
19
+ const hasValidationErrors =
20
+ !isValidating &&
21
+ validation?.length > 0 &&
22
+ validation.find((v) => v.level === 'error')
23
+
24
+ const user = useCurrentUser()
25
+ const client = useClient({apiVersion: API_VERSION})
26
+ const toast = useToast()
27
+ const currentUser = useCurrentUser()
28
+
29
+ const {data, loading, error} = useWorkflowMetadata(id, allStates)
30
+ const {state: currentState} = data
31
+ const {assignees = []} = data?.metadata ?? {}
32
+
33
+ if (error) {
34
+ console.error(error)
35
+ }
36
+
37
+ const onHandle = (documentId: string, newState: State) => {
38
+ client
39
+ .patch(`workflow-metadata.${documentId}`)
40
+ .set({state: newState.id})
41
+ .commit()
42
+ .then(() => {
43
+ props.onComplete()
44
+ toast.push({
45
+ status: 'success',
46
+ title: `Document state now "${newState.title}"`,
47
+ })
48
+ // Perform document operations after State changes
49
+ // If State has changed and the document needs to be un/published
50
+ // This functionality was deemed too dangerous / unexpected
51
+ // Revisit with improved UX
52
+ // if (!ops.publish.disabled && nextOperation === 'publish') {
53
+ // ops.publish.execute()
54
+ // } else if (!ops.unpublish.disabled && nextOperation === 'unpublish') {
55
+ // ops.unpublish.execute()
56
+ // }
57
+ })
58
+ .catch((err) => {
59
+ props.onComplete()
60
+ console.error(err)
61
+ toast.push({
62
+ status: 'error',
63
+ title: `Document state update failed`,
64
+ })
65
+ })
66
+ }
67
+
68
+ // Remove button if:
69
+ // Document is not in Workflow OR
70
+ // The current State is the same as this actions State
71
+ if (!data.metadata || (currentState && currentState.id === actionState.id)) {
72
+ return null
73
+ }
74
+
75
+ const currentStateIndex = allStates.findIndex(
76
+ (s) => s.id === currentState?.id
77
+ )
78
+ const actionStateIndex = allStates.findIndex((s) => s.id === actionState.id)
79
+ const direction = actionStateIndex > currentStateIndex ? 'promote' : 'demote'
80
+ const DirectionIcon = direction === 'promote' ? ArrowRightIcon : ArrowLeftIcon
81
+ const directionLabel = direction === 'promote' ? 'Promote' : 'Demote'
82
+
83
+ let title = `${directionLabel} State to "${actionState.title}"`
84
+
85
+ const userRoleCanUpdateState =
86
+ user?.roles?.length && actionState?.roles?.length
87
+ ? // If the Action state is limited to specific roles
88
+ // check that the current user has one of those roles
89
+ arraysContainMatchingString(
90
+ user.roles.map((r) => r.name),
91
+ actionState.roles
92
+ )
93
+ : // No roles specified on the next state, so anyone can update
94
+ actionState?.roles?.length !== 0
95
+
96
+ if (!userRoleCanUpdateState) {
97
+ title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
98
+ }
99
+
100
+ const actionStateIsAValidTransition =
101
+ currentState?.id && currentState.transitions.length
102
+ ? // If the Current State limits transitions to specific States
103
+ // Check that the Action State is in Current State's transitions array
104
+ currentState.transitions.includes(actionState.id)
105
+ : // Otherwise this isn't a problem
106
+ true
107
+
108
+ if (!actionStateIsAValidTransition) {
109
+ title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
110
+ }
111
+
112
+ const userAssignmentCanUpdateState = actionState.requireAssignment
113
+ ? // If the Action State requires assigned users
114
+ // Check the current user ID is in the assignees array
115
+ currentUser && assignees.length && assignees.includes(currentUser.id)
116
+ : // Otherwise this isn't a problem
117
+ true
118
+
119
+ if (!userAssignmentCanUpdateState) {
120
+ title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
121
+ }
122
+
123
+ if (hasValidationErrors) {
124
+ title = `Document has validation errors, cannot ${directionLabel} State to "${actionState.title}"`
125
+ }
126
+
127
+ return {
128
+ icon: DirectionIcon,
129
+ disabled:
130
+ loading ||
131
+ error ||
132
+ isValidating ||
133
+ hasValidationErrors ||
134
+ !currentState ||
135
+ !userRoleCanUpdateState ||
136
+ !actionStateIsAValidTransition ||
137
+ !userAssignmentCanUpdateState,
138
+ title,
139
+ label: actionState.title,
140
+ onHandle: () => onHandle(id, actionState),
141
+ }
142
+ }
@@ -0,0 +1,52 @@
1
+ import {CurrentUser, DocumentBadgeDescription} from 'sanity'
2
+ import {useProjectUsers} from 'sanity-plugin-utils'
3
+ import {API_VERSION} from '../constants'
4
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
5
+
6
+ import {State} from '../types'
7
+
8
+ export function AssigneesBadge(
9
+ states: State[],
10
+ documentId: string,
11
+ currentUser: CurrentUser | null
12
+ ): DocumentBadgeDescription | null {
13
+ const {data, loading, error} = useWorkflowMetadata(documentId, states)
14
+ const {metadata} = data
15
+ const userList = useProjectUsers({apiVersion: API_VERSION})
16
+
17
+ if (loading || error || !metadata) {
18
+ if (error) {
19
+ console.error(error)
20
+ }
21
+
22
+ return null
23
+ }
24
+
25
+ if (!metadata?.assignees?.length) {
26
+ return {
27
+ label: 'Unassigned',
28
+ }
29
+ }
30
+
31
+ const {assignees} = metadata ?? []
32
+ const hasMe = currentUser ? assignees.some((assignee) => assignee === currentUser.id) : false
33
+ const assigneesCount = hasMe ? assignees.length - 1 : assignees.length
34
+ const assigneeUsers = userList.filter((user) => assignees.includes(user.id))
35
+ const title = assigneeUsers.map((user) => user.displayName).join(', ')
36
+
37
+ let label
38
+
39
+ if (hasMe && assigneesCount === 0) {
40
+ label = 'Assigned to Me'
41
+ } else if (hasMe && assigneesCount > 0) {
42
+ label = `Me and ${assigneesCount} ${assigneesCount === 1 ? 'other' : 'others'}`
43
+ } else {
44
+ label = `${assigneesCount} assigned`
45
+ }
46
+
47
+ return {
48
+ label,
49
+ title,
50
+ color: 'primary',
51
+ }
52
+ }
@@ -1,14 +1,10 @@
1
- import {DocumentBadgeDescription, DocumentBadgeProps} from 'sanity'
1
+ import {DocumentBadgeDescription} from 'sanity'
2
2
  import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
3
3
 
4
4
  import {State} from '../types'
5
5
 
6
- export function StateBadge(
7
- props: DocumentBadgeProps,
8
- states: State[]
9
- ): DocumentBadgeDescription | null {
10
- const {id} = props
11
- const {data, loading, error} = useWorkflowMetadata(id, states)
6
+ export function StateBadge(states: State[], documentId: string): DocumentBadgeDescription | null {
7
+ const {data, loading, error} = useWorkflowMetadata(documentId, states)
12
8
  const {state} = data
13
9
 
14
10
  if (loading || error) {
@@ -25,7 +21,7 @@ export function StateBadge(
25
21
 
26
22
  return {
27
23
  label: state.title,
28
- title: state.title,
24
+ // title: state.title,
29
25
  color: state?.color,
30
26
  }
31
27
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import {Box, Flex, Text} from '@sanity/ui'
3
- import {UserAvatar} from 'sanity'
3
+ import {useCurrentUser, UserAvatar} from 'sanity'
4
4
 
5
5
  import {User} from '../../types'
6
6
 
@@ -10,22 +10,26 @@ type AvatarGroupProps = {
10
10
  }
11
11
 
12
12
  export default function AvatarGroup(props: AvatarGroupProps) {
13
- const {users, max = 3} = props
13
+ const currentUser = useCurrentUser()
14
+ const {users, max = 4} = props
14
15
 
15
16
  const len = users?.length
16
- const visibleUsers = React.useMemo(
17
- () => users.slice(0, max),
18
- [users]
19
- ) as User[]
17
+ const {me, visibleUsers} = React.useMemo(() => {
18
+ return {
19
+ me: currentUser?.id ? users.find((u) => u.id === currentUser.id) : undefined,
20
+ visibleUsers: users.filter((u) => u.id !== currentUser?.id).slice(0, max - 1),
21
+ }
22
+ }, [users, max, currentUser])
20
23
 
21
24
  if (!users?.length) {
22
25
  return null
23
26
  }
24
27
 
25
28
  return (
26
- <Flex align="center">
29
+ <Flex align="center" gap={1}>
30
+ {me ? <UserAvatar user={me} /> : null}
27
31
  {visibleUsers.map((user) => (
28
- <Box key={user.id} style={{marginRight: -5}}>
32
+ <Box key={user.id} style={{marginRight: -8}}>
29
33
  <UserAvatar user={user} />
30
34
  </Box>
31
35
  ))}
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import {Button, useToast} from '@sanity/ui'
3
+ import {CheckmarkIcon} from '@sanity/icons'
4
+ import {useClient} from 'sanity'
5
+
6
+ import {API_VERSION} from '../../constants'
7
+
8
+ type CompleteButtonProps = {
9
+ documentId: string
10
+ disabled: boolean
11
+ }
12
+
13
+ export default function CompleteButton(props: CompleteButtonProps) {
14
+ const {documentId, disabled = false} = props
15
+ const client = useClient({apiVersion: API_VERSION})
16
+ const toast = useToast()
17
+
18
+ const handleComplete = React.useCallback(
19
+ (id: string) => {
20
+ client
21
+ .delete(`workflow-metadata.${id}`)
22
+ .then(() => {
23
+ toast.push({
24
+ status: 'success',
25
+ title: 'Workflow completed',
26
+ description: id,
27
+ })
28
+ })
29
+ .catch(() => {
30
+ toast.push({
31
+ status: 'error',
32
+ title: 'Could not complete Workflow',
33
+ description: id,
34
+ })
35
+ })
36
+ },
37
+ [client, toast]
38
+ )
39
+
40
+ return (
41
+ <Button
42
+ onClick={() => handleComplete(documentId)}
43
+ text="Complete"
44
+ icon={CheckmarkIcon}
45
+ tone="positive"
46
+ mode="ghost"
47
+ fontSize={1}
48
+ padding={2}
49
+ tabIndex={-1}
50
+ disabled={disabled}
51
+ />
52
+ )
53
+ }
@@ -1,4 +1,3 @@
1
- import React from 'react'
2
1
  import {Button} from '@sanity/ui'
3
2
  import {EditIcon} from '@sanity/icons'
4
3
  import {useRouter} from 'sanity/router'
@@ -6,10 +5,11 @@ import {useRouter} from 'sanity/router'
6
5
  type EditButtonProps = {
7
6
  id: string
8
7
  type: string
8
+ disabled?: boolean
9
9
  }
10
10
 
11
11
  export default function EditButton(props: EditButtonProps) {
12
- const {id, type} = props
12
+ const {id, type, disabled = false} = props
13
13
  const {navigateIntent} = useRouter()
14
14
 
15
15
  return (
@@ -22,6 +22,7 @@ export default function EditButton(props: EditButtonProps) {
22
22
  tabIndex={-1}
23
23
  icon={EditIcon}
24
24
  text="Edit"
25
+ disabled={disabled}
25
26
  />
26
27
  )
27
28
  }
@@ -0,0 +1,38 @@
1
+ import {Flex, Card, Spinner} from '@sanity/ui'
2
+ import {Preview, SanityDocument, StringInputProps, useSchema} from 'sanity'
3
+ import {useListeningQuery, Feedback} from 'sanity-plugin-utils'
4
+
5
+ import EditButton from './EditButton'
6
+
7
+ // TODO: Update this to use the same component as the Tool
8
+ export default function Field(props: StringInputProps) {
9
+ const schema = useSchema()
10
+ const {data, loading, error} = useListeningQuery<SanityDocument>(
11
+ `*[_id in [$id, $draftId]]|order(_updatedAt)[0]`,
12
+ {
13
+ params: {
14
+ id: String(props.value),
15
+ draftId: `drafts.${String(props.value)}`,
16
+ },
17
+ }
18
+ )
19
+
20
+ if (loading) {
21
+ return <Spinner />
22
+ }
23
+
24
+ const schemaType = schema.get(data?._type ?? ``)
25
+
26
+ if (error || !data?._type || !schemaType) {
27
+ return <Feedback tone="critical" title="Error with query" />
28
+ }
29
+
30
+ return (
31
+ <Card border padding={2}>
32
+ <Flex align="center" justify="space-between" gap={2}>
33
+ <Preview layout="default" value={data} schemaType={schemaType} />
34
+ <EditButton id={data._id} type={data._type} />
35
+ </Flex>
36
+ </Card>
37
+ )
38
+ }
@@ -0,0 +1,37 @@
1
+ import {ErrorOutlineIcon, WarningOutlineIcon} from '@sanity/icons'
2
+ import {ValidationMarker} from '@sanity/types'
3
+ import {Box, Text, Tooltip} from '@sanity/ui'
4
+ import {TextWithTone} from 'sanity'
5
+
6
+ type ValidationStatusProps = {
7
+ validation: ValidationMarker[]
8
+ }
9
+
10
+ export function ValidationStatus(props: ValidationStatusProps) {
11
+ const {validation = []} = props
12
+
13
+ if (!validation.length) {
14
+ return null
15
+ }
16
+
17
+ const hasError = validation.some((item) => item.level === 'error')
18
+
19
+ return (
20
+ <Tooltip
21
+ portal
22
+ content={
23
+ <Box padding={2}>
24
+ <Text size={1}>
25
+ {validation.length === 1
26
+ ? `1 validation issue`
27
+ : `${validation.length} validation issues`}
28
+ </Text>
29
+ </Box>
30
+ }
31
+ >
32
+ <TextWithTone tone={hasError ? `critical` : `caution`} size={1}>
33
+ {hasError ? <ErrorOutlineIcon /> : <WarningOutlineIcon />}
34
+ </TextWithTone>
35
+ </Tooltip>
36
+ )
37
+ }
@@ -0,0 +1,32 @@
1
+ import {EditIcon} from '@sanity/icons'
2
+ import {PreviewValue, SanityDocument} from '@sanity/types'
3
+ import {Box, Text, Tooltip} from '@sanity/ui'
4
+ import {TextWithTone} from 'sanity'
5
+
6
+ import {TimeAgo} from './TimeAgo'
7
+
8
+ export function DraftStatus(props: {document?: PreviewValue | Partial<SanityDocument> | null}) {
9
+ const {document} = props
10
+ const updatedAt = document && '_updatedAt' in document && document._updatedAt
11
+
12
+ return (
13
+ <Tooltip
14
+ portal
15
+ content={
16
+ <Box padding={2}>
17
+ <Text size={1}>
18
+ {document ? (
19
+ <>Edited {updatedAt && <TimeAgo time={updatedAt} />}</>
20
+ ) : (
21
+ <>No unpublished edits</>
22
+ )}
23
+ </Text>
24
+ </Box>
25
+ }
26
+ >
27
+ <TextWithTone tone="caution" dimmed={!document} muted={!document} size={1}>
28
+ <EditIcon />
29
+ </TextWithTone>
30
+ </Tooltip>
31
+ )
32
+ }
@@ -0,0 +1,39 @@
1
+ import {PublishIcon} from '@sanity/icons'
2
+ import {PreviewValue, SanityDocument} from '@sanity/types'
3
+ import {Box, Text, Tooltip} from '@sanity/ui'
4
+ import {TextWithTone} from 'sanity'
5
+
6
+ import {TimeAgo} from './TimeAgo'
7
+
8
+ export function PublishedStatus(props: {
9
+ document?: PreviewValue | Partial<SanityDocument> | null
10
+ }) {
11
+ const {document} = props
12
+ const updatedAt = document && '_updatedAt' in document && document._updatedAt
13
+
14
+ return (
15
+ <Tooltip
16
+ portal
17
+ content={
18
+ <Box padding={2}>
19
+ <Text size={1}>
20
+ {document ? (
21
+ <>Published {updatedAt && <TimeAgo time={updatedAt} />}</>
22
+ ) : (
23
+ <>Not published</>
24
+ )}
25
+ </Text>
26
+ </Box>
27
+ }
28
+ >
29
+ <TextWithTone
30
+ tone="positive"
31
+ dimmed={!document}
32
+ muted={!document}
33
+ size={1}
34
+ >
35
+ <PublishIcon />
36
+ </TextWithTone>
37
+ </Tooltip>
38
+ )
39
+ }
@@ -0,0 +1,11 @@
1
+ import {useTimeAgo} from 'sanity'
2
+
3
+ export interface TimeAgoProps {
4
+ time: string | Date
5
+ }
6
+
7
+ export function TimeAgo({time}: TimeAgoProps) {
8
+ const timeAgo = useTimeAgo(time)
9
+
10
+ return <span title={timeAgo}>{timeAgo} ago</span>
11
+ }