sanity-plugin-media 4.1.1 → 4.3.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.
Files changed (66) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -3
  3. package/dist/index.d.mts +227 -56
  4. package/dist/index.d.ts +227 -56
  5. package/dist/index.js +473 -184
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +476 -187
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +9 -2
  10. package/src/__tests__/fixtures/createEpicTestStore.ts +27 -0
  11. package/src/__tests__/fixtures/listenMock.ts +9 -0
  12. package/src/__tests__/fixtures/mockSanityClient.ts +84 -0
  13. package/src/__tests__/fixtures/renderWithProviders.tsx +54 -0
  14. package/src/__tests__/fixtures/rootState.ts +27 -0
  15. package/src/__tests__/fixtures/withinDialog.ts +28 -0
  16. package/src/components/AutoTagInputWrapper/index.tsx +82 -0
  17. package/src/components/Browser/Browser.test.tsx +44 -0
  18. package/src/components/Browser/index.tsx +12 -69
  19. package/src/components/Browser/useBrowserInit.ts +126 -0
  20. package/src/components/CardAsset/CardAsset.test.tsx +322 -0
  21. package/src/components/DialogAssetEdit/Details.tsx +123 -44
  22. package/src/components/DialogAssetEdit/DialogAssetEdit.test.tsx +215 -0
  23. package/src/components/DialogAssetEdit/index.tsx +138 -30
  24. package/src/components/DialogTagCreate/DialogTagCreate.test.tsx +120 -0
  25. package/src/components/DialogTagEdit/DialogTagEdit.test.tsx +164 -0
  26. package/src/components/FormBuilderTool/FormBuilderTool.test.tsx +62 -0
  27. package/src/components/FormBuilderTool/index.tsx +1 -1
  28. package/src/components/UploadDropzone/UploadDropzone.test.tsx +39 -0
  29. package/src/contexts/ToolOptionsContext.tsx +9 -3
  30. package/src/formSchema/index.test.ts +55 -0
  31. package/src/formSchema/index.ts +28 -12
  32. package/src/hooks/useVersionedClient.ts +1 -1
  33. package/src/index.ts +4 -1
  34. package/src/modules/assets/deleteAndUpdateEpics.test.ts +86 -0
  35. package/src/modules/assets/fetchEpic.test.ts +72 -0
  36. package/src/modules/assets/reducer.test.ts +90 -0
  37. package/src/modules/assets/tagsAndListenerEpics.test.ts +205 -0
  38. package/src/modules/dialog/epics.test.ts +167 -0
  39. package/src/modules/dialog/reducer.test.ts +184 -0
  40. package/src/modules/notifications/epics.test.ts +373 -0
  41. package/src/modules/notifications/index.ts +24 -4
  42. package/src/modules/notifications/reducer.test.ts +53 -0
  43. package/src/modules/search/index.test.ts +35 -0
  44. package/src/modules/selectors.test.ts +20 -0
  45. package/src/modules/tags/epics.test.ts +95 -0
  46. package/src/modules/tags/index.test.ts +41 -0
  47. package/src/modules/uploads/epics.test.ts +108 -0
  48. package/src/modules/uploads/index.test.ts +58 -0
  49. package/src/operators/checkTagName.test.ts +28 -0
  50. package/src/types/index.ts +25 -7
  51. package/src/utils/applyMediaTags.ts +86 -0
  52. package/src/utils/blocksToText.test.ts +42 -0
  53. package/src/utils/constructFilter.test.ts +119 -0
  54. package/src/utils/generatePreviewBlobUrl.test.ts +69 -0
  55. package/src/utils/getAssetResolution.test.ts +12 -0
  56. package/src/utils/getDocumentAssetIds.test.ts +49 -0
  57. package/src/utils/getSchemeColor.test.ts +11 -0
  58. package/src/utils/getTagSelectOptions.test.ts +43 -0
  59. package/src/utils/getUniqueDocuments.test.ts +25 -0
  60. package/src/utils/imageDprUrl.test.ts +45 -0
  61. package/src/utils/isSupportedAssetType.test.ts +15 -0
  62. package/src/utils/mediaField.ts +72 -0
  63. package/src/utils/sanitizeFormData.test.ts +58 -0
  64. package/src/utils/typeGuards.test.ts +17 -0
  65. package/src/utils/uploadSanityAsset.test.ts +28 -0
  66. package/src/utils/withMaxConcurrency.test.ts +42 -0
package/dist/index.mjs CHANGED
@@ -3,16 +3,16 @@ import { useClient, useColorSchemeValue, useSchema, Preview, useDocumentStore, W
3
3
  import { ThLargeIcon, ThListIcon, SortIcon, CloseIcon, SelectIcon, AddIcon, SearchIcon, PlugIcon, ClipboardIcon, DownloadIcon, ChevronDownIcon, ErrorOutlineIcon, WarningOutlineIcon, EditIcon, ArrowUpIcon, ArrowDownIcon, TrashIcon, ComposeIcon, Icon, UploadIcon, WarningFilledIcon, CheckmarkCircleIcon, ChevronUpIcon, ImageIcon } from "@sanity/icons";
4
4
  import { Inline, Button, usePortal, MenuButton, Menu as Menu$2, MenuItem, MenuDivider, Box, studioTheme, rem, Flex, Label, Text, TextInput, Card, MenuGroup, useMediaIndex, Tooltip, Switch, Popover, Stack, Dialog as Dialog$1, TextArea, TabList, Tab, TabPanel, Container as Container$2, Spinner, Checkbox, Grid, useToast, PortalProvider, useLayer, Portal } from "@sanity/ui";
5
5
  import { useRef, useCallback, useEffect, createContext, useContext, useMemo, useState, forwardRef, memo, Component } from "react";
6
- import groq from "groq";
6
+ import { css, createGlobalStyle, styled } from "styled-components";
7
7
  import { useSelector, useDispatch, Provider } from "react-redux";
8
8
  import { createAction, createSlice, isAnyOf, createSelector, combineReducers, configureStore } from "@reduxjs/toolkit";
9
- import { nanoid } from "nanoid";
9
+ import pluralize from "pluralize";
10
10
  import { ofType, combineEpics, createEpicMiddleware } from "redux-observable";
11
11
  import { iif, throwError, of, from, EMPTY, Subject, Observable, merge, empty } from "rxjs";
12
12
  import { delay, mergeMap, filter, withLatestFrom, catchError, switchMap, bufferTime, debounceTime, first, map, takeUntil } from "rxjs/operators";
13
+ import groq from "groq";
14
+ import { nanoid } from "nanoid";
13
15
  import { uuid } from "@sanity/uuid";
14
- import { css, createGlobalStyle, styled } from "styled-components";
15
- import pluralize from "pluralize";
16
16
  import { useNProgress } from "@tanem/react-nprogress";
17
17
  import { hues, white } from "@sanity/color";
18
18
  import Select, { components } from "react-select";
@@ -176,7 +176,71 @@ const useKeyPress = (hotkey, onPress) => {
176
176
  return useEffect(() => (window.addEventListener("keydown", downHandler), window.addEventListener("keyup", upHandler), () => {
177
177
  window.removeEventListener("keydown", downHandler), window.removeEventListener("keyup", upHandler);
178
178
  }), [downHandler, upHandler]), keyPressed;
179
- }, divider = { type: "divider" }, inputs = {
179
+ }, AssetSourceDispatchContext = createContext(void 0), AssetBrowserDispatchProvider = (props) => {
180
+ const { children, onSelect } = props, contextValue = {
181
+ onSelect
182
+ };
183
+ return /* @__PURE__ */ jsx(AssetSourceDispatchContext.Provider, { value: contextValue, children });
184
+ }, useAssetSourceActions = () => {
185
+ const context = useContext(AssetSourceDispatchContext);
186
+ if (context === void 0)
187
+ throw new Error("useAssetSourceActions must be used within an AssetSourceDispatchProvider");
188
+ return context;
189
+ }, useVersionedClient = () => useClient({ apiVersion: "2025-10-02" }), customScrollbar = css`
190
+ ::-webkit-scrollbar {
191
+ width: 14px;
192
+ }
193
+
194
+ ::-webkit-scrollbar-thumb {
195
+ border-radius: 10px;
196
+ border: 4px solid rgba(0, 0, 0, 0);
197
+ background: var(--card-border-color);
198
+ background-clip: padding-box;
199
+
200
+ &:hover {
201
+ background: var(--card-muted-fg-color);
202
+ background-clip: padding-box;
203
+ }
204
+ }
205
+ `, GlobalStyle = createGlobalStyle`
206
+ .media__custom-scrollbar {
207
+ ${customScrollbar}
208
+ }
209
+
210
+ // @sanity/ui overrides
211
+
212
+ // Custom scrollbar on Box (used in Dialogs)
213
+ div[data-ui="Box"] {
214
+ ${customScrollbar}
215
+ }
216
+
217
+ // Dialog background color
218
+ div[data-ui="Dialog"] {
219
+ background-color: rgba(15, 17, 18, 0.9);
220
+ }
221
+
222
+ `, useTypedSelector = useSelector, ORDER_DICTIONARY = {
223
+ _createdAt: {
224
+ asc: "Last created: Oldest first",
225
+ desc: "Last created: Newest first"
226
+ },
227
+ _updatedAt: {
228
+ asc: "Last updated: Oldest first",
229
+ desc: "Last updated: Newest first"
230
+ },
231
+ mimeType: {
232
+ asc: "MIME type: A to Z",
233
+ desc: "MIME type: Z to A"
234
+ },
235
+ originalFilename: {
236
+ asc: "File name: A to Z",
237
+ desc: "File name: Z to A"
238
+ },
239
+ size: {
240
+ asc: "File size: Smallest first",
241
+ desc: "File size: Largest first"
242
+ }
243
+ }, getOrderTitle = (field, direction) => ORDER_DICTIONARY[field][direction], divider = { type: "divider" }, inputs = {
180
244
  altText: {
181
245
  assetTypes: ["file", "image"],
182
246
  field: "altText",
@@ -539,38 +603,7 @@ const useKeyPress = (hotkey, onPress) => {
539
603
  ], GRID_TEMPLATE_COLUMNS = {
540
604
  SMALL: "3rem 100px auto 1.5rem",
541
605
  LARGE: "3rem 100px auto 5.5rem 5.5rem 3.5rem 8.5rem 4.75rem 2rem"
542
- }, PANEL_HEIGHT = 32, TAG_DOCUMENT_NAME = "media.tag", TAGS_PANEL_WIDTH = 250, AssetSourceDispatchContext = createContext(void 0), AssetBrowserDispatchProvider = (props) => {
543
- const { children, onSelect } = props, contextValue = {
544
- onSelect
545
- };
546
- return /* @__PURE__ */ jsx(AssetSourceDispatchContext.Provider, { value: contextValue, children });
547
- }, useAssetSourceActions = () => {
548
- const context = useContext(AssetSourceDispatchContext);
549
- if (context === void 0)
550
- throw new Error("useAssetSourceActions must be used within an AssetSourceDispatchProvider");
551
- return context;
552
- }, useVersionedClient = () => useClient({ apiVersion: "2022-10-01" }), ORDER_DICTIONARY = {
553
- _createdAt: {
554
- asc: "Last created: Oldest first",
555
- desc: "Last created: Newest first"
556
- },
557
- _updatedAt: {
558
- asc: "Last updated: Oldest first",
559
- desc: "Last updated: Newest first"
560
- },
561
- mimeType: {
562
- asc: "MIME type: A to Z",
563
- desc: "MIME type: Z to A"
564
- },
565
- originalFilename: {
566
- asc: "File name: A to Z",
567
- desc: "File name: Z to A"
568
- },
569
- size: {
570
- asc: "File size: Smallest first",
571
- desc: "File size: Largest first"
572
- }
573
- }, getOrderTitle = (field, direction) => ORDER_DICTIONARY[field][direction], debugThrottle = (throttled) => function(source) {
606
+ }, PANEL_HEIGHT = 32, TAG_DOCUMENT_NAME = "media.tag", TAGS_PANEL_WIDTH = 250, debugThrottle = (throttled) => function(source) {
574
607
  return iif(
575
608
  () => !!throttled,
576
609
  source.pipe(
@@ -1614,40 +1647,7 @@ const UPLOADS_ACTIONS = {
1614
1647
  (assetsPicked) => assetsPicked.length
1615
1648
  ), assetsActions = { ...assetsSlice.actions };
1616
1649
  var assetsReducer = assetsSlice.reducer;
1617
- const customScrollbar = css`
1618
- ::-webkit-scrollbar {
1619
- width: 14px;
1620
- }
1621
-
1622
- ::-webkit-scrollbar-thumb {
1623
- border-radius: 10px;
1624
- border: 4px solid rgba(0, 0, 0, 0);
1625
- background: var(--card-border-color);
1626
- background-clip: padding-box;
1627
-
1628
- &:hover {
1629
- background: var(--card-muted-fg-color);
1630
- background-clip: padding-box;
1631
- }
1632
- }
1633
- `, GlobalStyle = createGlobalStyle`
1634
- .media__custom-scrollbar {
1635
- ${customScrollbar}
1636
- }
1637
-
1638
- // @sanity/ui overrides
1639
-
1640
- // Custom scrollbar on Box (used in Dialogs)
1641
- div[data-ui="Box"] {
1642
- ${customScrollbar}
1643
- }
1644
-
1645
- // Dialog background color
1646
- div[data-ui="Dialog"] {
1647
- background-color: rgba(15, 17, 18, 0.9);
1648
- }
1649
-
1650
- `, useTypedSelector = useSelector, initialState$4 = {
1650
+ const initialState$4 = {
1651
1651
  items: []
1652
1652
  }, dialogSlice = createSlice({
1653
1653
  name: "dialog",
@@ -2368,18 +2368,22 @@ const Container$1 = styled(Box)(({ $scheme, theme }) => css`
2368
2368
  components: {
2369
2369
  details: options?.components?.details
2370
2370
  },
2371
+ createTagsOnUpload: options?.createTagsOnUpload ?? !0,
2371
2372
  creditLine: {
2372
2373
  enabled: options?.creditLine?.enabled || !1,
2373
2374
  excludeSources: creditLineExcludeSources
2374
2375
  },
2375
- directUploads: options?.directUploads ?? !0
2376
+ directUploads: options?.directUploads ?? !0,
2377
+ locales: options?.locales
2376
2378
  };
2377
2379
  }, [
2378
2380
  options?.creditLine?.enabled,
2379
2381
  options?.components,
2382
+ options?.createTagsOnUpload,
2380
2383
  options?.creditLine?.excludeSources,
2381
2384
  options?.maximumUploadSize,
2382
- options?.directUploads
2385
+ options?.directUploads,
2386
+ options?.locales
2383
2387
  ]);
2384
2388
  return /* @__PURE__ */ jsx(ToolOptionsContext.Provider, { value, children });
2385
2389
  }, useToolOptions = () => {
@@ -2658,21 +2662,35 @@ const DebugControls = () => {
2658
2662
  ] })
2659
2663
  }
2660
2664
  ) : null;
2661
- }, tagOptionSchema = z.object({
2665
+ };
2666
+ function localizedStringSchema(locales) {
2667
+ if (!locales || locales.length === 0)
2668
+ return z.string().trim().optional();
2669
+ const shape = {};
2670
+ for (const locale of locales)
2671
+ shape[locale.id] = z.string().trim().optional();
2672
+ return z.object(shape).passthrough();
2673
+ }
2674
+ const tagOptionSchema = z.object({
2662
2675
  label: z.string().trim().min(1, { message: "Label cannot be empty" }),
2663
2676
  value: z.string().trim().min(1, { message: "Value cannot be empty" })
2664
- }), assetFormSchema = z.object({
2665
- altText: z.string().trim().optional(),
2666
- creditLine: z.string().trim().optional(),
2667
- description: z.string().trim().optional(),
2668
- opt: z.object({
2669
- media: z.object({
2670
- tags: z.array(tagOptionSchema).nullable()
2671
- })
2672
- }),
2673
- originalFilename: z.string().trim().min(1, { message: "Filename cannot be empty" }),
2674
- title: z.string().trim().optional()
2675
- }), tagFormSchema = z.object({
2677
+ });
2678
+ function getAssetFormSchema(locales) {
2679
+ return z.object({
2680
+ altText: localizedStringSchema(locales),
2681
+ creditLine: localizedStringSchema(locales),
2682
+ description: localizedStringSchema(locales),
2683
+ opt: z.object({
2684
+ media: z.object({
2685
+ tags: z.array(tagOptionSchema).nullable()
2686
+ })
2687
+ }),
2688
+ originalFilename: z.string().trim().min(1, { message: "Filename cannot be empty" }),
2689
+ title: localizedStringSchema(locales)
2690
+ });
2691
+ }
2692
+ getAssetFormSchema();
2693
+ const tagFormSchema = z.object({
2676
2694
  name: z.string().min(1, { message: "Name cannot be empty" })
2677
2695
  });
2678
2696
  function getUniqueDocuments(documents) {
@@ -3107,6 +3125,11 @@ const imageDprUrl = (asset, options) => {
3107
3125
  )
3108
3126
  ] });
3109
3127
  });
3128
+ function toStringField(value) {
3129
+ if (typeof value == "string") return value;
3130
+ if (typeof value == "object" && value !== null)
3131
+ return Object.values(value).find((v) => v) || void 0;
3132
+ }
3110
3133
  function Details({
3111
3134
  formUpdating,
3112
3135
  handleCreateTag,
@@ -3116,8 +3139,10 @@ function Details({
3116
3139
  allTagOptions,
3117
3140
  assetTagOptions,
3118
3141
  currentAsset,
3119
- creditLine
3142
+ creditLine,
3143
+ locales
3120
3144
  }) {
3145
+ const hasLocales = locales && locales.length > 0, [activeLocaleTab, setActiveLocaleTab] = useState(0);
3121
3146
  return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3122
3147
  /* @__PURE__ */ jsx(
3123
3148
  FormFieldInputTags,
@@ -3144,51 +3169,117 @@ function Details({
3144
3169
  value: currentAsset?.originalFilename
3145
3170
  }
3146
3171
  ),
3147
- /* @__PURE__ */ jsx(
3148
- FormFieldInputText,
3149
- {
3150
- ...register("title"),
3151
- disabled: formUpdating,
3152
- error: errors?.title?.message,
3153
- label: "Title",
3154
- name: "title",
3155
- value: currentAsset?.title
3156
- }
3157
- ),
3158
- /* @__PURE__ */ jsx(
3159
- FormFieldInputText,
3160
- {
3161
- ...register("altText"),
3162
- disabled: formUpdating,
3163
- error: errors?.altText?.message,
3164
- label: "Alt Text",
3165
- name: "altText",
3166
- value: currentAsset?.altText
3167
- }
3168
- ),
3169
- /* @__PURE__ */ jsx(
3170
- FormFieldInputTextarea,
3171
- {
3172
- ...register("description"),
3173
- disabled: formUpdating,
3174
- error: errors?.description?.message,
3175
- label: "Description",
3176
- name: "description",
3177
- rows: 5,
3178
- value: currentAsset?.description
3179
- }
3180
- ),
3181
- creditLine?.enabled && /* @__PURE__ */ jsx(
3182
- FormFieldInputText,
3183
- {
3184
- ...register("creditLine"),
3185
- error: errors?.creditLine?.message,
3186
- label: "Credit",
3187
- name: "creditLine",
3188
- value: currentAsset?.creditLine,
3189
- disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3190
- }
3191
- )
3172
+ hasLocales ? /* @__PURE__ */ jsx(Card, { marginTop: 2, shadow: 1, padding: 3, radius: 1, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3173
+ /* @__PURE__ */ jsx(TabList, { space: 2, children: locales.map((locale, idx) => /* @__PURE__ */ jsx(
3174
+ Tab,
3175
+ {
3176
+ id: `locale-tab-${locale.id}`,
3177
+ "aria-controls": `locale-panel-${locale.id}`,
3178
+ selected: activeLocaleTab === idx,
3179
+ onClick: () => setActiveLocaleTab(idx),
3180
+ label: locale.title
3181
+ },
3182
+ locale.id
3183
+ )) }),
3184
+ locales.map((locale, idx) => /* @__PURE__ */ jsx(
3185
+ TabPanel,
3186
+ {
3187
+ id: `locale-panel-${locale.id}`,
3188
+ "aria-labelledby": `locale-tab-${locale.id}`,
3189
+ hidden: activeLocaleTab !== idx,
3190
+ children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3191
+ /* @__PURE__ */ jsx(
3192
+ FormFieldInputText,
3193
+ {
3194
+ ...register(`title.${locale.id}`),
3195
+ disabled: formUpdating,
3196
+ error: errors?.title?.[locale.id]?.message,
3197
+ label: "Title",
3198
+ name: `title.${locale.id}`
3199
+ }
3200
+ ),
3201
+ /* @__PURE__ */ jsx(
3202
+ FormFieldInputText,
3203
+ {
3204
+ ...register(`altText.${locale.id}`),
3205
+ disabled: formUpdating,
3206
+ error: errors?.altText?.[locale.id]?.message,
3207
+ label: "Alt Text",
3208
+ name: `altText.${locale.id}`
3209
+ }
3210
+ ),
3211
+ /* @__PURE__ */ jsx(
3212
+ FormFieldInputTextarea,
3213
+ {
3214
+ ...register(`description.${locale.id}`),
3215
+ disabled: formUpdating,
3216
+ error: errors?.description?.[locale.id]?.message,
3217
+ label: "Description",
3218
+ name: `description.${locale.id}`,
3219
+ rows: 5
3220
+ }
3221
+ ),
3222
+ creditLine?.enabled && /* @__PURE__ */ jsx(
3223
+ FormFieldInputText,
3224
+ {
3225
+ ...register(`creditLine.${locale.id}`),
3226
+ error: errors?.creditLine?.[locale.id]?.message,
3227
+ label: "Credit",
3228
+ name: `creditLine.${locale.id}`,
3229
+ disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3230
+ }
3231
+ )
3232
+ ] })
3233
+ },
3234
+ locale.id
3235
+ ))
3236
+ ] }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3237
+ /* @__PURE__ */ jsx(
3238
+ FormFieldInputText,
3239
+ {
3240
+ ...register("title"),
3241
+ disabled: formUpdating,
3242
+ error: errors?.title?.message,
3243
+ label: "Title",
3244
+ name: "title",
3245
+ value: toStringField(currentAsset?.title)
3246
+ }
3247
+ ),
3248
+ /* @__PURE__ */ jsx(
3249
+ FormFieldInputText,
3250
+ {
3251
+ ...register("altText"),
3252
+ disabled: formUpdating,
3253
+ error: errors?.altText?.message,
3254
+ label: "Alt Text",
3255
+ name: "altText",
3256
+ value: toStringField(currentAsset?.altText)
3257
+ }
3258
+ ),
3259
+ /* @__PURE__ */ jsx(
3260
+ FormFieldInputTextarea,
3261
+ {
3262
+ ...register("description"),
3263
+ disabled: formUpdating,
3264
+ error: errors?.description?.message,
3265
+ label: "Description",
3266
+ name: "description",
3267
+ rows: 5,
3268
+ value: toStringField(currentAsset?.description)
3269
+ }
3270
+ ),
3271
+ creditLine?.enabled && /* @__PURE__ */ jsx(
3272
+ FormFieldInputText,
3273
+ {
3274
+ ...register("creditLine"),
3275
+ error: errors?.creditLine?.message,
3276
+ label: "Credit",
3277
+ name: "creditLine",
3278
+ value: toStringField(currentAsset?.creditLine),
3279
+ disabled: formUpdating || creditLine?.excludeSources?.includes(currentAsset?.source?.name)
3280
+ }
3281
+ )
3282
+ ] })
3192
3283
  ] });
3193
3284
  }
3194
3285
  function renderDefaultDetails(props) {
@@ -3198,16 +3289,37 @@ const DialogAssetEdit = (props) => {
3198
3289
  const {
3199
3290
  children,
3200
3291
  dialog: { assetId, id, lastCreatedTag, lastRemovedTagIds }
3201
- } = props, client = useVersionedClient(), scheme = useColorSchemeValue(), documentStore = useDocumentStore(), dispatch = useDispatch(), assetItem = useTypedSelector((state) => selectAssetById(state, String(assetId))), tags = useTypedSelector(selectTags), assetUpdatedPrev = useRef(void 0), [assetSnapshot, setAssetSnapshot] = useState(assetItem?.asset), [tabSection, setTabSection] = useState("details"), currentAsset = assetItem ? assetItem?.asset : assetSnapshot, allTagOptions = getTagSelectOptions(tags), assetTagOptions = useTypedSelector(selectTagSelectOptions(currentAsset)), { creditLine, components: { details: CustomDetails } = {} } = useToolOptions(), generateDefaultValues = useCallback(
3202
- (asset) => ({
3203
- altText: asset?.altText || "",
3204
- creditLine: asset?.creditLine || "",
3205
- description: asset?.description || "",
3206
- originalFilename: asset?.originalFilename || "",
3207
- opt: { media: { tags: assetTagOptions } },
3208
- title: asset?.title || ""
3209
- }),
3210
- [assetTagOptions]
3292
+ } = props, client = useVersionedClient(), scheme = useColorSchemeValue(), documentStore = useDocumentStore(), dispatch = useDispatch(), assetItem = useTypedSelector((state) => selectAssetById(state, String(assetId))), tags = useTypedSelector(selectTags), assetUpdatedPrev = useRef(void 0), [assetSnapshot, setAssetSnapshot] = useState(assetItem?.asset), [tabSection, setTabSection] = useState("details"), currentAsset = assetItem ? assetItem?.asset : assetSnapshot, allTagOptions = getTagSelectOptions(tags), assetTagOptions = useTypedSelector(selectTagSelectOptions(currentAsset)), { creditLine, components: { details: CustomDetails } = {}, locales } = useToolOptions(), generateDefaultValues = useCallback(
3293
+ (asset) => {
3294
+ if (locales && locales.length > 0) {
3295
+ const makeLocaleObj = (field) => {
3296
+ const obj = {};
3297
+ for (let i = 0; i < locales.length; i++) {
3298
+ const locale = locales[i];
3299
+ typeof field == "object" && field && field[locale.id] ? obj[locale.id] = field[locale.id] : typeof field == "string" ? obj[locale.id] = i === 0 ? field : "" : obj[locale.id] = "";
3300
+ }
3301
+ return obj;
3302
+ };
3303
+ return {
3304
+ altText: makeLocaleObj(asset?.altText),
3305
+ creditLine: makeLocaleObj(asset?.creditLine),
3306
+ description: makeLocaleObj(asset?.description),
3307
+ originalFilename: asset?.originalFilename || "",
3308
+ opt: { media: { tags: assetTagOptions } },
3309
+ title: makeLocaleObj(asset?.title)
3310
+ };
3311
+ }
3312
+ const flattenField = (field) => typeof field == "string" ? field : typeof field == "object" && field !== null && Object.values(field).find((v) => v) || "";
3313
+ return {
3314
+ altText: flattenField(asset?.altText),
3315
+ creditLine: flattenField(asset?.creditLine),
3316
+ description: flattenField(asset?.description),
3317
+ originalFilename: asset?.originalFilename || "",
3318
+ opt: { media: { tags: assetTagOptions } },
3319
+ title: flattenField(asset?.title)
3320
+ };
3321
+ },
3322
+ [assetTagOptions, locales]
3211
3323
  ), {
3212
3324
  control,
3213
3325
  // Read the formState before render to subscribe the form state through Proxy
@@ -3220,7 +3332,7 @@ const DialogAssetEdit = (props) => {
3220
3332
  } = useForm({
3221
3333
  defaultValues: generateDefaultValues(assetItem?.asset),
3222
3334
  mode: "onChange",
3223
- resolver: zodResolver(assetFormSchema)
3335
+ resolver: zodResolver(getAssetFormSchema(locales))
3224
3336
  }), formUpdating = !assetItem || assetItem?.updating, handleClose = useCallback(() => {
3225
3337
  dispatch(dialogActions.remove({ id }));
3226
3338
  }, [dispatch, id]), handleDelete = useCallback(() => {
@@ -3243,7 +3355,39 @@ const DialogAssetEdit = (props) => {
3243
3355
  );
3244
3356
  },
3245
3357
  [currentAsset?._id, dispatch]
3246
- ), onSubmit = useCallback(
3358
+ ), hasOrphanedLocales = useMemo(() => {
3359
+ if (!currentAsset) return !1;
3360
+ const isLocaleObj = (v) => typeof v == "object" && v !== null && !Array.isArray(v), fields = [
3361
+ currentAsset.title,
3362
+ currentAsset.altText,
3363
+ currentAsset.description,
3364
+ ...currentAsset._type === "sanity.imageAsset" ? [currentAsset.creditLine] : []
3365
+ ];
3366
+ if (!fields.some((f) => isLocaleObj(f))) return !1;
3367
+ if (!locales || locales.length === 0) return !0;
3368
+ const configuredIds = new Set(locales.map((l) => l.id));
3369
+ return fields.some((f) => isLocaleObj(f) ? Object.keys(f).some((k) => !configuredIds.has(k)) : !1);
3370
+ }, [currentAsset, locales]), handleCleanupLocales = useCallback(async () => {
3371
+ if (!currentAsset) return;
3372
+ const cleanField = (field) => {
3373
+ if (typeof field != "object" || field === null || Array.isArray(field)) return field;
3374
+ const obj = field;
3375
+ if (!locales || locales.length === 0)
3376
+ return Object.keys(obj).sort().map((k) => obj[k]).find((v) => v) || "";
3377
+ const configuredIds = new Set(locales.map((l) => l.id)), cleaned = {};
3378
+ for (const [key, val] of Object.entries(obj))
3379
+ configuredIds.has(key) && (cleaned[key] = val);
3380
+ return cleaned;
3381
+ };
3382
+ await client.patch(currentAsset._id).set({
3383
+ title: cleanField(currentAsset.title),
3384
+ altText: cleanField(currentAsset.altText),
3385
+ description: cleanField(currentAsset.description),
3386
+ ...currentAsset._type === "sanity.imageAsset" && {
3387
+ creditLine: cleanField(currentAsset.creditLine)
3388
+ }
3389
+ }).commit();
3390
+ }, [client, currentAsset, locales]), onSubmit = useCallback(
3247
3391
  (formData) => {
3248
3392
  if (!assetItem?.asset)
3249
3393
  return;
@@ -3291,27 +3435,42 @@ const DialogAssetEdit = (props) => {
3291
3435
  }, [getValues, lastRemovedTagIds, setValue]), useEffect(() => {
3292
3436
  assetUpdatedPrev.current !== assetItem?.asset._updatedAt && reset(generateDefaultValues(assetItem?.asset)), assetUpdatedPrev.current = assetItem?.asset._updatedAt;
3293
3437
  }, [assetItem?.asset, generateDefaultValues, reset]);
3294
- const Footer = () => /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Flex, { justify: "space-between", children: [
3295
- /* @__PURE__ */ jsx(
3296
- Button,
3297
- {
3298
- disabled: formUpdating,
3299
- fontSize: 1,
3300
- mode: "bleed",
3301
- onClick: handleDelete,
3302
- text: "Delete",
3303
- tone: "critical"
3304
- }
3305
- ),
3306
- /* @__PURE__ */ jsx(
3307
- FormSubmitButton,
3308
- {
3309
- disabled: formUpdating || !isDirty || !isValid,
3310
- isValid,
3311
- lastUpdated: currentAsset?._updatedAt,
3312
- onClick: handleSubmit(onSubmit)
3313
- }
3314
- )
3438
+ const Footer = () => /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3439
+ hasOrphanedLocales && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
3440
+ /* @__PURE__ */ jsx(Text, { size: 1, children: "This asset has localized fields that are no longer configured. Clean them up to avoid validation errors." }),
3441
+ /* @__PURE__ */ jsx(
3442
+ Button,
3443
+ {
3444
+ fontSize: 1,
3445
+ mode: "ghost",
3446
+ onClick: handleCleanupLocales,
3447
+ text: "Cleanup localized fields",
3448
+ tone: "caution"
3449
+ }
3450
+ )
3451
+ ] }) }),
3452
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", children: [
3453
+ /* @__PURE__ */ jsx(
3454
+ Button,
3455
+ {
3456
+ disabled: formUpdating,
3457
+ fontSize: 1,
3458
+ mode: "bleed",
3459
+ onClick: handleDelete,
3460
+ text: "Delete",
3461
+ tone: "critical"
3462
+ }
3463
+ ),
3464
+ /* @__PURE__ */ jsx(
3465
+ FormSubmitButton,
3466
+ {
3467
+ disabled: formUpdating || !isDirty || !isValid || hasOrphanedLocales,
3468
+ isValid,
3469
+ lastUpdated: currentAsset?._updatedAt,
3470
+ onClick: handleSubmit(onSubmit)
3471
+ }
3472
+ )
3473
+ ] })
3315
3474
  ] }) });
3316
3475
  if (!currentAsset)
3317
3476
  return null;
@@ -3325,7 +3484,8 @@ const DialogAssetEdit = (props) => {
3325
3484
  allTagOptions,
3326
3485
  handleCreateTag,
3327
3486
  currentAsset,
3328
- creditLine
3487
+ creditLine,
3488
+ locales
3329
3489
  };
3330
3490
  return /* @__PURE__ */ jsxs(
3331
3491
  Dialog,
@@ -5109,6 +5269,9 @@ const TableRowUpload = (props) => {
5109
5269
  reducers: {}
5110
5270
  });
5111
5271
  var selectedReducer = selectedSlice.reducer;
5272
+ function messageFromGenericErrorPayload(payload) {
5273
+ return !payload || typeof payload != "object" ? "Unknown error" : "error" in payload && payload.error && typeof payload.error == "object" && payload.error !== null && "message" in payload.error ? String(payload.error.message) : "message" in payload && typeof payload.message == "string" ? String(payload.message) : "Unknown error";
5274
+ }
5112
5275
  const initialState = {
5113
5276
  items: []
5114
5277
  }, notificationsSlice = createSlice({
@@ -5195,11 +5358,11 @@ const initialState = {
5195
5358
  uploadsActions.uploadError.type
5196
5359
  ),
5197
5360
  mergeMap((action) => {
5198
- const error = action.payload?.error;
5361
+ const title = `An error occurred: ${messageFromGenericErrorPayload(action.payload)}`;
5199
5362
  return of(
5200
5363
  notificationsSlice.actions.add({
5201
5364
  status: "error",
5202
- title: `An error occured: ${error.message}`
5365
+ title
5203
5366
  })
5204
5367
  );
5205
5368
  })
@@ -5447,24 +5610,81 @@ const UploadDropzone = (props) => {
5447
5610
  isDragActive && /* @__PURE__ */ jsx(DragActiveContainer, { children: /* @__PURE__ */ jsx(Flex, { direction: "column", justify: "center", style: { color: white.hex }, children: /* @__PURE__ */ jsx(Text, { size: 3, style: { color: "inherit" }, children: "Drop files to upload" }) }) }),
5448
5611
  children
5449
5612
  ] }) });
5450
- }, BrowserContent = ({ onClose }) => {
5451
- const client = useVersionedClient(), [portalElement, setPortalElement] = useState(null), dispatch = useDispatch();
5452
- return useEffect(() => {
5453
- const handleAssetUpdate = (update) => {
5454
- const { documentId, result, transition } = update;
5455
- transition === "appear" && dispatch(assetsActions.listenerCreateQueue({ asset: result })), transition === "disappear" && dispatch(assetsActions.listenerDeleteQueue({ assetId: documentId })), transition === "update" && dispatch(assetsActions.listenerUpdateQueue({ asset: result }));
5456
- }, handleTagUpdate = (update) => {
5457
- const { documentId, result, transition } = update;
5458
- transition === "appear" && dispatch(tagsActions.listenerCreateQueue({ tag: result })), transition === "disappear" && dispatch(tagsActions.listenerDeleteQueue({ tagId: documentId })), transition === "update" && dispatch(tagsActions.listenerUpdateQueue({ tag: result }));
5459
- };
5460
- dispatch(assetsActions.loadPageIndex({ pageIndex: 0 })), dispatch(tagsActions.fetchRequest());
5461
- const subscriptionAsset = client.listen(
5613
+ };
5614
+ function getMediaTagNames(schemaType) {
5615
+ const mediaTags = schemaType?.options?.mediaTags;
5616
+ if (!mediaTags?.length) return [];
5617
+ const unique = new Set(
5618
+ mediaTags.map((t) => t?.trim()).filter((t) => !!t?.length)
5619
+ );
5620
+ return Array.from(unique);
5621
+ }
5622
+ function createAssetHandler(dispatch) {
5623
+ return (update) => {
5624
+ const { documentId, result, transition } = update;
5625
+ switch (transition) {
5626
+ case "appear":
5627
+ dispatch(assetsActions.listenerCreateQueue({ asset: result }));
5628
+ break;
5629
+ case "disappear":
5630
+ dispatch(assetsActions.listenerDeleteQueue({ assetId: documentId }));
5631
+ break;
5632
+ case "update":
5633
+ dispatch(assetsActions.listenerUpdateQueue({ asset: result }));
5634
+ break;
5635
+ }
5636
+ };
5637
+ }
5638
+ function createTagHandler(dispatch) {
5639
+ return (update) => {
5640
+ const { documentId, result, transition } = update;
5641
+ switch (transition) {
5642
+ case "appear":
5643
+ dispatch(tagsActions.listenerCreateQueue({ tag: result }));
5644
+ break;
5645
+ case "disappear":
5646
+ dispatch(tagsActions.listenerDeleteQueue({ tagId: documentId }));
5647
+ break;
5648
+ case "update":
5649
+ dispatch(tagsActions.listenerUpdateQueue({ tag: result }));
5650
+ break;
5651
+ }
5652
+ };
5653
+ }
5654
+ function useBrowserInit(client, schemaType) {
5655
+ const dispatch = useDispatch(), tagsByIds = useSelector((state) => state.tags.byIds), tagsFetchCount = useSelector((state) => state.tags.fetchCount), tagNames = getMediaTagNames(schemaType), hasMediaTags = tagNames.length > 0;
5656
+ useEffect(() => {
5657
+ hasMediaTags || dispatch(searchActions.facetsClear()), dispatch(tagsActions.fetchRequest());
5658
+ const assetSubscription = client.listen(
5462
5659
  groq`*[_type in ["sanity.fileAsset", "sanity.imageAsset"] && !(_id in path("drafts.**"))]`
5463
- ).subscribe(handleAssetUpdate), subscriptionTag = client.listen(groq`*[_type == "${TAG_DOCUMENT_NAME}" && !(_id in path("drafts.**"))]`).subscribe(handleTagUpdate);
5660
+ ).subscribe(createAssetHandler(dispatch)), tagSubscription = client.listen(groq`*[_type == "${TAG_DOCUMENT_NAME}" && !(_id in path("drafts.**"))]`).subscribe(createTagHandler(dispatch));
5464
5661
  return () => {
5465
- subscriptionAsset?.unsubscribe(), subscriptionTag?.unsubscribe();
5662
+ assetSubscription.unsubscribe(), tagSubscription.unsubscribe();
5466
5663
  };
5467
- }, [client, dispatch]), /* @__PURE__ */ jsx(PortalProvider, { element: portalElement, children: /* @__PURE__ */ jsxs(UploadDropzone, { children: [
5664
+ }, [client, dispatch, hasMediaTags]), useEffect(() => {
5665
+ if (!hasMediaTags || tagsFetchCount < 0) return;
5666
+ const tagFacetInput = inputs.tag;
5667
+ if (tagFacetInput.type !== "searchable") return;
5668
+ const resolvedTags = tagNames.map((name) => Object.values(tagsByIds).find((item) => item.tag.name.current === name)).filter((item) => !!item);
5669
+ dispatch(searchActions.facetsClear());
5670
+ for (const tagItem of resolvedTags)
5671
+ dispatch(
5672
+ searchActions.facetsAdd({
5673
+ facet: {
5674
+ ...tagFacetInput,
5675
+ operatorType: "references",
5676
+ value: { label: tagItem.tag.name.current, value: tagItem.tag._id }
5677
+ }
5678
+ })
5679
+ );
5680
+ }, [tagsFetchCount, hasMediaTags]);
5681
+ }
5682
+ const BrowserContent = ({
5683
+ onClose,
5684
+ schemaType
5685
+ }) => {
5686
+ const client = useVersionedClient(), [portalElement, setPortalElement] = useState(null);
5687
+ return useBrowserInit(client, schemaType), /* @__PURE__ */ jsx(PortalProvider, { element: portalElement, children: /* @__PURE__ */ jsxs(UploadDropzone, { children: [
5468
5688
  /* @__PURE__ */ jsx(Dialogs, {}),
5469
5689
  /* @__PURE__ */ jsx(Notifications, {}),
5470
5690
  /* @__PURE__ */ jsx(Card, { display: "flex", height: "fill", ref: setPortalElement, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", flex: 1, children: [
@@ -5491,7 +5711,7 @@ const UploadDropzone = (props) => {
5491
5711
  selectedAssets: props?.selectedAssets,
5492
5712
  children: /* @__PURE__ */ jsxs(AssetBrowserDispatchProvider, { onSelect: props?.onSelect, children: [
5493
5713
  /* @__PURE__ */ jsx(GlobalStyle, {}),
5494
- /* @__PURE__ */ jsx(BrowserContent, { onClose: props?.onClose })
5714
+ /* @__PURE__ */ jsx(BrowserContent, { onClose: props?.onClose, schemaType: props?.schemaType })
5495
5715
  ] })
5496
5716
  }
5497
5717
  );
@@ -5518,7 +5738,7 @@ const UploadDropzone = (props) => {
5518
5738
  width: "100%",
5519
5739
  zIndex
5520
5740
  },
5521
- children: /* @__PURE__ */ jsx(Browser, { document: currentDocument, ...props })
5741
+ children: /* @__PURE__ */ jsx(Browser, { document: currentDocument, schemaType: props.schemaType, ...props })
5522
5742
  }
5523
5743
  ) }) });
5524
5744
  }, useRootPortalElement = () => {
@@ -5583,9 +5803,78 @@ const plugin = {
5583
5803
  types: [mediaTag]
5584
5804
  },
5585
5805
  tools: (prev) => [...prev, tool]
5586
- }));
5806
+ })), pendingByAsset = /* @__PURE__ */ new Map();
5807
+ function applyMediaTags(options) {
5808
+ const { assetId } = options, chain = (pendingByAsset.get(assetId) ?? Promise.resolve()).then(
5809
+ () => doApplyMediaTags(options)
5810
+ ), cleanup = chain.catch(() => {
5811
+ }).finally(() => {
5812
+ pendingByAsset.get(assetId) === cleanup && pendingByAsset.delete(assetId);
5813
+ });
5814
+ return pendingByAsset.set(assetId, cleanup), chain;
5815
+ }
5816
+ async function doApplyMediaTags({
5817
+ client,
5818
+ assetId,
5819
+ mediaTags,
5820
+ createTagsOnUpload = !0
5821
+ }) {
5822
+ if (!mediaTags || mediaTags.length === 0) return;
5823
+ const validTags = (await Promise.all(
5824
+ mediaTags.map(async (tagName) => await client.fetch(
5825
+ groq`*[_type == "${TAG_DOCUMENT_NAME}" && name.current == $tagName][0]`,
5826
+ { tagName }
5827
+ ) || (createTagsOnUpload ? await client.create({
5828
+ _type: TAG_DOCUMENT_NAME,
5829
+ name: { _type: "slug", current: tagName }
5830
+ }) : null))
5831
+ )).filter((tag) => tag !== null);
5832
+ if (validTags.length === 0) return;
5833
+ const existing = await client.fetch(
5834
+ groq`*[_id == $assetId][0]{'tagIds': opt.media.tags[]._ref}`,
5835
+ { assetId },
5836
+ { useCdn: !1 }
5837
+ // bypass CDN cache so we see the latest committed tag refs
5838
+ ), existingIds = new Set(existing?.tagIds ?? []), tagReferences = validTags.filter((tag) => !existingIds.has(tag._id)).map((tag) => ({
5839
+ _key: nanoid(),
5840
+ _ref: tag._id,
5841
+ _type: "reference",
5842
+ _weak: !0
5843
+ }));
5844
+ tagReferences.length !== 0 && await client.patch(assetId).setIfMissing({ opt: {} }).setIfMissing({ "opt.media": {} }).setIfMissing({ "opt.media.tags": [] }).append("opt.media.tags", tagReferences).commit();
5845
+ }
5846
+ function AutoTagInput(props) {
5847
+ const { renderDefault, schemaType, value, mediaTags: mediaTagsProp } = props, toast = useToast(), mediaTags = mediaTagsProp ?? schemaType?.options?.mediaTags, client = useVersionedClient(), { createTagsOnUpload } = useToolOptions(), prevAssetRef = useRef(void 0), isInitialMount = useRef(!0), currentAssetRef = value?.asset?._ref;
5848
+ return useEffect(() => {
5849
+ if (isInitialMount.current) {
5850
+ isInitialMount.current = !1, prevAssetRef.current = currentAssetRef;
5851
+ return;
5852
+ }
5853
+ const previousRef = prevAssetRef.current;
5854
+ prevAssetRef.current = currentAssetRef, !(!mediaTags?.length || !currentAssetRef || currentAssetRef === previousRef) && applyMediaTags({
5855
+ client,
5856
+ assetId: currentAssetRef,
5857
+ mediaTags,
5858
+ createTagsOnUpload
5859
+ }).catch((err) => {
5860
+ console.error("[sanity-plugin-media] Failed to apply auto-tags:", err);
5861
+ const label = mediaTags.length === 1 ? "tag" : "tags";
5862
+ toast.push({ closable: !0, status: "error", title: `Failed to apply the media ${label} ${mediaTags.join(", ")}` });
5863
+ });
5864
+ }, [currentAssetRef, mediaTags, client, createTagsOnUpload]), renderDefault(props);
5865
+ }
5866
+ function mediaField(config) {
5867
+ const { mediaTags, options, components: components2, ...rest } = config;
5868
+ return {
5869
+ ...rest,
5870
+ options: { ...options, mediaTags },
5871
+ components: { ...components2, input: AutoTagInput }
5872
+ };
5873
+ }
5587
5874
  export {
5875
+ AutoTagInput,
5588
5876
  media,
5589
- mediaAssetSource
5877
+ mediaAssetSource,
5878
+ mediaField
5590
5879
  };
5591
5880
  //# sourceMappingURL=index.mjs.map