sanity-plugin-utils 0.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.
@@ -0,0 +1,133 @@
1
+ import React, {useEffect, useRef} from 'react'
2
+ import {Box, Text, Menu, MenuItem, TextInput, Flex, Badge} from '@sanity/ui'
3
+ import {AddCircleIcon, RemoveCircleIcon, RestoreIcon} from '@sanity/icons'
4
+ import {UserAvatar} from 'sanity/_unstable'
5
+
6
+ import {UserExtended} from '../../hooks/useProjectUsers'
7
+
8
+ function searchUsers(users: UserExtended[], searchString: string): UserExtended[] {
9
+ return users.filter((user) => {
10
+ const displayName = (user.displayName || '').toLowerCase()
11
+ if (displayName.startsWith(searchString)) return true
12
+ const givenName = (user.givenName || '').toLowerCase()
13
+ if (givenName.startsWith(searchString)) return true
14
+ const middleName = (user.middleName || '').toLowerCase()
15
+ if (middleName.startsWith(searchString)) return true
16
+ const familyName = (user.familyName || '').toLowerCase()
17
+ if (familyName.startsWith(searchString)) return true
18
+
19
+ return false
20
+ })
21
+ }
22
+
23
+ type UserSelectMenuProps = {
24
+ value: string[]
25
+ userList: UserExtended[]
26
+ onAdd: any
27
+ onRemove: any
28
+ onClear: any
29
+ open: boolean
30
+ style?: React.CSSProperties
31
+ }
32
+
33
+ export function UserSelectMenu(props: UserSelectMenuProps) {
34
+ const {value = [], userList = [], onAdd, onRemove, onClear, style = {}} = props
35
+ const [searchString, setSearchString] = React.useState('')
36
+ const searchResults = searchUsers(userList || [], searchString)
37
+
38
+ const me = userList.find((u) => u.isCurrentUser)
39
+ const meAssigned = me && value.includes(me.id)
40
+
41
+ // Focus input on open
42
+ // TODO: Fix focus, it gets immediately taken away
43
+ const input = useRef<HTMLInputElement>()
44
+ // useEffect(() => {
45
+ // if (open && input?.current) {
46
+ // input.current.focus()
47
+ // }
48
+ // }, [open])
49
+
50
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
51
+ setSearchString(event.target.value)
52
+ }
53
+
54
+ const handleSelect = (isChecked: boolean, user: UserExtended) => {
55
+ if (!isChecked) {
56
+ if (onAdd) onAdd(user.id)
57
+ } else if (onRemove) onRemove(user.id)
58
+ }
59
+
60
+ const handleAssignMyself = () => {
61
+ if (me && onAdd) onAdd(me.id)
62
+ }
63
+
64
+ const handleUnassignMyself = () => {
65
+ if (me && onRemove) onRemove(me.id)
66
+ }
67
+
68
+ const handleClearAssigneesClick = () => {
69
+ if (onClear) onClear()
70
+ }
71
+
72
+ return (
73
+ <Menu style={style}>
74
+ {meAssigned ? (
75
+ <MenuItem
76
+ tone="caution"
77
+ disabled={!me}
78
+ onClick={handleUnassignMyself}
79
+ icon={RemoveCircleIcon}
80
+ text="Unassign myself"
81
+ />
82
+ ) : (
83
+ <MenuItem
84
+ tone="positive"
85
+ onClick={handleAssignMyself}
86
+ icon={AddCircleIcon}
87
+ text="Assign myself"
88
+ />
89
+ )}
90
+
91
+ <MenuItem
92
+ tone="critical"
93
+ disabled={value.length === 0}
94
+ onClick={handleClearAssigneesClick}
95
+ icon={RestoreIcon}
96
+ text="Clear assignees"
97
+ />
98
+
99
+ <Box padding={1}>
100
+ <TextInput
101
+ // @ts-ignore TODO: Satisfy ref
102
+ ref={input}
103
+ onChange={handleSearchChange}
104
+ placeholder="Search members"
105
+ value={searchString}
106
+ />
107
+ </Box>
108
+
109
+ {searchString && searchResults?.length === 0 && <MenuItem disabled text="No matches" />}
110
+
111
+ {searchResults &&
112
+ searchResults.map((user) => (
113
+ <MenuItem
114
+ key={user.id}
115
+ pressed={value.includes(user.id)}
116
+ onClick={() => handleSelect(value.indexOf(user.id) > -1, user)}
117
+ >
118
+ <Flex align="center">
119
+ <UserAvatar user={user} size={1} />
120
+ <Box paddingX={2} flex={1}>
121
+ <Text>{user.displayName}</Text>
122
+ </Box>
123
+ {user.isCurrentUser && (
124
+ <Badge fontSize={1} tone="positive" mode="outline">
125
+ Me
126
+ </Badge>
127
+ )}
128
+ </Flex>
129
+ </MenuItem>
130
+ ))}
131
+ </Menu>
132
+ )
133
+ }
@@ -0,0 +1,73 @@
1
+ import {useEffect, useState, useRef} from 'react'
2
+ import {catchError, distinctUntilChanged} from 'rxjs/operators'
3
+ import isEqual from 'react-fast-compare'
4
+ import {useDocumentStore} from 'sanity/_unstable'
5
+ import {Subscription} from 'rxjs'
6
+
7
+ type Params = Record<string, string | number | boolean | string[]>
8
+
9
+ interface ListenQueryOptions {
10
+ tag?: string
11
+ apiVersion?: string
12
+ }
13
+
14
+ type Value = any
15
+
16
+ interface Config<V> {
17
+ params: Params
18
+ options?: ListenQueryOptions
19
+ initialValue?: null | V
20
+ }
21
+
22
+ interface Return<V> {
23
+ loading: boolean
24
+ error: boolean
25
+ data: null | V
26
+ initialValue?: Value
27
+ }
28
+
29
+ const DEFAULT_PARAMS = {}
30
+ const DEFAULT_OPTIONS = {apiVersion: `v2022-05-09`}
31
+ const DEFAULT_INITIAL_VALUE = null
32
+
33
+ export function useListeningQuery<V>(
34
+ query: string | {fetch: string; listen: string},
35
+ {
36
+ params = DEFAULT_PARAMS,
37
+ options = DEFAULT_OPTIONS,
38
+ initialValue = DEFAULT_INITIAL_VALUE,
39
+ }: Config<V>
40
+ ): Return<V> {
41
+ const [loading, setLoading] = useState(true)
42
+ const [error, setError] = useState(false)
43
+ const [data, setData] = useState(initialValue)
44
+ const subscription = useRef<null | Subscription>(null)
45
+ const documentStore = useDocumentStore()
46
+
47
+ useEffect(() => {
48
+ if (query) {
49
+ subscription.current = documentStore
50
+ .listenQuery(query, params, options)
51
+ .pipe(
52
+ distinctUntilChanged(isEqual),
53
+ catchError((err) => {
54
+ console.error(err)
55
+ setError(err)
56
+ setLoading(false)
57
+ setData(null)
58
+
59
+ return err
60
+ })
61
+ )
62
+ .subscribe((documents) => {
63
+ setData((current: Value) => (isEqual(current, documents) ? current : documents))
64
+ setLoading(false)
65
+ setError(false)
66
+ })
67
+ }
68
+
69
+ return () => subscription?.current?.unsubscribe()
70
+ }, [query, params, options, documentStore])
71
+
72
+ return {data, loading, error}
73
+ }
@@ -0,0 +1,73 @@
1
+ import {useState, useEffect} from 'react'
2
+ import {useClient, useWorkspace} from 'sanity'
3
+
4
+ export type UserExtended = {
5
+ createdAt: string
6
+ displayName: string
7
+ email: string
8
+ familyName: string
9
+ givenName: string
10
+ id: string
11
+ imageUrl: string
12
+ isCurrentUser: boolean
13
+ middleName: string
14
+ projectId: string
15
+ provider: string
16
+ sanityUserId: string
17
+ updatedAt: string
18
+ }
19
+
20
+ type UserRole = {
21
+ name: string
22
+ title: string
23
+ }
24
+
25
+ type UserResponse = {
26
+ isRobot: boolean
27
+ projectUserId: string
28
+ roles: UserRole[]
29
+ }
30
+
31
+ // Custom hook to fetch user details
32
+ // Built-in hook doesn't fetch all user details
33
+ export function useProjectUsers(): UserExtended[] {
34
+ const {currentUser} = useWorkspace()
35
+ const client = useClient()
36
+ const [users, setUsers] = useState([])
37
+
38
+ useEffect(() => {
39
+ const {projectId} = client.config()
40
+
41
+ async function getUser(id: string) {
42
+ const userDetails = await client.request({
43
+ url: `/projects/${projectId}/users/${id}`,
44
+ })
45
+
46
+ return userDetails
47
+ }
48
+
49
+ async function getUsersWithRoles() {
50
+ const userRoles = await client
51
+ .request({
52
+ url: `/projects/${projectId}/acl`,
53
+ })
54
+ .then(async (res) =>
55
+ Promise.all(
56
+ res.map(async (user: UserResponse) => ({
57
+ isCurrentUser: user.projectUserId === currentUser?.id,
58
+ ...(await getUser(user.projectUserId)),
59
+ }))
60
+ )
61
+ )
62
+ .catch((err) => err)
63
+
64
+ setUsers(userRoles)
65
+ }
66
+
67
+ if (!users.length) {
68
+ getUsersWithRoles()
69
+ }
70
+ }, [client, currentUser?.id, users.length])
71
+
72
+ return users
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export {useListeningQuery} from './hooks/useListeningQuery'
2
+ export {useProjectUsers} from './hooks/useProjectUsers'
3
+
4
+ export {Feedback} from './components/Feedback'
5
+ export {Table, Row, Cell} from './components/Table'
6
+ export {UserSelectMenu} from './components/UserSelectMenu/index'
@@ -0,0 +1,11 @@
1
+ const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
+ const {name, version, sanityExchangeUrl} = require('./package.json')
3
+
4
+ export default showIncompatiblePluginDialog({
5
+ name: name,
6
+ versions: {
7
+ v3: version,
8
+ v2: undefined,
9
+ },
10
+ sanityExchangeUrl,
11
+ })