sanity-plugin-mux-input 2.8.0 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-mux-input",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -104,7 +104,7 @@
104
104
  "peerDependencies": {
105
105
  "react": "^18.3 || ^19",
106
106
  "react-is": "^18.3 || ^19",
107
- "sanity": "^3.42.0",
107
+ "sanity": "^3.42.0 || ^4.0.0-0",
108
108
  "styled-components": "^5 || ^6"
109
109
  },
110
110
  "engines": {
@@ -13,6 +13,7 @@ export const defaultConfig: PluginConfig = {
13
13
  normalize_audio: false,
14
14
  defaultSigned: false,
15
15
  tool: DEFAULT_TOOL_CONFIG,
16
+ allowedRolesForConfiguration: [],
16
17
  }
17
18
 
18
19
  export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig) => {
@@ -12,6 +12,7 @@ import ErrorBoundaryCard from './ErrorBoundaryCard'
12
12
  import {InputFallback} from './Input.styled'
13
13
  import Onboard from './Onboard'
14
14
  import Uploader from './Uploader'
15
+ import {useAccessControl} from '../hooks/useAccessControl'
15
16
 
16
17
  export interface InputProps extends MuxInputProps {
17
18
  config: PluginConfig
@@ -22,6 +23,7 @@ const Input = (props: InputProps) => {
22
23
  const assetDocumentValues = useAssetDocumentValues(props.value?.asset)
23
24
  const poll = useMuxPolling(props.readOnly ? undefined : assetDocumentValues?.value || undefined)
24
25
  const [dialogState, setDialogState] = useDialogState()
26
+ const {hasConfigAccess} = useAccessControl(props.config)
25
27
 
26
28
  const error = secretDocumentValues.error || assetDocumentValues.error || poll.error /*||
27
29
  // @TODO move errored logic to Uploader, where handleRemoveVideo can be called
@@ -44,7 +46,7 @@ const Input = (props: InputProps) => {
44
46
  ) : (
45
47
  <>
46
48
  {secretDocumentValues.value.needsSetup && !assetDocumentValues.value ? (
47
- <Onboard setDialogState={setDialogState} />
49
+ <Onboard setDialogState={setDialogState} config={props.config} />
48
50
  ) : (
49
51
  <Uploader
50
52
  {...props}
@@ -59,7 +61,7 @@ const Input = (props: InputProps) => {
59
61
  />
60
62
  )}
61
63
 
62
- {dialogState === 'secrets' && (
64
+ {dialogState === 'secrets' && hasConfigAccess && (
63
65
  <ConfigureApi
64
66
  setDialogState={setDialogState}
65
67
  secrets={secretDocumentValues.value.secrets}
@@ -1,17 +1,21 @@
1
1
  import {PlugIcon} from '@sanity/icons'
2
- import {Button, Card, Flex, Grid, Heading, Inline} from '@sanity/ui'
2
+ import {Button, Card, Flex, Grid, Heading, Inline, Text} from '@sanity/ui'
3
3
  import {useCallback} from 'react'
4
4
 
5
5
  import type {SetDialogState} from '../hooks/useDialogState'
6
6
  import MuxLogo from './MuxLogo'
7
+ import {PluginConfig} from '../util/types'
8
+ import {useAccessControl} from '../hooks/useAccessControl'
7
9
 
8
10
  interface OnboardProps {
9
11
  setDialogState: SetDialogState
12
+ config: PluginConfig
10
13
  }
11
14
 
12
15
  export default function Onboard(props: OnboardProps) {
13
16
  const {setDialogState} = props
14
17
  const handleOpen = useCallback(() => setDialogState('secrets'), [setDialogState])
18
+ const {hasConfigAccess} = useAccessControl(props.config)
15
19
 
16
20
  return (
17
21
  <>
@@ -41,7 +45,16 @@ export default function Onboard(props: OnboardProps) {
41
45
  </Heading>
42
46
  </Inline>
43
47
  <Inline paddingY={1}>
44
- <Button mode="ghost" icon={PlugIcon} text="Configure API" onClick={handleOpen} />
48
+ {hasConfigAccess ? (
49
+ <Button mode="ghost" icon={PlugIcon} text="Configure API" onClick={handleOpen} />
50
+ ) : (
51
+ <Card padding={[3, 3, 3]} radius={2} shadow={1} tone="critical">
52
+ <Text>
53
+ You do not have access to configure the Mux API. Please contact your
54
+ administrator.
55
+ </Text>
56
+ </Card>
57
+ )}
45
58
  </Inline>
46
59
  </Grid>
47
60
  </Flex>
@@ -27,8 +27,9 @@ import {styled} from 'styled-components'
27
27
 
28
28
  import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
29
29
  import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
30
- import type {MuxInputProps, VideoAssetDocument} from '../util/types'
30
+ import type {MuxInputProps, PluginConfig, VideoAssetDocument} from '../util/types'
31
31
  import {FileInputMenuItem} from './FileInputMenuItem'
32
+ import {useAccessControl} from '../hooks/useAccessControl'
32
33
 
33
34
  const LockCard = styled(Card)`
34
35
  position: absolute;
@@ -55,12 +56,14 @@ function PlayerActionsMenu(
55
56
  onSelect: (files: File[]) => void
56
57
  dialogState: DialogState
57
58
  setDialogState: SetDialogState
59
+ config: PluginConfig
58
60
  }
59
61
  ) {
60
62
  const {asset, readOnly, dialogState, setDialogState, onChange, onSelect} = props
61
63
  const [open, setOpen] = useState(false)
62
64
  const [menuElement, setMenuRef] = useState<HTMLDivElement | null>(null)
63
65
  const isSigned = useMemo(() => getPlaybackPolicy(asset) === 'signed', [asset])
66
+ const {hasConfigAccess} = useAccessControl(props.config)
64
67
 
65
68
  const onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange])
66
69
 
@@ -125,12 +128,16 @@ function PlayerActionsMenu(
125
128
  />
126
129
  )}
127
130
  <MenuDivider />
128
- <MenuItem
129
- icon={PlugIcon}
130
- text="Configure API"
131
- onClick={() => setDialogState('secrets')}
132
- />
133
- <MenuDivider />
131
+ {hasConfigAccess && (
132
+ <>
133
+ <MenuItem
134
+ icon={PlugIcon}
135
+ text="Configure API"
136
+ onClick={() => setDialogState('secrets')}
137
+ />
138
+ <MenuDivider />
139
+ </>
140
+ )}
134
141
  <MenuItem
135
142
  tone="critical"
136
143
  icon={ResetIcon}
@@ -5,6 +5,8 @@ import {useCallback} from 'react'
5
5
 
6
6
  import type {SetDialogState} from '../hooks/useDialogState'
7
7
  import {FileInputButton, type FileInputButtonProps} from './FileInputButton'
8
+ import {useAccessControl} from '../hooks/useAccessControl'
9
+ import {PluginConfig} from '../util/types'
8
10
 
9
11
  interface UploadPlaceholderProps {
10
12
  setDialogState: SetDialogState
@@ -12,11 +14,13 @@ interface UploadPlaceholderProps {
12
14
  hovering: boolean
13
15
  needsSetup: boolean
14
16
  onSelect: FileInputButtonProps['onSelect']
17
+ config: PluginConfig
15
18
  }
16
19
  export default function UploadPlaceholder(props: UploadPlaceholderProps) {
17
20
  const {setDialogState, readOnly, onSelect, hovering, needsSetup} = props
18
21
  const handleBrowse = useCallback(() => setDialogState('select-video'), [setDialogState])
19
22
  const handleConfigureApi = useCallback(() => setDialogState('secrets'), [setDialogState])
23
+ const {hasConfigAccess} = useAccessControl(props.config)
20
24
 
21
25
  return (
22
26
  <Card
@@ -58,15 +62,17 @@ export default function UploadPlaceholder(props: UploadPlaceholderProps) {
58
62
  />
59
63
  <Button mode="bleed" icon={SearchIcon} text="Select" onClick={handleBrowse} />
60
64
 
61
- <Button
62
- padding={3}
63
- radius={3}
64
- tone={needsSetup ? 'critical' : undefined}
65
- onClick={handleConfigureApi}
66
- icon={PlugIcon}
67
- mode="bleed"
68
- title="Configure plugin credentials"
69
- />
65
+ {hasConfigAccess && (
66
+ <Button
67
+ padding={3}
68
+ radius={3}
69
+ tone={needsSetup ? 'critical' : undefined}
70
+ onClick={handleConfigureApi}
71
+ icon={PlugIcon}
72
+ mode="bleed"
73
+ title="Configure plugin credentials"
74
+ />
75
+ )}
70
76
  </Inline>
71
77
  </Flex>
72
78
  </Card>
@@ -364,6 +364,7 @@ export default function Uploader(props: Props) {
364
364
  onChange={props.onChange}
365
365
  onSelect={handleUpload}
366
366
  readOnly={props.readOnly}
367
+ config={props.config}
367
368
  />
368
369
  }
369
370
  />
@@ -375,6 +376,7 @@ export default function Uploader(props: Props) {
375
376
  readOnly={!!props.readOnly}
376
377
  setDialogState={props.setDialogState}
377
378
  needsSetup={props.needsSetup}
379
+ config={props.config}
378
380
  />
379
381
  )}
380
382
  </UploadCard>
@@ -6,7 +6,7 @@ import type {SanityDocument} from 'sanity'
6
6
  import {deleteAsset} from '../../actions/assets'
7
7
  import {useClient} from '../../hooks/useClient'
8
8
  import {DIALOGS_Z_INDEX} from '../../util/constants'
9
- import type {PluginPlacement, VideoAssetDocument} from '../../util/types'
9
+ import type {VideoAssetDocument} from '../../util/types'
10
10
  import SpinnerBox from '../SpinnerBox'
11
11
  import VideoReferences from './VideoReferences'
12
12
 
@@ -15,11 +15,9 @@ export default function DeleteDialog({
15
15
  references,
16
16
  referencesLoading,
17
17
  cancelDelete,
18
- placement,
19
18
  succeededDeleting,
20
19
  }: {
21
20
  asset: VideoAssetDocument
22
- placement: PluginPlacement
23
21
  references?: SanityDocument[]
24
22
  referencesLoading: boolean
25
23
  cancelDelete: () => void
@@ -95,11 +93,7 @@ export default function DeleteDialog({
95
93
  pointing to this video. Remove their references to this file or delete them before
96
94
  proceeding.
97
95
  </Text>
98
- <VideoReferences
99
- references={references}
100
- isLoaded={!referencesLoading}
101
- placement={placement}
102
- />
96
+ <VideoReferences references={references} isLoaded={!referencesLoading} />
103
97
  </>
104
98
  )}
105
99
  {state === 'confirm' && (
@@ -127,7 +127,6 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
127
127
  <DeleteDialog
128
128
  asset={props.asset}
129
129
  cancelDelete={() => setState('idle')}
130
- placement={props.placement}
131
130
  referencesLoading={referencesLoading}
132
131
  references={references}
133
132
  succeededDeleting={() => {
@@ -295,11 +294,7 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
295
294
  id="references-panel"
296
295
  hidden={tab !== 'references'}
297
296
  >
298
- <VideoReferences
299
- references={references}
300
- isLoaded={!referencesLoading}
301
- placement={props.placement}
302
- />
297
+ <VideoReferences references={references} isLoaded={!referencesLoading} />
303
298
  </TabPanel>
304
299
  </Stack>
305
300
  </Flex>
@@ -3,7 +3,6 @@ import {Box, Card, Text} from '@sanity/ui'
3
3
  import {collate, useSchema} from 'sanity'
4
4
  import {styled} from 'styled-components'
5
5
 
6
- import type {PluginPlacement} from '../../util/types'
7
6
  import {DocumentPreview} from '../documentPreview/DocumentPreview'
8
7
  import SpinnerBox from '../SpinnerBox'
9
8
 
@@ -22,7 +21,6 @@ const Container = styled(Box)`
22
21
  const VideoReferences: React.FC<{
23
22
  references?: SanityDocument[]
24
23
  isLoaded: boolean
25
- placement: PluginPlacement
26
24
  }> = (props) => {
27
25
  const schema = useSchema()
28
26
  if (!props.isLoaded) {
@@ -53,11 +51,7 @@ const VideoReferences: React.FC<{
53
51
  style={{overflow: 'hidden'}}
54
52
  >
55
53
  <Box>
56
- <DocumentPreview
57
- documentPair={documentPair}
58
- schemaType={schemaType}
59
- placement={props.placement}
60
- />
54
+ <DocumentPreview documentPair={documentPair} schemaType={schemaType} />
61
55
  </Box>
62
56
  </Card>
63
57
  )
@@ -5,10 +5,9 @@ import {useDocumentStore} from 'sanity'
5
5
  import {useClient} from '../../hooks/useClient'
6
6
  import useDocReferences from '../../hooks/useDocReferences'
7
7
  import getVideoMetadata from '../../util/getVideoMetadata'
8
- import {PluginPlacement, VideoAssetDocument} from '../../util/types'
8
+ import {VideoAssetDocument} from '../../util/types'
9
9
 
10
10
  export interface VideoDetailsProps {
11
- placement: PluginPlacement
12
11
  closeDialog: () => void
13
12
  asset: VideoAssetDocument & {autoPlay?: boolean}
14
13
  }
@@ -75,11 +75,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
75
75
  )}
76
76
  </Stack>
77
77
  {freshEditedAsset && (
78
- <VideoDetails
79
- closeDialog={() => setEditedAsset(null)}
80
- asset={freshEditedAsset}
81
- placement={placement}
82
- />
78
+ <VideoDetails closeDialog={() => setEditedAsset(null)} asset={freshEditedAsset} />
83
79
  )}
84
80
  </>
85
81
  )
@@ -3,20 +3,16 @@
3
3
  import {DocumentIcon} from '@sanity/icons'
4
4
  import type {PropsWithChildren} from 'react'
5
5
  import React, {useMemo} from 'react'
6
- import type {SanityDocument} from 'sanity'
7
- import type {CollatedHit, FIXME, SchemaType} from 'sanity'
6
+ import type {CollatedHit, FIXME, SanityDocument, SchemaType} from 'sanity'
8
7
  import {PreviewCard, useDocumentPresence, useDocumentPreviewStore, useSchema} from 'sanity'
9
- import {usePaneRouter} from 'sanity/desk'
10
8
  import {IntentLink} from 'sanity/router'
11
9
 
12
- import {PluginPlacement} from '../../util/types'
13
10
  import {MissingSchemaType} from './MissingSchemaType'
14
11
  import {PaneItemPreview} from './PaneItemPreview'
15
12
 
16
13
  interface DocumentPreviewProps {
17
14
  schemaType?: SchemaType
18
15
  documentPair: CollatedHit<SanityDocument>
19
- placement?: PluginPlacement
20
16
  }
21
17
 
22
18
  /**
@@ -35,23 +31,7 @@ export function getIconWithFallback(
35
31
  return icon || ((schemaType && schemaType.icon) as any) || defaultIcon || false
36
32
  }
37
33
 
38
- /** When inside the field input, we can open the reference on a child pane */
39
- function DocumentPreviewInInput(props: PropsWithChildren<DocumentPreviewProps>) {
40
- const {ChildLink} = usePaneRouter()
41
-
42
- return (linkProps: PropsWithChildren) => (
43
- <ChildLink
44
- childId={props.documentPair.id}
45
- // Pass the schemaType of the document so `paneChild` in `buildPagesStructure` can access it
46
- childParameters={{type: props.documentPair.type}}
47
- >
48
- {linkProps.children}
49
- </ChildLink>
50
- )
51
- }
52
-
53
- /** When inside the tool, we must use a regular intent link to take users to the desk tool */
54
- function DocumentPreviewInRool(props: DocumentPreviewProps) {
34
+ function DocumentPreviewLink(props: DocumentPreviewProps) {
55
35
  return (linkProps: PropsWithChildren) => (
56
36
  <IntentLink intent="edit" params={{id: props.documentPair.id}}>
57
37
  {linkProps.children}
@@ -90,11 +70,7 @@ export function DocumentPreview(props: DocumentPreviewProps) {
90
70
  return (
91
71
  <PreviewCard
92
72
  __unstable_focusRing
93
- as={
94
- (props.placement === 'input'
95
- ? DocumentPreviewInInput(props)
96
- : DocumentPreviewInRool(props)) as FIXME
97
- }
73
+ as={DocumentPreviewLink(props) as FIXME}
98
74
  data-as="a"
99
75
  data-ui="PaneItem"
100
76
  padding={2}
@@ -0,0 +1,12 @@
1
+ import {useCurrentUser} from 'sanity'
2
+ import {PluginConfig} from '../util/types'
3
+
4
+ export const useAccessControl = (config: PluginConfig) => {
5
+ const user = useCurrentUser()
6
+
7
+ const hasConfigAccess =
8
+ !config?.allowedRolesForConfiguration?.length ||
9
+ user?.roles?.some((role) => config.allowedRolesForConfiguration.includes(role.name))
10
+
11
+ return {hasConfigAccess}
12
+ }
@@ -101,7 +101,7 @@ function muxAssetToSanityDocument(asset: MuxAsset): VideoAssetDocument | undefin
101
101
  _createdAt: parseMuxDate(asset.created_at).toISOString(),
102
102
  assetId: asset.id,
103
103
  playbackId,
104
- filename: `Asset #${truncateString(asset.id, 15)}`,
104
+ filename: asset.meta?.title ?? `Asset #${truncateString(asset.id, 15)}`,
105
105
  status: asset.status,
106
106
  data: asset,
107
107
  }
package/src/util/types.ts CHANGED
@@ -88,6 +88,14 @@ export interface PluginConfig extends MuxInputConfig {
88
88
  title?: string
89
89
  icon?: React.ComponentType
90
90
  }
91
+
92
+ /**
93
+ * The roles that are allowed to configure the plugin.
94
+ *
95
+ * If not set, all roles will be allowed to configure the plugin.
96
+ * @defaultValue []
97
+ */
98
+ allowedRolesForConfiguration: string[]
91
99
  }
92
100
 
93
101
  export const SUPPORTED_MUX_LANGUAGES = [
@@ -374,6 +382,9 @@ export interface MuxAsset {
374
382
  unexpected_media_file_parameters?: 'non-standard'
375
383
  test?: boolean
376
384
  }
385
+ meta?: {
386
+ title?: string
387
+ }
377
388
  }
378
389
 
379
390
  export interface VideoAssetDocument {