sanity-plugin-mux-input 2.9.1 → 2.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-mux-input",
3
- "version": "2.9.1",
3
+ "version": "2.10.1",
4
4
  "description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -8,7 +8,7 @@ export type {VideoAssetDocument} from '../util/types'
8
8
 
9
9
  export const defaultConfig: PluginConfig = {
10
10
  mp4_support: 'none',
11
- encoding_tier: 'smart',
11
+ video_quality: 'plus',
12
12
  max_resolution_tier: '1080p',
13
13
  normalize_audio: false,
14
14
  defaultSigned: false,
@@ -17,6 +17,18 @@ export const defaultConfig: PluginConfig = {
17
17
  }
18
18
 
19
19
  export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig) => {
20
+ // TODO: Remove this on next major version when we end support for encoding_tier
21
+ if (typeof userConfig === 'object' && 'encoding_tier' in userConfig) {
22
+ const deprecated_encoding_tier = userConfig.encoding_tier
23
+ if (!userConfig.video_quality) {
24
+ if (deprecated_encoding_tier === 'baseline') {
25
+ userConfig.video_quality = 'basic'
26
+ }
27
+ if (deprecated_encoding_tier === 'smart') {
28
+ userConfig.video_quality = 'plus'
29
+ }
30
+ }
31
+ }
20
32
  const config: PluginConfig = {...defaultConfig, ...(userConfig || {})}
21
33
  return {
22
34
  name: 'mux-input',
@@ -47,3 +47,25 @@ export function getAsset(client: SanityClient, assetId: string) {
47
47
  method: 'GET',
48
48
  })
49
49
  }
50
+
51
+ export function listAssets(
52
+ client: SanityClient,
53
+ options: {limit?: number; cursor?: string | null}
54
+ ) {
55
+ const {dataset} = client.config()
56
+ const query: {limit?: string; cursor?: string} = {}
57
+
58
+ if (options.limit) {
59
+ query.limit = options.limit.toString()
60
+ }
61
+ if (options.cursor) {
62
+ query.cursor = options.cursor
63
+ }
64
+
65
+ return client.request<{data: MuxAsset[]; next_cursor?: string | null}>({
66
+ url: `/addons/mux/assets/${dataset}/data/list`,
67
+ withCredentials: true,
68
+ method: 'GET',
69
+ query,
70
+ })
71
+ }
@@ -11,25 +11,31 @@ import {
11
11
  Text,
12
12
  TextInput,
13
13
  } from '@sanity/ui'
14
- import React, {memo, useCallback, useEffect, useId, useMemo, useRef} from 'react'
14
+ import {useCallback, useEffect, useId, useMemo, useRef} from 'react'
15
15
  import {clear, preload} from 'suspend-react'
16
16
 
17
17
  import {useClient} from '../hooks/useClient'
18
18
  import type {SetDialogState} from '../hooks/useDialogState'
19
+ import {useDialogState} from '../hooks/useDialogState'
19
20
  import {useSaveSecrets} from '../hooks/useSaveSecrets'
21
+ import {useSecretsDocumentValues} from '../hooks/useSecretsDocumentValues'
20
22
  import {useSecretsFormState} from '../hooks/useSecretsFormState'
21
- import {cacheNs} from '../util/constants'
23
+ import {cacheNs, DIALOGS_Z_INDEX} from '../util/constants'
22
24
  import {_id as secretsId} from '../util/readSecrets'
23
25
  import type {Secrets} from '../util/types'
24
26
  import {Header} from './ConfigureApi.styled'
25
27
  import FormField from './FormField'
26
28
 
27
- export interface Props {
29
+ // Props for the dialog component when used with external state management
30
+ export interface ConfigureApiDialogProps {
28
31
  setDialogState: SetDialogState
29
32
  secrets: Secrets
30
33
  }
34
+
31
35
  const fieldNames = ['token', 'secretKey', 'enableSignedUrls'] as const
32
- function ConfigureApi({secrets, setDialogState}: Props) {
36
+
37
+ // Internal dialog component that can be used with external state
38
+ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialogProps) {
33
39
  const client = useClient()
34
40
  const [state, dispatch] = useSecretsFormState(secrets)
35
41
  const hasSecretsInitially = useMemo(() => secrets.token && secrets.secretKey, [secrets])
@@ -112,13 +118,13 @@ function ConfigureApi({secrets, setDialogState}: Props) {
112
118
  animate
113
119
  id={id}
114
120
  onClose={handleClose}
121
+ onClickOutside={handleClose}
115
122
  header={<Header />}
123
+ zOffset={DIALOGS_Z_INDEX}
124
+ position="fixed"
116
125
  width={1}
117
- style={{
118
- maxWidth: '550px',
119
- }}
120
126
  >
121
- <Box padding={4} style={{position: 'relative'}}>
127
+ <Box padding={3}>
122
128
  <form onSubmit={handleSubmit} noValidate>
123
129
  <Stack space={4}>
124
130
  {!hasSecretsInitially && (
@@ -224,4 +230,21 @@ function ConfigureApi({secrets, setDialogState}: Props) {
224
230
  )
225
231
  }
226
232
 
227
- export default memo(ConfigureApi)
233
+ // Wrapper component that manages its own dialog state (used in VideosBrowser)
234
+ export default function ConfigureApi() {
235
+ const [dialogOpen, setDialogOpen] = useDialogState()
236
+ const secretDocumentValues = useSecretsDocumentValues()
237
+
238
+ const openDialog = useCallback(() => setDialogOpen('secrets'), [setDialogOpen])
239
+
240
+ if (dialogOpen === 'secrets') {
241
+ return (
242
+ <ConfigureApiDialog
243
+ secrets={secretDocumentValues.value.secrets}
244
+ setDialogState={setDialogOpen}
245
+ />
246
+ )
247
+ }
248
+
249
+ return <Button mode="bleed" text="Configure plugin" onClick={openDialog} />
250
+ }
@@ -1,4 +1,10 @@
1
- import {CheckmarkCircleIcon, ErrorOutlineIcon, RetrieveIcon, RetryIcon} from '@sanity/icons'
1
+ import {
2
+ CheckmarkCircleIcon,
3
+ ErrorOutlineIcon,
4
+ InfoOutlineIcon,
5
+ RetrieveIcon,
6
+ RetryIcon,
7
+ } from '@sanity/icons'
2
8
  import {
3
9
  Box,
4
10
  Button,
@@ -120,7 +126,7 @@ function ImportVideosDialog(props: ReturnType<typeof useImportMuxAssets>) {
120
126
  <Button
121
127
  fontSize={2}
122
128
  padding={3}
123
- mode="bleed"
129
+ mode="ghost"
124
130
  text="Cancel"
125
131
  tone="critical"
126
132
  onClick={props.closeDialog}
@@ -149,6 +155,24 @@ function ImportVideosDialog(props: ReturnType<typeof useImportMuxAssets>) {
149
155
  }
150
156
  >
151
157
  <Box padding={3}>
158
+ {/* WARNING: SKIPPED ASSETS WITHOUT PLAYBACK */}
159
+ {props.muxAssets.hasSkippedAssetsWithoutPlayback && (
160
+ <Card tone="caution" marginBottom={5} padding={3} border>
161
+ <Flex align="center" gap={2}>
162
+ <InfoOutlineIcon fontSize={36} />
163
+ <Stack space={2}>
164
+ <Text size={2} weight="semibold">
165
+ Some videos were skipped
166
+ </Text>
167
+ <Text size={1}>
168
+ Videos without playback IDs cannot be imported and have been excluded from the
169
+ list.
170
+ </Text>
171
+ </Stack>
172
+ </Flex>
173
+ </Card>
174
+ )}
175
+
152
176
  {/* LOADING ASSETS STATE */}
153
177
  {(props.muxAssets.loading || props.assetsInSanityLoading) && (
154
178
  <Card tone="primary" marginBottom={5} padding={3} border>
@@ -1,18 +1,18 @@
1
1
  import {Card} from '@sanity/ui'
2
2
  import {memo, Suspense} from 'react'
3
3
 
4
+ import {useAccessControl} from '../hooks/useAccessControl'
4
5
  import {useAssetDocumentValues} from '../hooks/useAssetDocumentValues'
5
6
  import {useClient} from '../hooks/useClient'
6
7
  import {useDialogState} from '../hooks/useDialogState'
7
8
  import {useMuxPolling} from '../hooks/useMuxPolling'
8
9
  import {useSecretsDocumentValues} from '../hooks/useSecretsDocumentValues'
9
10
  import type {MuxInputProps, PluginConfig} from '../util/types'
10
- import ConfigureApi from './ConfigureApi'
11
+ import {ConfigureApiDialog} from './ConfigureApi'
11
12
  import ErrorBoundaryCard from './ErrorBoundaryCard'
12
13
  import {InputFallback} from './Input.styled'
13
14
  import Onboard from './Onboard'
14
15
  import Uploader from './Uploader'
15
- import {useAccessControl} from '../hooks/useAccessControl'
16
16
 
17
17
  export interface InputProps extends MuxInputProps {
18
18
  config: PluginConfig
@@ -62,7 +62,7 @@ const Input = (props: InputProps) => {
62
62
  )}
63
63
 
64
64
  {dialogState === 'secrets' && hasConfigAccess && (
65
- <ConfigureApi
65
+ <ConfigureApiDialog
66
66
  setDialogState={setDialogState}
67
67
  secrets={secretDocumentValues.value.secrets}
68
68
  />
@@ -0,0 +1,201 @@
1
+ import {CheckmarkCircleIcon, ErrorOutlineIcon, SyncIcon} from '@sanity/icons'
2
+ import {Box, Button, Card, Dialog, Flex, Heading, Spinner, Stack, Text} from '@sanity/ui'
3
+
4
+ import useResyncMuxMetadata from '../hooks/useResyncMuxMetadata'
5
+ import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
6
+ import {DIALOGS_Z_INDEX} from '../util/constants'
7
+
8
+ // eslint-disable-next-line complexity
9
+ function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
10
+ const {resyncState} = props
11
+
12
+ const canTriggerResync = resyncState === 'idle' || resyncState === 'error'
13
+ const isResyncing = resyncState === 'syncing'
14
+ const isDone = resyncState === 'done'
15
+
16
+ const videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0
17
+ const videosWithEmptyOrPlaceholder =
18
+ props.matchedAssets?.filter(
19
+ (m) => m.muxAsset && m.muxTitle && isEmptyOrPlaceholderTitle(m.currentTitle, m.muxAsset.id)
20
+ ).length || 0
21
+
22
+ return (
23
+ <Dialog
24
+ animate
25
+ header={'Resync Metadata from Mux'}
26
+ zOffset={DIALOGS_Z_INDEX}
27
+ id="resync-metadata-dialog"
28
+ onClose={props.closeDialog}
29
+ onClickOutside={props.closeDialog}
30
+ width={1}
31
+ position="fixed"
32
+ footer={
33
+ !isDone && (
34
+ <Card padding={3}>
35
+ <Flex justify="space-between" align="center">
36
+ <Button
37
+ fontSize={2}
38
+ padding={3}
39
+ mode="ghost"
40
+ text="Cancel"
41
+ tone="critical"
42
+ onClick={props.closeDialog}
43
+ disabled={isResyncing}
44
+ />
45
+ <Flex gap={2}>
46
+ {videosWithEmptyOrPlaceholder > 0 && (
47
+ <Button
48
+ fontSize={2}
49
+ padding={3}
50
+ mode="ghost"
51
+ text={`Update empty (${videosWithEmptyOrPlaceholder})`}
52
+ tone="caution"
53
+ onClick={props.syncOnlyEmpty}
54
+ disabled={isResyncing || !canTriggerResync}
55
+ />
56
+ )}
57
+ <Button
58
+ icon={SyncIcon}
59
+ fontSize={2}
60
+ padding={3}
61
+ mode="ghost"
62
+ text={`Update all (${videosToUpdate})`}
63
+ tone="positive"
64
+ onClick={props.syncAllVideos}
65
+ iconRight={isResyncing && Spinner}
66
+ disabled={!canTriggerResync}
67
+ />
68
+ </Flex>
69
+ </Flex>
70
+ </Card>
71
+ )
72
+ }
73
+ >
74
+ <Box padding={4}>
75
+ {/* LOADING ASSETS STATE */}
76
+ {(props.muxAssets.loading || props.sanityAssetsLoading) && (
77
+ <Card tone="primary" marginBottom={5} padding={3} border>
78
+ <Flex align="center" gap={4}>
79
+ <Spinner muted size={4} />
80
+ <Stack space={2}>
81
+ <Text size={2} weight="semibold">
82
+ Loading assets from Mux
83
+ </Text>
84
+ <Text size={1}>This may take a while.</Text>
85
+ </Stack>
86
+ </Flex>
87
+ </Card>
88
+ )}
89
+
90
+ {/* ERROR LOADING MUX */}
91
+ {props.muxAssets.error && (
92
+ <Card tone="critical" marginBottom={5} padding={3} border>
93
+ <Flex align="center" gap={2}>
94
+ <ErrorOutlineIcon fontSize={36} />
95
+ <Stack space={2}>
96
+ <Text size={2} weight="semibold">
97
+ There was an error getting data from Mux
98
+ </Text>
99
+ <Text size={1}>Please try again or contact a developer for help.</Text>
100
+ </Stack>
101
+ </Flex>
102
+ </Card>
103
+ )}
104
+
105
+ {/* SYNCING STATE */}
106
+ {resyncState === 'syncing' && (
107
+ <Card tone="primary" marginBottom={5} padding={3} border>
108
+ <Flex align="center" gap={4}>
109
+ <Spinner muted size={4} />
110
+ <Stack space={2}>
111
+ <Text size={2} weight="semibold">
112
+ Updating video metadata
113
+ </Text>
114
+ <Text size={1}>Syncing titles from Mux...</Text>
115
+ </Stack>
116
+ </Flex>
117
+ </Card>
118
+ )}
119
+
120
+ {/* ERROR SYNCING */}
121
+ {resyncState === 'error' && (
122
+ <Card tone="critical" marginBottom={5} padding={3} border>
123
+ <Flex align="center" gap={2}>
124
+ <ErrorOutlineIcon fontSize={36} />
125
+ <Stack space={2}>
126
+ <Text size={2} weight="semibold">
127
+ There was an error syncing metadata
128
+ </Text>
129
+ <Text size={1}>
130
+ {props.resyncError
131
+ ? `Error: ${props.resyncError}`
132
+ : 'Please try again or contact a developer for help.'}
133
+ </Text>
134
+ </Stack>
135
+ </Flex>
136
+ </Card>
137
+ )}
138
+
139
+ {/* SUCCESS STATE */}
140
+ {resyncState === 'done' && (
141
+ <Stack paddingY={5} marginBottom={4} space={3} style={{textAlign: 'center'}}>
142
+ <Box>
143
+ <CheckmarkCircleIcon fontSize={48} />
144
+ </Box>
145
+ <Heading size={2}>Metadata synced successfully</Heading>
146
+ <Text size={2}>All video titles have been updated from Mux.</Text>
147
+ </Stack>
148
+ )}
149
+
150
+ {/* CONFIRMATION MESSAGE */}
151
+ {resyncState === 'idle' && !props.muxAssets.loading && !props.sanityAssetsLoading && (
152
+ <Stack space={4}>
153
+ <Heading size={1}>
154
+ There {videosToUpdate === 1 ? 'is' : 'are'} {videosToUpdate} video
155
+ {videosToUpdate === 1 ? '' : 's'} with Mux metadata
156
+ </Heading>
157
+ <Text size={2}>
158
+ This will update video titles in Sanity to match those in Mux. No new videos will be
159
+ created.
160
+ </Text>
161
+ {videosWithEmptyOrPlaceholder > 0 && (
162
+ <Card padding={3} tone="caution" border>
163
+ <Flex align="flex-start" gap={2}>
164
+ <Box>
165
+ <ErrorOutlineIcon />
166
+ </Box>
167
+ <Stack space={2}>
168
+ <Text size={2} weight="semibold">
169
+ Videos with empty or placeholder titles
170
+ </Text>
171
+ <Text size={1} muted>
172
+ {videosWithEmptyOrPlaceholder} video
173
+ {videosWithEmptyOrPlaceholder === 1 ? '' : 's'} without titles or with
174
+ placeholder titles (e.g., &quot;Asset #123&quot;) can be updated selectively.
175
+ </Text>
176
+ </Stack>
177
+ </Flex>
178
+ </Card>
179
+ )}
180
+ </Stack>
181
+ )}
182
+ </Box>
183
+ </Dialog>
184
+ )
185
+ }
186
+
187
+ export default function ResyncMetadata() {
188
+ const resyncMetadata = useResyncMuxMetadata()
189
+
190
+ if (!resyncMetadata.hasSecrets) {
191
+ return
192
+ }
193
+
194
+ if (resyncMetadata.dialogOpen) {
195
+ // eslint-disable-next-line consistent-return
196
+ return <ResyncMetadataDialog {...resyncMetadata} />
197
+ }
198
+
199
+ // eslint-disable-next-line consistent-return
200
+ return <Button mode="bleed" text="Resync Metadata" onClick={resyncMetadata.openDialog} />
201
+ }
@@ -23,7 +23,7 @@ import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
23
23
  import type {StagedUpload} from './Uploader'
24
24
 
25
25
  export type UploadConfigurationStateAction =
26
- | {action: 'encoding_tier'; value: UploadConfig['encoding_tier']}
26
+ | {action: 'video_quality'; value: UploadConfig['video_quality']}
27
27
  | {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
28
28
  | {action: 'mp4_support'; value: UploadConfig['mp4_support']}
29
29
  | {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
@@ -31,10 +31,11 @@ export type UploadConfigurationStateAction =
31
31
  | {action: 'public_policy'; value: UploadConfig['public_policy']}
32
32
  | TrackAction
33
33
 
34
- const ENCODING_OPTIONS = [
35
- {value: 'smart', label: 'Smart'},
36
- {value: 'baseline', label: 'Baseline'},
37
- ] as const satisfies {value: UploadConfig['encoding_tier']; label: string}[]
34
+ const VIDEO_QUALITY_LEVELS = [
35
+ {value: 'basic', label: 'Basic'},
36
+ {value: 'plus', label: 'Plus'},
37
+ {value: 'premium', label: 'Premium'},
38
+ ] as const satisfies {value: UploadConfig['video_quality']; label: string}[]
38
39
 
39
40
  const RESOLUTION_TIERS = [
40
41
  {value: '1080p', label: '1080p'},
@@ -63,7 +64,7 @@ export default function UploadConfiguration({
63
64
  }) {
64
65
  const id = useId()
65
66
  const autoTextTracks = useRef<NonNullable<UploadConfig['text_tracks']>>(
66
- pluginConfig.encoding_tier === 'smart' && pluginConfig.defaultAutogeneratedSubtitleLang
67
+ pluginConfig.video_quality === 'plus' && pluginConfig.defaultAutogeneratedSubtitleLang
67
68
  ? [
68
69
  {
69
70
  _id: uuid(),
@@ -78,21 +79,21 @@ export default function UploadConfiguration({
78
79
  const [config, dispatch] = useReducer(
79
80
  (prev: UploadConfig, action: UploadConfigurationStateAction) => {
80
81
  switch (action.action) {
81
- case 'encoding_tier':
82
- // If encoding tier switches to baseline, remove smart-only features
83
- if (action.value === 'baseline') {
82
+ case 'video_quality':
83
+ // If video quality level switches to basic, remove plus-only features
84
+ if (action.value === 'basic') {
84
85
  return Object.assign({}, prev, {
85
- encoding_tier: action.value,
86
+ video_quality: action.value,
86
87
  mp4_support: 'none',
87
88
  max_resolution_tier: '1080p',
88
89
  text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
89
90
  public_policy: true,
90
91
  signed_policy: false,
91
92
  })
92
- // If encoding tier switches to smart, add back in default smart features
93
+ // If video quality level switches to plus, add back in default plus features
93
94
  }
94
95
  return Object.assign({}, prev, {
95
- encoding_tier: action.value,
96
+ video_quality: action.value,
96
97
  mp4_support: pluginConfig.mp4_support,
97
98
  max_resolution_tier: pluginConfig.max_resolution_tier,
98
99
  text_tracks: [...autoTextTracks, ...(prev.text_tracks || [])],
@@ -138,7 +139,7 @@ export default function UploadConfiguration({
138
139
  }
139
140
  },
140
141
  {
141
- encoding_tier: pluginConfig.encoding_tier,
142
+ video_quality: pluginConfig.video_quality,
142
143
  max_resolution_tier: pluginConfig.max_resolution_tier,
143
144
  mp4_support: pluginConfig.mp4_support,
144
145
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
@@ -159,6 +160,7 @@ export default function UploadConfiguration({
159
160
  }, [])
160
161
  if (skipConfig) return null
161
162
 
163
+ const basicConfig = config.video_quality !== 'plus' && config.video_quality !== 'premium'
162
164
  const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
163
165
  (rt) => rt.value === pluginConfig.max_resolution_tier
164
166
  )
@@ -198,11 +200,11 @@ export default function UploadConfiguration({
198
200
  {!disableUploadConfig && (
199
201
  <Stack space={3} paddingBottom={2}>
200
202
  <FormField
201
- title="Encoding Tier"
203
+ title="Video Quality Level"
202
204
  description={
203
205
  <>
204
- The encoding tier informs the cost, quality, and available platform features for
205
- the asset.{' '}
206
+ The video quality level informs the cost, quality, and available platform features
207
+ for the asset.{' '}
206
208
  <a
207
209
  href="https://docs.mux.com/guides/use-encoding-tiers"
208
210
  target="_blank"
@@ -214,17 +216,17 @@ export default function UploadConfiguration({
214
216
  }
215
217
  >
216
218
  <Flex gap={3}>
217
- {ENCODING_OPTIONS.map(({value, label}) => {
219
+ {VIDEO_QUALITY_LEVELS.map(({value, label}) => {
218
220
  const inputId = `${id}--encodingtier-${value}`
219
221
  return (
220
222
  <Flex key={value} align="center" gap={2}>
221
223
  <Radio
222
- checked={config.encoding_tier === value}
224
+ checked={config.video_quality === value}
223
225
  name="asset-encodingtier"
224
226
  onChange={(e) =>
225
227
  dispatch({
226
- action: 'encoding_tier' as const,
227
- value: e.currentTarget.value as UploadConfig['encoding_tier'],
228
+ action: 'video_quality' as const,
229
+ value: e.currentTarget.value as UploadConfig['video_quality'],
228
230
  })
229
231
  }
230
232
  value={value}
@@ -239,7 +241,7 @@ export default function UploadConfiguration({
239
241
  </Flex>
240
242
  </FormField>
241
243
 
242
- {config.encoding_tier === 'smart' && maxSupportedResolution > 0 && (
244
+ {!basicConfig && maxSupportedResolution > 0 && (
243
245
  <FormField
244
246
  title="Resolution Tier"
245
247
  description={
@@ -286,12 +288,12 @@ export default function UploadConfiguration({
286
288
  </FormField>
287
289
  )}
288
290
 
289
- {config.encoding_tier === 'smart' && (
291
+ {!basicConfig && (
290
292
  <FormField title="Additional Configuration">
291
293
  <Stack space={2}>
292
294
  <PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
293
295
 
294
- {config.encoding_tier === 'smart' && (
296
+ {!basicConfig && (
295
297
  <Flex align="center" gap={2} padding={[0, 2]}>
296
298
  <Checkbox
297
299
  id={`${id}--mp4_support`}
@@ -319,7 +321,7 @@ export default function UploadConfiguration({
319
321
  </Stack>
320
322
  )}
321
323
 
322
- {!disableTextTrackConfig && config.encoding_tier === 'smart' && (
324
+ {!disableTextTrackConfig && !basicConfig && (
323
325
  <TextTracksEditor
324
326
  tracks={config.text_tracks}
325
327
  dispatch={dispatch}
@@ -329,9 +331,7 @@ export default function UploadConfiguration({
329
331
 
330
332
  <Box marginTop={4}>
331
333
  <Button
332
- disabled={
333
- config.encoding_tier === 'smart' && !config.public_policy && !config.signed_policy
334
- }
334
+ disabled={!basicConfig && !config.public_policy && !config.signed_policy}
335
335
  icon={UploadIcon}
336
336
  text="Upload"
337
337
  tone="positive"
@@ -388,7 +388,7 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
388
388
  mp4_support: config.mp4_support,
389
389
  playback_policy: setPlaybackPolicy(config),
390
390
  max_resolution_tier: config.max_resolution_tier,
391
- encoding_tier: config.encoding_tier,
391
+ video_quality: config.video_quality,
392
392
  normalize_audio: config.normalize_audio,
393
393
  }
394
394
  }
@@ -1,10 +1,12 @@
1
1
  import {SearchIcon} from '@sanity/icons'
2
- import {Card, Flex, Grid, Label, Stack, Text, TextInput} from '@sanity/ui'
2
+ import {Card, Flex, Grid, Inline, Label, Stack, Text, TextInput} from '@sanity/ui'
3
3
  import {useMemo, useState} from 'react'
4
4
 
5
5
  import useAssets from '../hooks/useAssets'
6
6
  import type {VideoAssetDocument} from '../util/types'
7
+ import ConfigureApi from './ConfigureApi'
7
8
  import ImportVideosFromMux from './ImportVideosFromMux'
9
+ import ResyncMetadata from './ResyncMetadata'
8
10
  import {SelectSortOptions} from './SelectSortOptions'
9
11
  import SpinnerBox from './SpinnerBox'
10
12
  import type {VideoDetailsProps} from './VideoDetails/useVideoDetails'
@@ -39,7 +41,13 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
39
41
  />
40
42
  <SelectSortOptions setSort={setSort} sort={sort} />
41
43
  </Flex>
42
- {placement === 'tool' && <ImportVideosFromMux />}
44
+ {placement === 'tool' && (
45
+ <Inline space={2}>
46
+ <ImportVideosFromMux />
47
+ <ResyncMetadata />
48
+ <ConfigureApi />
49
+ </Inline>
50
+ )}
43
51
  </Flex>
44
52
  <Stack space={3}>
45
53
  {assets?.length > 0 && (
@@ -3,11 +3,11 @@ import {useMemo, useState} from 'react'
3
3
  import {
4
4
  createHookFromObservableFactory,
5
5
  type DocumentStore,
6
- truncateString,
7
6
  useClient,
8
7
  useDocumentStore,
9
8
  } from 'sanity'
10
9
 
10
+ import {generateAssetPlaceholder} from '../util/assetTitlePlaceholder'
11
11
  import {parseMuxDate} from '../util/parsers'
12
12
  import type {MuxAsset, VideoAssetDocument} from '../util/types'
13
13
  import {SANITY_API_VERSION} from './useClient'
@@ -37,7 +37,7 @@ export default function useImportMuxAssets() {
37
37
  const dialogOpen = importState !== 'closed'
38
38
 
39
39
  const muxAssets = useMuxAssets({
40
- secrets: secretDocumentValues.value.secrets,
40
+ client,
41
41
  enabled: hasSecrets && dialogOpen,
42
42
  })
43
43
 
@@ -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.meta?.title ?? `Asset #${truncateString(asset.id, 15)}`,
104
+ filename: asset.meta?.title ?? generateAssetPlaceholder(asset.id),
105
105
  status: asset.status,
106
106
  data: asset,
107
107
  }