sanity-plugin-media 4.2.0 → 4.3.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/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: "2025-10-02" }), 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,6 +2368,7 @@ 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
@@ -2378,6 +2379,7 @@ const Container$1 = styled(Box)(({ $scheme, theme }) => css`
2378
2379
  }, [
2379
2380
  options?.creditLine?.enabled,
2380
2381
  options?.components,
2382
+ options?.createTagsOnUpload,
2381
2383
  options?.creditLine?.excludeSources,
2382
2384
  options?.maximumUploadSize,
2383
2385
  options?.directUploads,
@@ -5608,24 +5610,81 @@ const UploadDropzone = (props) => {
5608
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" }) }) }),
5609
5611
  children
5610
5612
  ] }) });
5611
- }, BrowserContent = ({ onClose }) => {
5612
- const client = useVersionedClient(), [portalElement, setPortalElement] = useState(null), dispatch = useDispatch();
5613
- return useEffect(() => {
5614
- const handleAssetUpdate = (update) => {
5615
- const { documentId, result, transition } = update;
5616
- transition === "appear" && dispatch(assetsActions.listenerCreateQueue({ asset: result })), transition === "disappear" && dispatch(assetsActions.listenerDeleteQueue({ assetId: documentId })), transition === "update" && dispatch(assetsActions.listenerUpdateQueue({ asset: result }));
5617
- }, handleTagUpdate = (update) => {
5618
- const { documentId, result, transition } = update;
5619
- transition === "appear" && dispatch(tagsActions.listenerCreateQueue({ tag: result })), transition === "disappear" && dispatch(tagsActions.listenerDeleteQueue({ tagId: documentId })), transition === "update" && dispatch(tagsActions.listenerUpdateQueue({ tag: result }));
5620
- };
5621
- dispatch(assetsActions.loadPageIndex({ pageIndex: 0 })), dispatch(tagsActions.fetchRequest());
5622
- 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(
5623
5659
  groq`*[_type in ["sanity.fileAsset", "sanity.imageAsset"] && !(_id in path("drafts.**"))]`
5624
- ).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));
5625
5661
  return () => {
5626
- subscriptionAsset?.unsubscribe(), subscriptionTag?.unsubscribe();
5662
+ assetSubscription.unsubscribe(), tagSubscription.unsubscribe();
5627
5663
  };
5628
- }, [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: [
5629
5688
  /* @__PURE__ */ jsx(Dialogs, {}),
5630
5689
  /* @__PURE__ */ jsx(Notifications, {}),
5631
5690
  /* @__PURE__ */ jsx(Card, { display: "flex", height: "fill", ref: setPortalElement, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", flex: 1, children: [
@@ -5652,7 +5711,7 @@ const UploadDropzone = (props) => {
5652
5711
  selectedAssets: props?.selectedAssets,
5653
5712
  children: /* @__PURE__ */ jsxs(AssetBrowserDispatchProvider, { onSelect: props?.onSelect, children: [
5654
5713
  /* @__PURE__ */ jsx(GlobalStyle, {}),
5655
- /* @__PURE__ */ jsx(BrowserContent, { onClose: props?.onClose })
5714
+ /* @__PURE__ */ jsx(BrowserContent, { onClose: props?.onClose, schemaType: props?.schemaType })
5656
5715
  ] })
5657
5716
  }
5658
5717
  );
@@ -5679,7 +5738,7 @@ const UploadDropzone = (props) => {
5679
5738
  width: "100%",
5680
5739
  zIndex
5681
5740
  },
5682
- children: /* @__PURE__ */ jsx(Browser, { document: currentDocument, ...props })
5741
+ children: /* @__PURE__ */ jsx(Browser, { document: currentDocument, schemaType: props.schemaType, ...props })
5683
5742
  }
5684
5743
  ) }) });
5685
5744
  }, useRootPortalElement = () => {
@@ -5744,9 +5803,78 @@ const plugin = {
5744
5803
  types: [mediaTag]
5745
5804
  },
5746
5805
  tools: (prev) => [...prev, tool]
5747
- }));
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
+ }
5748
5874
  export {
5875
+ AutoTagInput,
5749
5876
  media,
5750
- mediaAssetSource
5877
+ mediaAssetSource,
5878
+ mediaField
5751
5879
  };
5752
5880
  //# sourceMappingURL=index.mjs.map