sanity-plugin-media 4.2.0 → 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.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var jsxRuntime = require("react/jsx-runtime"), sanity = require("sanity"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), react = require("react"), groq = require("groq"), reactRedux = require("react-redux"), toolkit = require("@reduxjs/toolkit"), nanoid = require("nanoid"), reduxObservable = require("redux-observable"), rxjs = require("rxjs"), operators$1 = require("rxjs/operators"), uuid = require("@sanity/uuid"), styledComponents = require("styled-components"), pluralize = require("pluralize"), reactNprogress = require("@tanem/react-nprogress"), color = require("@sanity/color"), Select = require("react-select"), reactVirtuoso = require("react-virtuoso"), zod = require("@hookform/resolvers/zod"), reactHookForm = require("react-hook-form"), z = require("zod"), dateFns = require("date-fns"), filesize = require("filesize"), copy = require("copy-to-clipboard"), router = require("sanity/router"), reactFileIcon = require("react-file-icon"), CreatableSelect = require("react-select/creatable"), reactDropzone = require("react-dropzone");
3
+ var jsxRuntime = require("react/jsx-runtime"), sanity = require("sanity"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), react = require("react"), styledComponents = require("styled-components"), reactRedux = require("react-redux"), toolkit = require("@reduxjs/toolkit"), pluralize = require("pluralize"), reduxObservable = require("redux-observable"), rxjs = require("rxjs"), operators$1 = require("rxjs/operators"), groq = require("groq"), nanoid = require("nanoid"), uuid = require("@sanity/uuid"), reactNprogress = require("@tanem/react-nprogress"), color = require("@sanity/color"), Select = require("react-select"), reactVirtuoso = require("react-virtuoso"), zod = require("@hookform/resolvers/zod"), reactHookForm = require("react-hook-form"), z = require("zod"), dateFns = require("date-fns"), filesize = require("filesize"), copy = require("copy-to-clipboard"), router = require("sanity/router"), reactFileIcon = require("react-file-icon"), CreatableSelect = require("react-select/creatable"), reactDropzone = require("react-dropzone");
4
4
  function _interopDefaultCompat(e) {
5
5
  return e && typeof e == "object" && "default" in e ? e : { default: e };
6
6
  }
@@ -19,7 +19,7 @@ function _interopNamespaceCompat(e) {
19
19
  }
20
20
  }), n.default = e, Object.freeze(n);
21
21
  }
22
- var groq__default = /* @__PURE__ */ _interopDefaultCompat(groq), pluralize__default = /* @__PURE__ */ _interopDefaultCompat(pluralize), Select__default = /* @__PURE__ */ _interopDefaultCompat(Select), z__namespace = /* @__PURE__ */ _interopNamespaceCompat(z), filesize__default = /* @__PURE__ */ _interopDefaultCompat(filesize), copy__default = /* @__PURE__ */ _interopDefaultCompat(copy), CreatableSelect__default = /* @__PURE__ */ _interopDefaultCompat(CreatableSelect);
22
+ var pluralize__default = /* @__PURE__ */ _interopDefaultCompat(pluralize), groq__default = /* @__PURE__ */ _interopDefaultCompat(groq), Select__default = /* @__PURE__ */ _interopDefaultCompat(Select), z__namespace = /* @__PURE__ */ _interopNamespaceCompat(z), filesize__default = /* @__PURE__ */ _interopDefaultCompat(filesize), copy__default = /* @__PURE__ */ _interopDefaultCompat(copy), CreatableSelect__default = /* @__PURE__ */ _interopDefaultCompat(CreatableSelect);
23
23
  function getDefaultExportFromCjs(x) {
24
24
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x.default : x;
25
25
  }
@@ -169,7 +169,71 @@ const useKeyPress = (hotkey, onPress) => {
169
169
  return react.useEffect(() => (window.addEventListener("keydown", downHandler), window.addEventListener("keyup", upHandler), () => {
170
170
  window.removeEventListener("keydown", downHandler), window.removeEventListener("keyup", upHandler);
171
171
  }), [downHandler, upHandler]), keyPressed;
172
- }, divider = { type: "divider" }, inputs = {
172
+ }, AssetSourceDispatchContext = react.createContext(void 0), AssetBrowserDispatchProvider = (props) => {
173
+ const { children, onSelect } = props, contextValue = {
174
+ onSelect
175
+ };
176
+ return /* @__PURE__ */ jsxRuntime.jsx(AssetSourceDispatchContext.Provider, { value: contextValue, children });
177
+ }, useAssetSourceActions = () => {
178
+ const context = react.useContext(AssetSourceDispatchContext);
179
+ if (context === void 0)
180
+ throw new Error("useAssetSourceActions must be used within an AssetSourceDispatchProvider");
181
+ return context;
182
+ }, useVersionedClient = () => sanity.useClient({ apiVersion: "2025-10-02" }), customScrollbar = styledComponents.css`
183
+ ::-webkit-scrollbar {
184
+ width: 14px;
185
+ }
186
+
187
+ ::-webkit-scrollbar-thumb {
188
+ border-radius: 10px;
189
+ border: 4px solid rgba(0, 0, 0, 0);
190
+ background: var(--card-border-color);
191
+ background-clip: padding-box;
192
+
193
+ &:hover {
194
+ background: var(--card-muted-fg-color);
195
+ background-clip: padding-box;
196
+ }
197
+ }
198
+ `, GlobalStyle = styledComponents.createGlobalStyle`
199
+ .media__custom-scrollbar {
200
+ ${customScrollbar}
201
+ }
202
+
203
+ // @sanity/ui overrides
204
+
205
+ // Custom scrollbar on Box (used in Dialogs)
206
+ div[data-ui="Box"] {
207
+ ${customScrollbar}
208
+ }
209
+
210
+ // Dialog background color
211
+ div[data-ui="Dialog"] {
212
+ background-color: rgba(15, 17, 18, 0.9);
213
+ }
214
+
215
+ `, useTypedSelector = reactRedux.useSelector, ORDER_DICTIONARY = {
216
+ _createdAt: {
217
+ asc: "Last created: Oldest first",
218
+ desc: "Last created: Newest first"
219
+ },
220
+ _updatedAt: {
221
+ asc: "Last updated: Oldest first",
222
+ desc: "Last updated: Newest first"
223
+ },
224
+ mimeType: {
225
+ asc: "MIME type: A to Z",
226
+ desc: "MIME type: Z to A"
227
+ },
228
+ originalFilename: {
229
+ asc: "File name: A to Z",
230
+ desc: "File name: Z to A"
231
+ },
232
+ size: {
233
+ asc: "File size: Smallest first",
234
+ desc: "File size: Largest first"
235
+ }
236
+ }, getOrderTitle = (field, direction) => ORDER_DICTIONARY[field][direction], divider = { type: "divider" }, inputs = {
173
237
  altText: {
174
238
  assetTypes: ["file", "image"],
175
239
  field: "altText",
@@ -532,38 +596,7 @@ const useKeyPress = (hotkey, onPress) => {
532
596
  ], GRID_TEMPLATE_COLUMNS = {
533
597
  SMALL: "3rem 100px auto 1.5rem",
534
598
  LARGE: "3rem 100px auto 5.5rem 5.5rem 3.5rem 8.5rem 4.75rem 2rem"
535
- }, PANEL_HEIGHT = 32, TAG_DOCUMENT_NAME = "media.tag", TAGS_PANEL_WIDTH = 250, AssetSourceDispatchContext = react.createContext(void 0), AssetBrowserDispatchProvider = (props) => {
536
- const { children, onSelect } = props, contextValue = {
537
- onSelect
538
- };
539
- return /* @__PURE__ */ jsxRuntime.jsx(AssetSourceDispatchContext.Provider, { value: contextValue, children });
540
- }, useAssetSourceActions = () => {
541
- const context = react.useContext(AssetSourceDispatchContext);
542
- if (context === void 0)
543
- throw new Error("useAssetSourceActions must be used within an AssetSourceDispatchProvider");
544
- return context;
545
- }, useVersionedClient = () => sanity.useClient({ apiVersion: "2025-10-02" }), ORDER_DICTIONARY = {
546
- _createdAt: {
547
- asc: "Last created: Oldest first",
548
- desc: "Last created: Newest first"
549
- },
550
- _updatedAt: {
551
- asc: "Last updated: Oldest first",
552
- desc: "Last updated: Newest first"
553
- },
554
- mimeType: {
555
- asc: "MIME type: A to Z",
556
- desc: "MIME type: Z to A"
557
- },
558
- originalFilename: {
559
- asc: "File name: A to Z",
560
- desc: "File name: Z to A"
561
- },
562
- size: {
563
- asc: "File size: Smallest first",
564
- desc: "File size: Largest first"
565
- }
566
- }, getOrderTitle = (field, direction) => ORDER_DICTIONARY[field][direction], debugThrottle = (throttled) => function(source) {
599
+ }, PANEL_HEIGHT = 32, TAG_DOCUMENT_NAME = "media.tag", TAGS_PANEL_WIDTH = 250, debugThrottle = (throttled) => function(source) {
567
600
  return rxjs.iif(
568
601
  () => !!throttled,
569
602
  source.pipe(
@@ -1607,40 +1640,7 @@ const UPLOADS_ACTIONS = {
1607
1640
  (assetsPicked) => assetsPicked.length
1608
1641
  ), assetsActions = { ...assetsSlice.actions };
1609
1642
  var assetsReducer = assetsSlice.reducer;
1610
- const customScrollbar = styledComponents.css`
1611
- ::-webkit-scrollbar {
1612
- width: 14px;
1613
- }
1614
-
1615
- ::-webkit-scrollbar-thumb {
1616
- border-radius: 10px;
1617
- border: 4px solid rgba(0, 0, 0, 0);
1618
- background: var(--card-border-color);
1619
- background-clip: padding-box;
1620
-
1621
- &:hover {
1622
- background: var(--card-muted-fg-color);
1623
- background-clip: padding-box;
1624
- }
1625
- }
1626
- `, GlobalStyle = styledComponents.createGlobalStyle`
1627
- .media__custom-scrollbar {
1628
- ${customScrollbar}
1629
- }
1630
-
1631
- // @sanity/ui overrides
1632
-
1633
- // Custom scrollbar on Box (used in Dialogs)
1634
- div[data-ui="Box"] {
1635
- ${customScrollbar}
1636
- }
1637
-
1638
- // Dialog background color
1639
- div[data-ui="Dialog"] {
1640
- background-color: rgba(15, 17, 18, 0.9);
1641
- }
1642
-
1643
- `, useTypedSelector = reactRedux.useSelector, initialState$4 = {
1643
+ const initialState$4 = {
1644
1644
  items: []
1645
1645
  }, dialogSlice = toolkit.createSlice({
1646
1646
  name: "dialog",
@@ -2361,6 +2361,7 @@ const Container$1 = styledComponents.styled(ui.Box)(({ $scheme, theme }) => styl
2361
2361
  components: {
2362
2362
  details: options?.components?.details
2363
2363
  },
2364
+ createTagsOnUpload: options?.createTagsOnUpload ?? !0,
2364
2365
  creditLine: {
2365
2366
  enabled: options?.creditLine?.enabled || !1,
2366
2367
  excludeSources: creditLineExcludeSources
@@ -2371,6 +2372,7 @@ const Container$1 = styledComponents.styled(ui.Box)(({ $scheme, theme }) => styl
2371
2372
  }, [
2372
2373
  options?.creditLine?.enabled,
2373
2374
  options?.components,
2375
+ options?.createTagsOnUpload,
2374
2376
  options?.creditLine?.excludeSources,
2375
2377
  options?.maximumUploadSize,
2376
2378
  options?.directUploads,
@@ -5603,24 +5605,81 @@ const UploadDropzone = (props) => {
5603
5605
  isDragActive && /* @__PURE__ */ jsxRuntime.jsx(DragActiveContainer, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { direction: "column", justify: "center", style: { color: color.white.hex }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 3, style: { color: "inherit" }, children: "Drop files to upload" }) }) }),
5604
5606
  children
5605
5607
  ] }) });
5606
- }, BrowserContent = ({ onClose }) => {
5607
- const client = useVersionedClient(), [portalElement, setPortalElement] = react.useState(null), dispatch = reactRedux.useDispatch();
5608
- return react.useEffect(() => {
5609
- const handleAssetUpdate = (update) => {
5610
- const { documentId, result, transition } = update;
5611
- transition === "appear" && dispatch(assetsActions.listenerCreateQueue({ asset: result })), transition === "disappear" && dispatch(assetsActions.listenerDeleteQueue({ assetId: documentId })), transition === "update" && dispatch(assetsActions.listenerUpdateQueue({ asset: result }));
5612
- }, handleTagUpdate = (update) => {
5613
- const { documentId, result, transition } = update;
5614
- transition === "appear" && dispatch(tagsActions.listenerCreateQueue({ tag: result })), transition === "disappear" && dispatch(tagsActions.listenerDeleteQueue({ tagId: documentId })), transition === "update" && dispatch(tagsActions.listenerUpdateQueue({ tag: result }));
5615
- };
5616
- dispatch(assetsActions.loadPageIndex({ pageIndex: 0 })), dispatch(tagsActions.fetchRequest());
5617
- const subscriptionAsset = client.listen(
5608
+ };
5609
+ function getMediaTagNames(schemaType) {
5610
+ const mediaTags = schemaType?.options?.mediaTags;
5611
+ if (!mediaTags?.length) return [];
5612
+ const unique = new Set(
5613
+ mediaTags.map((t) => t?.trim()).filter((t) => !!t?.length)
5614
+ );
5615
+ return Array.from(unique);
5616
+ }
5617
+ function createAssetHandler(dispatch) {
5618
+ return (update) => {
5619
+ const { documentId, result, transition } = update;
5620
+ switch (transition) {
5621
+ case "appear":
5622
+ dispatch(assetsActions.listenerCreateQueue({ asset: result }));
5623
+ break;
5624
+ case "disappear":
5625
+ dispatch(assetsActions.listenerDeleteQueue({ assetId: documentId }));
5626
+ break;
5627
+ case "update":
5628
+ dispatch(assetsActions.listenerUpdateQueue({ asset: result }));
5629
+ break;
5630
+ }
5631
+ };
5632
+ }
5633
+ function createTagHandler(dispatch) {
5634
+ return (update) => {
5635
+ const { documentId, result, transition } = update;
5636
+ switch (transition) {
5637
+ case "appear":
5638
+ dispatch(tagsActions.listenerCreateQueue({ tag: result }));
5639
+ break;
5640
+ case "disappear":
5641
+ dispatch(tagsActions.listenerDeleteQueue({ tagId: documentId }));
5642
+ break;
5643
+ case "update":
5644
+ dispatch(tagsActions.listenerUpdateQueue({ tag: result }));
5645
+ break;
5646
+ }
5647
+ };
5648
+ }
5649
+ function useBrowserInit(client, schemaType) {
5650
+ const dispatch = reactRedux.useDispatch(), tagsByIds = reactRedux.useSelector((state) => state.tags.byIds), tagsFetchCount = reactRedux.useSelector((state) => state.tags.fetchCount), tagNames = getMediaTagNames(schemaType), hasMediaTags = tagNames.length > 0;
5651
+ react.useEffect(() => {
5652
+ hasMediaTags || dispatch(searchActions.facetsClear()), dispatch(tagsActions.fetchRequest());
5653
+ const assetSubscription = client.listen(
5618
5654
  groq__default.default`*[_type in ["sanity.fileAsset", "sanity.imageAsset"] && !(_id in path("drafts.**"))]`
5619
- ).subscribe(handleAssetUpdate), subscriptionTag = client.listen(groq__default.default`*[_type == "${TAG_DOCUMENT_NAME}" && !(_id in path("drafts.**"))]`).subscribe(handleTagUpdate);
5655
+ ).subscribe(createAssetHandler(dispatch)), tagSubscription = client.listen(groq__default.default`*[_type == "${TAG_DOCUMENT_NAME}" && !(_id in path("drafts.**"))]`).subscribe(createTagHandler(dispatch));
5620
5656
  return () => {
5621
- subscriptionAsset?.unsubscribe(), subscriptionTag?.unsubscribe();
5657
+ assetSubscription.unsubscribe(), tagSubscription.unsubscribe();
5622
5658
  };
5623
- }, [client, dispatch]), /* @__PURE__ */ jsxRuntime.jsx(ui.PortalProvider, { element: portalElement, children: /* @__PURE__ */ jsxRuntime.jsxs(UploadDropzone, { children: [
5659
+ }, [client, dispatch, hasMediaTags]), react.useEffect(() => {
5660
+ if (!hasMediaTags || tagsFetchCount < 0) return;
5661
+ const tagFacetInput = inputs.tag;
5662
+ if (tagFacetInput.type !== "searchable") return;
5663
+ const resolvedTags = tagNames.map((name) => Object.values(tagsByIds).find((item) => item.tag.name.current === name)).filter((item) => !!item);
5664
+ dispatch(searchActions.facetsClear());
5665
+ for (const tagItem of resolvedTags)
5666
+ dispatch(
5667
+ searchActions.facetsAdd({
5668
+ facet: {
5669
+ ...tagFacetInput,
5670
+ operatorType: "references",
5671
+ value: { label: tagItem.tag.name.current, value: tagItem.tag._id }
5672
+ }
5673
+ })
5674
+ );
5675
+ }, [tagsFetchCount, hasMediaTags]);
5676
+ }
5677
+ const BrowserContent = ({
5678
+ onClose,
5679
+ schemaType
5680
+ }) => {
5681
+ const client = useVersionedClient(), [portalElement, setPortalElement] = react.useState(null);
5682
+ return useBrowserInit(client, schemaType), /* @__PURE__ */ jsxRuntime.jsx(ui.PortalProvider, { element: portalElement, children: /* @__PURE__ */ jsxRuntime.jsxs(UploadDropzone, { children: [
5624
5683
  /* @__PURE__ */ jsxRuntime.jsx(Dialogs, {}),
5625
5684
  /* @__PURE__ */ jsxRuntime.jsx(Notifications, {}),
5626
5685
  /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { display: "flex", height: "fill", ref: setPortalElement, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", flex: 1, children: [
@@ -5647,7 +5706,7 @@ const UploadDropzone = (props) => {
5647
5706
  selectedAssets: props?.selectedAssets,
5648
5707
  children: /* @__PURE__ */ jsxRuntime.jsxs(AssetBrowserDispatchProvider, { onSelect: props?.onSelect, children: [
5649
5708
  /* @__PURE__ */ jsxRuntime.jsx(GlobalStyle, {}),
5650
- /* @__PURE__ */ jsxRuntime.jsx(BrowserContent, { onClose: props?.onClose })
5709
+ /* @__PURE__ */ jsxRuntime.jsx(BrowserContent, { onClose: props?.onClose, schemaType: props?.schemaType })
5651
5710
  ] })
5652
5711
  }
5653
5712
  );
@@ -5674,7 +5733,7 @@ const UploadDropzone = (props) => {
5674
5733
  width: "100%",
5675
5734
  zIndex
5676
5735
  },
5677
- children: /* @__PURE__ */ jsxRuntime.jsx(Browser, { document: currentDocument, ...props })
5736
+ children: /* @__PURE__ */ jsxRuntime.jsx(Browser, { document: currentDocument, schemaType: props.schemaType, ...props })
5678
5737
  }
5679
5738
  ) }) });
5680
5739
  }, useRootPortalElement = () => {
@@ -5739,7 +5798,76 @@ const plugin = {
5739
5798
  types: [mediaTag]
5740
5799
  },
5741
5800
  tools: (prev) => [...prev, tool]
5742
- }));
5801
+ })), pendingByAsset = /* @__PURE__ */ new Map();
5802
+ function applyMediaTags(options) {
5803
+ const { assetId } = options, chain = (pendingByAsset.get(assetId) ?? Promise.resolve()).then(
5804
+ () => doApplyMediaTags(options)
5805
+ ), cleanup = chain.catch(() => {
5806
+ }).finally(() => {
5807
+ pendingByAsset.get(assetId) === cleanup && pendingByAsset.delete(assetId);
5808
+ });
5809
+ return pendingByAsset.set(assetId, cleanup), chain;
5810
+ }
5811
+ async function doApplyMediaTags({
5812
+ client,
5813
+ assetId,
5814
+ mediaTags,
5815
+ createTagsOnUpload = !0
5816
+ }) {
5817
+ if (!mediaTags || mediaTags.length === 0) return;
5818
+ const validTags = (await Promise.all(
5819
+ mediaTags.map(async (tagName) => await client.fetch(
5820
+ groq__default.default`*[_type == "${TAG_DOCUMENT_NAME}" && name.current == $tagName][0]`,
5821
+ { tagName }
5822
+ ) || (createTagsOnUpload ? await client.create({
5823
+ _type: TAG_DOCUMENT_NAME,
5824
+ name: { _type: "slug", current: tagName }
5825
+ }) : null))
5826
+ )).filter((tag) => tag !== null);
5827
+ if (validTags.length === 0) return;
5828
+ const existing = await client.fetch(
5829
+ groq__default.default`*[_id == $assetId][0]{'tagIds': opt.media.tags[]._ref}`,
5830
+ { assetId },
5831
+ { useCdn: !1 }
5832
+ // bypass CDN cache so we see the latest committed tag refs
5833
+ ), existingIds = new Set(existing?.tagIds ?? []), tagReferences = validTags.filter((tag) => !existingIds.has(tag._id)).map((tag) => ({
5834
+ _key: nanoid.nanoid(),
5835
+ _ref: tag._id,
5836
+ _type: "reference",
5837
+ _weak: !0
5838
+ }));
5839
+ tagReferences.length !== 0 && await client.patch(assetId).setIfMissing({ opt: {} }).setIfMissing({ "opt.media": {} }).setIfMissing({ "opt.media.tags": [] }).append("opt.media.tags", tagReferences).commit();
5840
+ }
5841
+ function AutoTagInput(props) {
5842
+ const { renderDefault, schemaType, value, mediaTags: mediaTagsProp } = props, toast = ui.useToast(), mediaTags = mediaTagsProp ?? schemaType?.options?.mediaTags, client = useVersionedClient(), { createTagsOnUpload } = useToolOptions(), prevAssetRef = react.useRef(void 0), isInitialMount = react.useRef(!0), currentAssetRef = value?.asset?._ref;
5843
+ return react.useEffect(() => {
5844
+ if (isInitialMount.current) {
5845
+ isInitialMount.current = !1, prevAssetRef.current = currentAssetRef;
5846
+ return;
5847
+ }
5848
+ const previousRef = prevAssetRef.current;
5849
+ prevAssetRef.current = currentAssetRef, !(!mediaTags?.length || !currentAssetRef || currentAssetRef === previousRef) && applyMediaTags({
5850
+ client,
5851
+ assetId: currentAssetRef,
5852
+ mediaTags,
5853
+ createTagsOnUpload
5854
+ }).catch((err) => {
5855
+ console.error("[sanity-plugin-media] Failed to apply auto-tags:", err);
5856
+ const label = mediaTags.length === 1 ? "tag" : "tags";
5857
+ toast.push({ closable: !0, status: "error", title: `Failed to apply the media ${label} ${mediaTags.join(", ")}` });
5858
+ });
5859
+ }, [currentAssetRef, mediaTags, client, createTagsOnUpload]), renderDefault(props);
5860
+ }
5861
+ function mediaField(config) {
5862
+ const { mediaTags, options, components, ...rest } = config;
5863
+ return {
5864
+ ...rest,
5865
+ options: { ...options, mediaTags },
5866
+ components: { ...components, input: AutoTagInput }
5867
+ };
5868
+ }
5869
+ exports.AutoTagInput = AutoTagInput;
5743
5870
  exports.media = media;
5744
5871
  exports.mediaAssetSource = mediaAssetSource;
5872
+ exports.mediaField = mediaField;
5745
5873
  //# sourceMappingURL=index.js.map