sanity-plugin-media
Version:
This version of `sanity-plugin-media` is for Sanity Studio V3.
1 lines • 427 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../node_modules/is-hotkey/lib/index.js","../src/hooks/useKeyPress.ts","../src/config/searchFacets.ts","../src/constants.ts","../src/contexts/AssetSourceDispatchContext.tsx","../src/hooks/useVersionedClient.ts","../src/config/orders.ts","../src/operators/debugThrottle.ts","../src/utils/constructFilter.ts","../src/operators/checkTagName.ts","../src/utils/getTagSelectOptions.ts","../src/modules/assets/actions.ts","../src/modules/dialog/actions.ts","../src/modules/tags/index.ts","../src/modules/search/index.ts","../src/modules/uploads/actions.ts","../src/modules/assets/index.ts","../src/styled/GlobalStyles/index.tsx","../src/hooks/useTypedSelector.ts","../src/modules/dialog/index.ts","../src/components/ButtonViewGroup/index.tsx","../src/hooks/usePortalPopoverProps.ts","../src/components/OrderSelect/index.tsx","../src/components/Progress/index.tsx","../src/utils/getSchemeColor.ts","../src/components/SearchFacet/index.tsx","../src/components/TextInputNumber/index.tsx","../src/components/SearchFacetNumber/index.tsx","../src/components/SearchFacetSelect/index.tsx","../src/components/SearchFacetString/index.tsx","../src/styled/react-select/single.tsx","../src/components/SearchFacetTags/index.tsx","../src/components/SearchFacets/index.tsx","../src/contexts/ToolOptionsContext.tsx","../src/components/SearchFacetsControl/index.tsx","../src/components/TagIcon/index.tsx","../src/components/TextInputSearch/index.tsx","../src/components/Controls/index.tsx","../src/modules/debug/index.ts","../src/components/DebugControls/index.tsx","../src/formSchema/index.ts","../src/utils/getUniqueDocuments.ts","../src/utils/imageDprUrl.ts","../src/utils/sanitizeFormData.ts","../src/utils/typeGuards.ts","../src/utils/getAssetResolution.ts","../src/components/ButtonAssetCopy/index.tsx","../src/components/AssetMetadata/index.tsx","../src/components/Dialog/index.tsx","../src/components/DocumentList/index.tsx","../src/components/FileIcon/index.tsx","../src/components/FileAssetPreview/index.tsx","../src/styled/react-select/creatable.tsx","../src/components/FormFieldInputLabel/index.tsx","../src/components/FormFieldInputTags/index.tsx","../src/components/FormFieldInputText/index.tsx","../src/components/FormFieldInputTextarea/index.tsx","../src/components/FormSubmitButton/index.tsx","../src/components/Image/index.tsx","../src/components/DialogAssetEdit/index.tsx","../src/components/DialogConfirm/index.tsx","../src/components/DialogSearchFacets/index.tsx","../src/components/DialogTagCreate/index.tsx","../src/components/DialogTagEdit/index.tsx","../src/components/Tag/index.tsx","../src/components/TagsVirtualized/index.tsx","../src/components/TagViewHeader/index.tsx","../src/components/TagView/index.tsx","../src/components/DialogTags/index.tsx","../src/components/Dialogs/index.tsx","../src/contexts/DropzoneDispatchContext.tsx","../src/components/Header/index.tsx","../src/hooks/useBreakpointIndex.ts","../src/modules/selectors.ts","../src/components/CardAsset/index.tsx","../src/utils/generatePreviewBlobUrl.ts","../src/utils/withMaxConcurrency.ts","../src/utils/uploadSanityAsset.ts","../src/modules/uploads/index.ts","../src/components/CardUpload/index.tsx","../src/components/AssetGridVirtualized/index.tsx","../src/components/TableHeaderItem/index.tsx","../src/components/TableHeader/index.tsx","../src/components/TableRowAsset/index.tsx","../src/components/TableRowUpload/index.tsx","../src/components/AssetTableVirtualized/index.tsx","../src/components/Items/index.tsx","../src/components/Notifications/index.tsx","../src/components/PickedBar/index.tsx","../src/modules/selected/index.ts","../src/modules/notifications/index.ts","../src/modules/index.ts","../src/utils/getDocumentAssetIds.ts","../src/components/ReduxProvider/index.tsx","../src/components/TagsPanel/index.tsx","../src/components/UploadDropzone/index.tsx","../src/components/Browser/index.tsx","../src/components/FormBuilderTool/index.tsx","../src/components/Tool/index.tsx","../src/schemas/tag.ts","../src/plugin.tsx"],"sourcesContent":["'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\n/**\n * Constants.\n */\n\nvar IS_MAC = typeof window != 'undefined' && /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);\n\nvar MODIFIERS = {\n alt: 'altKey',\n control: 'ctrlKey',\n meta: 'metaKey',\n shift: 'shiftKey'\n};\n\nvar ALIASES = {\n add: '+',\n break: 'pause',\n cmd: 'meta',\n command: 'meta',\n ctl: 'control',\n ctrl: 'control',\n del: 'delete',\n down: 'arrowdown',\n esc: 'escape',\n ins: 'insert',\n left: 'arrowleft',\n mod: IS_MAC ? 'meta' : 'control',\n opt: 'alt',\n option: 'alt',\n return: 'enter',\n right: 'arrowright',\n space: ' ',\n spacebar: ' ',\n up: 'arrowup',\n win: 'meta',\n windows: 'meta'\n};\n\nvar CODES = {\n backspace: 8,\n tab: 9,\n enter: 13,\n shift: 16,\n control: 17,\n alt: 18,\n pause: 19,\n capslock: 20,\n escape: 27,\n ' ': 32,\n pageup: 33,\n pagedown: 34,\n end: 35,\n home: 36,\n arrowleft: 37,\n arrowup: 38,\n arrowright: 39,\n arrowdown: 40,\n insert: 45,\n delete: 46,\n meta: 91,\n numlock: 144,\n scrolllock: 145,\n ';': 186,\n '=': 187,\n ',': 188,\n '-': 189,\n '.': 190,\n '/': 191,\n '`': 192,\n '[': 219,\n '\\\\': 220,\n ']': 221,\n '\\'': 222\n};\n\nfor (var f = 1; f < 20; f++) {\n CODES['f' + f] = 111 + f;\n}\n\n/**\n * Is hotkey?\n */\n\nfunction isHotkey(hotkey, options, event) {\n if (options && !('byKey' in options)) {\n event = options;\n options = null;\n }\n\n if (!Array.isArray(hotkey)) {\n hotkey = [hotkey];\n }\n\n var array = hotkey.map(function (string) {\n return parseHotkey(string, options);\n });\n var check = function check(e) {\n return array.some(function (object) {\n return compareHotkey(object, e);\n });\n };\n var ret = event == null ? check : check(event);\n return ret;\n}\n\nfunction isCodeHotkey(hotkey, event) {\n return isHotkey(hotkey, event);\n}\n\nfunction isKeyHotkey(hotkey, event) {\n return isHotkey(hotkey, { byKey: true }, event);\n}\n\n/**\n * Parse.\n */\n\nfunction parseHotkey(hotkey, options) {\n var byKey = options && options.byKey;\n var ret = {};\n\n // Special case to handle the `+` key since we use it as a separator.\n hotkey = hotkey.replace('++', '+add');\n var values = hotkey.split('+');\n var length = values.length;\n\n // Ensure that all the modifiers are set to false unless the hotkey has them.\n\n for (var k in MODIFIERS) {\n ret[MODIFIERS[k]] = false;\n }\n\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = values[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var value = _step.value;\n\n var optional = value.endsWith('?') && value.length > 1;\n\n if (optional) {\n value = value.slice(0, -1);\n }\n\n var name = toKeyName(value);\n var modifier = MODIFIERS[name];\n\n if (value.length > 1 && !modifier && !ALIASES[value] && !CODES[name]) {\n throw new TypeError('Unknown modifier: \"' + value + '\"');\n }\n\n if (length === 1 || !modifier) {\n if (byKey) {\n ret.key = name;\n } else {\n ret.which = toKeyCode(value);\n }\n }\n\n if (modifier) {\n ret[modifier] = optional ? null : true;\n }\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n return ret;\n}\n\n/**\n * Compare.\n */\n\nfunction compareHotkey(object, event) {\n for (var key in object) {\n var expected = object[key];\n var actual = void 0;\n\n if (expected == null) {\n continue;\n }\n\n if (key === 'key' && event.key != null) {\n actual = event.key.toLowerCase();\n } else if (key === 'which') {\n actual = expected === 91 && event.which === 93 ? 91 : event.which;\n } else {\n actual = event[key];\n }\n\n if (actual == null && expected === false) {\n continue;\n }\n\n if (actual !== expected) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Utils.\n */\n\nfunction toKeyCode(name) {\n name = toKeyName(name);\n var code = CODES[name] || name.toUpperCase().charCodeAt(0);\n return code;\n}\n\nfunction toKeyName(name) {\n name = name.toLowerCase();\n name = ALIASES[name] || name;\n return name;\n}\n\n/**\n * Export.\n */\n\nexports.default = isHotkey;\nexports.isHotkey = isHotkey;\nexports.isCodeHotkey = isCodeHotkey;\nexports.isKeyHotkey = isKeyHotkey;\nexports.parseHotkey = parseHotkey;\nexports.compareHotkey = compareHotkey;\nexports.toKeyCode = toKeyCode;\nexports.toKeyName = toKeyName;","import isHotkey from 'is-hotkey'\nimport {type RefObject, useCallback, useEffect, useRef} from 'react'\n\nconst useKeyPress = (hotkey: string, onPress?: () => void): RefObject<boolean> => {\n const keyPressed = useRef(false)\n\n // If pressed key is our target key then set to true\n const downHandler = useCallback(\n (e: KeyboardEvent) => {\n if (isHotkey(hotkey, e)) {\n keyPressed.current = true\n if (onPress) {\n onPress()\n }\n }\n },\n [hotkey, onPress]\n )\n\n // If released key is our target key then set to false\n const upHandler = useCallback(() => {\n keyPressed.current = false\n }, [])\n\n // Add event listeners\n useEffect(() => {\n window.addEventListener('keydown', downHandler)\n window.addEventListener('keyup', upHandler)\n // Remove event listeners on cleanup\n return () => {\n window.removeEventListener('keydown', downHandler)\n window.removeEventListener('keyup', upHandler)\n }\n }, [downHandler, upHandler])\n\n return keyPressed\n}\n\nexport default useKeyPress\n","import type {\n SearchFacetDivider,\n SearchFacetInputProps,\n SearchFacetName,\n SearchFacetOperators\n} from '../types'\nimport groq from 'groq'\n\nexport const divider: SearchFacetDivider = {type: 'divider'}\n\nexport const inputs: Record<SearchFacetName, SearchFacetInputProps> = {\n altText: {\n assetTypes: ['file', 'image'],\n field: 'altText',\n name: 'altText',\n operatorType: 'empty',\n operatorTypes: ['empty', 'notEmpty', null, 'includes', 'doesNotInclude'],\n title: 'Alt text',\n type: 'string',\n value: ''\n },\n creditLine: {\n assetTypes: ['file', 'image'],\n field: 'creditLine',\n name: 'creditLine',\n operatorType: 'empty',\n operatorTypes: ['empty', 'notEmpty', null, 'includes', 'doesNotInclude'],\n title: 'Credit',\n type: 'string',\n value: ''\n },\n description: {\n assetTypes: ['file', 'image'],\n field: 'description',\n name: 'description',\n operatorType: 'empty',\n operatorTypes: ['empty', 'notEmpty', null, 'includes', 'doesNotInclude'],\n title: 'Description',\n type: 'string',\n value: ''\n },\n fileName: {\n assetTypes: ['file', 'image'],\n field: 'originalFilename',\n name: 'filename',\n operatorType: 'includes',\n operatorTypes: ['includes', 'doesNotInclude'],\n title: 'File name',\n type: 'string',\n value: ''\n },\n height: {\n assetTypes: ['image'],\n field: 'metadata.dimensions.height',\n name: 'height',\n operatorType: 'greaterThan',\n operatorTypes: [\n 'greaterThan',\n 'greaterThanOrEqualTo',\n 'lessThan',\n 'lessThanOrEqualTo',\n null,\n 'equalTo'\n ],\n title: 'Height',\n type: 'number',\n value: 400\n },\n inCurrentDocument: {\n assetTypes: ['file', 'image'],\n name: 'inCurrentDocument',\n operatorType: 'is',\n options: [\n {\n name: 'true',\n title: 'True',\n value: groq`_id in $documentAssetIds`\n },\n {\n name: 'false',\n title: 'False',\n value: groq`!(_id in $documentAssetIds)`\n }\n ],\n selectOnly: true,\n title: 'In use in current document',\n type: 'select',\n value: 'true'\n },\n inUse: {\n assetTypes: ['file', 'image'],\n name: 'inUse',\n operatorType: 'is',\n options: [\n {\n name: 'true',\n title: 'True',\n value: groq`count(*[references(^._id)]) > 0`\n },\n {\n name: 'false',\n title: 'False',\n value: groq`count(*[references(^._id)]) == 0`\n }\n ],\n title: 'In use',\n type: 'select',\n value: 'true'\n },\n isOpaque: {\n assetTypes: ['image'],\n field: 'metadata.isOpaque',\n name: 'isOpaque',\n operatorType: 'equalTo',\n options: [\n {\n name: 'true',\n title: 'True',\n value: `false`\n },\n {\n name: 'false',\n title: 'False',\n value: `true`\n }\n ],\n title: 'Has transparency',\n type: 'select',\n value: 'true'\n },\n orientation: {\n assetTypes: ['image'],\n name: 'orientation',\n operatorType: 'is',\n operatorTypes: ['is', 'isNot'],\n options: [\n {\n name: 'portrait',\n title: 'Portrait',\n value: 'metadata.dimensions.aspectRatio < 1'\n },\n {\n name: 'landscape',\n title: 'Landscape',\n value: 'metadata.dimensions.aspectRatio > 1'\n },\n {\n name: 'square',\n title: 'Square',\n value: 'metadata.dimensions.aspectRatio == 1'\n }\n ],\n title: 'Orientation',\n type: 'select',\n value: 'portrait'\n },\n size: {\n assetTypes: ['file', 'image'],\n field: 'size',\n modifier: 'kb',\n modifiers: [\n {\n name: 'kb',\n title: 'KB',\n fieldModifier: fieldName => `round(${fieldName} / 1000)`\n },\n {\n name: 'mb',\n title: 'MB',\n fieldModifier: fieldName => `round(${fieldName} / 1000000)`\n }\n ],\n name: 'size',\n operatorType: 'greaterThan',\n operatorTypes: [\n 'greaterThan',\n 'greaterThanOrEqualTo',\n 'lessThan',\n 'lessThanOrEqualTo',\n null,\n 'equalTo'\n ],\n title: 'File size',\n type: 'number',\n value: 0\n },\n tag: {\n assetTypes: ['file', 'image'],\n field: 'opt.media.tags',\n name: 'tag',\n operatorType: 'references',\n operatorTypes: ['references', 'doesNotReference', null, 'empty', 'notEmpty'],\n title: 'Tags',\n type: 'searchable'\n },\n title: {\n assetTypes: ['file', 'image'],\n field: 'title',\n name: 'title',\n operatorType: 'empty',\n operatorTypes: ['empty', 'notEmpty', null, 'includes', 'doesNotInclude'],\n title: 'Title',\n type: 'string',\n value: ''\n },\n type: {\n assetTypes: ['file', 'image'],\n name: 'type',\n operatorType: 'is',\n operatorTypes: ['is', 'isNot'],\n options: [\n {\n name: 'image',\n title: 'Image',\n value: 'mimeType match \"image*\"'\n },\n {\n name: 'video',\n title: 'Video',\n value: 'mimeType match \"video*\"'\n },\n {\n name: 'audio',\n title: 'Audio',\n value: 'mimeType match \"audio*\"'\n },\n {\n name: 'pdf',\n title: 'PDF',\n value: 'mimeType == \"application/pdf\"'\n }\n ],\n title: 'File type',\n type: 'select',\n value: 'image'\n },\n width: {\n assetTypes: ['image'],\n field: 'metadata.dimensions.width',\n name: 'width',\n operatorType: 'greaterThan',\n operatorTypes: [\n 'greaterThan',\n 'greaterThanOrEqualTo',\n 'lessThan',\n 'lessThanOrEqualTo',\n null,\n 'equalTo'\n ],\n title: 'Width',\n type: 'number',\n value: 400\n }\n}\n\nexport const operators: SearchFacetOperators = {\n doesNotInclude: {\n fn: (value, field) => (value ? `!(${field} match '*${value}*')` : undefined),\n label: 'does not include'\n },\n doesNotReference: {\n fn: (value, _field) => (value ? `!references('${value}')` : undefined),\n label: 'does not include'\n },\n empty: {\n fn: (_value, field) => `!defined(${field})`,\n hideInput: true,\n label: 'is empty'\n },\n equalTo: {\n fn: (value, field) => (value ? `${field} == ${value}` : undefined),\n label: 'is equal to'\n },\n greaterThan: {\n fn: (value, field) => (value ? `${field} > ${value}` : undefined),\n label: 'is greater than'\n },\n greaterThanOrEqualTo: {\n fn: (value, field) => (value ? `${field} >= ${value}` : undefined),\n label: 'is greater than or equal to'\n },\n includes: {\n fn: (value, field) => (value ? `${field} match '*${value}*'` : undefined),\n label: 'includes'\n },\n is: {\n fn: (value, _field) => `${value}`,\n label: 'is'\n },\n isNot: {\n fn: (value, _field) => `!(${value})`,\n label: 'is not'\n },\n lessThan: {\n fn: (value, field) => (value ? `${field} < ${value}` : undefined),\n label: 'is less than'\n },\n lessThanOrEqualTo: {\n fn: (value, field) => (value ? `${field} <= ${value}` : undefined),\n label: 'is less than or equal to'\n },\n notEmpty: {\n fn: (_value, field) => `defined(${field})`,\n hideInput: true,\n label: 'is not empty'\n },\n references: {\n fn: (value, _field) => (value ? `references('${value}')` : undefined),\n label: 'includes'\n }\n}\n","import type {\n SearchFacetInputProps,\n SearchFacetDivider,\n SearchFacetGroup,\n OrderDirection\n} from './types'\nimport {divider, inputs} from './config/searchFacets'\n\n// Sort order dropdown options\n// null values are represented as menu dividers\nexport const ORDER_OPTIONS: ({direction: OrderDirection; field: string} | null)[] = [\n {\n direction: 'desc',\n field: '_createdAt'\n },\n {\n direction: 'asc',\n field: '_createdAt'\n },\n // Divider\n null,\n {\n direction: 'desc',\n field: '_updatedAt'\n },\n {\n direction: 'asc' as OrderDirection,\n field: '_updatedAt'\n },\n // Divider\n null,\n {\n direction: 'asc',\n field: 'originalFilename'\n },\n {\n direction: 'desc',\n field: 'originalFilename'\n },\n // Divider\n null,\n {\n direction: 'desc',\n field: 'size'\n },\n {\n direction: 'asc',\n field: 'size'\n }\n]\n\nexport const FACETS: (SearchFacetDivider | SearchFacetGroup | SearchFacetInputProps)[] = [\n inputs.tag,\n divider,\n inputs.inUse,\n inputs.inCurrentDocument,\n divider,\n inputs.title,\n inputs.altText,\n inputs.creditLine,\n inputs.description,\n divider,\n inputs.isOpaque,\n divider,\n inputs.fileName,\n inputs.size,\n inputs.type,\n divider,\n inputs.orientation,\n inputs.width,\n inputs.height\n]\n\nexport const GRID_TEMPLATE_COLUMNS = {\n SMALL: '3rem 100px auto 1.5rem',\n LARGE: '3rem 100px auto 5.5rem 5.5rem 3.5rem 8.5rem 4.75rem 2rem'\n}\nexport const PANEL_HEIGHT = 32 // px\nexport const TAG_DOCUMENT_NAME = 'media.tag'\nexport const TAGS_PANEL_WIDTH = 250 // px\n","import {type ReactNode, createContext, useContext} from 'react'\nimport type {AssetSourceComponentProps} from 'sanity'\n\ntype ContextProps = {\n onSelect?: AssetSourceComponentProps['onSelect']\n}\n\ntype Props = {\n children: ReactNode\n onSelect?: AssetSourceComponentProps['onSelect']\n}\n\nconst AssetSourceDispatchContext = createContext<ContextProps | undefined>(undefined)\n\nexport const AssetBrowserDispatchProvider = (props: Props) => {\n const {children, onSelect} = props\n\n const contextValue: ContextProps = {\n onSelect\n }\n\n return (\n <AssetSourceDispatchContext.Provider value={contextValue}>\n {children}\n </AssetSourceDispatchContext.Provider>\n )\n}\n\nexport const useAssetSourceActions = () => {\n const context = useContext(AssetSourceDispatchContext)\n if (context === undefined) {\n throw new Error('useAssetSourceActions must be used within an AssetSourceDispatchProvider')\n }\n return context\n}\n\nexport default AssetSourceDispatchContext\n","import type {SanityClient} from '@sanity/client'\nimport {useClient} from 'sanity'\n\nconst useVersionedClient = (): SanityClient => useClient({apiVersion: '2022-10-01'})\n\nexport default useVersionedClient\n","import type {OrderDirection} from '../types'\n\nconst ORDER_DICTIONARY: Record<string, {asc: string; desc: string}> = {\n _createdAt: {\n asc: 'Last created: Oldest first',\n desc: 'Last created: Newest first'\n },\n _updatedAt: {\n asc: 'Last updated: Oldest first',\n desc: 'Last updated: Newest first'\n },\n mimeType: {\n asc: 'MIME type: A to Z',\n desc: 'MIME type: Z to A'\n },\n originalFilename: {\n asc: 'File name: A to Z',\n desc: 'File name: Z to A'\n },\n size: {\n asc: 'File size: Smallest first',\n desc: 'File size: Largest first'\n }\n}\n\nexport const getOrderTitle = (field: string, direction: OrderDirection): string => {\n return ORDER_DICTIONARY[field][direction]\n}\n","import {iif, type Observable, of, throwError} from 'rxjs'\nimport {delay, mergeMap} from 'rxjs/operators'\n\nconst debugThrottle = (throttled?: boolean) => {\n return function <T>(source: Observable<T>): Observable<T> {\n return iif(\n () => !!throttled,\n source.pipe(\n delay(3000),\n mergeMap(v => {\n if (Math.random() > 0.5) {\n return throwError({\n message: 'Test error',\n statusCode: 500\n })\n }\n return of(v)\n })\n ),\n source\n )\n }\n}\n\nexport default debugThrottle\n","import type {AssetType, SearchFacetInputProps} from '../types'\nimport groq from 'groq'\n\nimport {operators} from '../config/searchFacets'\n\nconst constructFilter = ({\n assetTypes,\n searchFacets,\n searchQuery\n}: {\n assetTypes: AssetType[]\n searchFacets: SearchFacetInputProps[]\n searchQuery?: string\n}): string => {\n // Fetch asset types depending on current context.\n // Either limit to a specific type (if being used as a custom asset source) or fetch both files and images (if being used as a tool)\n // Sanity will crash if you try and insert incompatible asset types into fields!\n const documentAssetTypes = assetTypes.map(type => `sanity.${type}Asset`)\n\n const baseFilter = groq`\n _type in ${JSON.stringify(documentAssetTypes)} && !(_id in path(\"drafts.**\"))\n `\n\n const searchFacetFragments = searchFacets.reduce((acc: string[], facet) => {\n if (facet.type === 'number') {\n const {field, modifier, modifiers, operatorType, value} = facet\n const operator = operators[operatorType]\n\n // Get current modifier\n const currentModifier = modifiers?.find(m => m.name === modifier)\n\n // Apply field modifier function (if present)\n const facetField = currentModifier?.fieldModifier\n ? currentModifier.fieldModifier(field)\n : field\n\n const fragment = operator.fn(value, facetField)\n if (fragment) {\n acc.push(fragment)\n }\n }\n\n if (facet.type === 'searchable') {\n const {field, operatorType, value} = facet\n const operator = operators[operatorType]\n\n const fragment = operator.fn(value?.value, field)\n if (fragment) {\n acc.push(fragment)\n }\n }\n\n if (facet.type === 'select') {\n const {field, operatorType, options, value} = facet\n const operator = operators[operatorType]\n\n const currentOptionValue = options?.find(l => l.name === value)?.value\n\n const fragment = operator.fn(currentOptionValue, field)\n if (fragment) {\n acc.push(fragment)\n }\n }\n\n if (facet.type === 'string') {\n const {field, operatorType, value} = facet\n const operator = operators[operatorType]\n\n const fragment = operator.fn(value, field)\n if (fragment) {\n acc.push(fragment)\n }\n }\n\n return acc\n }, [])\n\n // Join separate filter fragments\n const constructedQuery = [\n // Base filter\n baseFilter,\n // Search query (if present)\n // NOTE: Currently this only searches direct fields on sanity.fileAsset/sanity.imageAsset and NOT referenced tags\n // It's possible to add this by adding the following line to the searchQuery, but it's quite slow\n // references(*[_type == \"media.tag\" && name.current == \"${searchQuery.trim()}\"]._id)\n ...(searchQuery\n ? [\n groq`[_id, altText, assetId, creditLine, description, originalFilename, title, url] match '*${searchQuery.trim()}*'`\n ]\n : []),\n // Search facets\n ...searchFacetFragments\n ].join(' && ')\n\n return constructedQuery\n}\n\nexport default constructFilter\n","import type {SanityClient} from '@sanity/client'\nimport type {HttpError} from '../types'\nimport groq from 'groq'\nimport {from, Observable, of, throwError} from 'rxjs'\nimport {mergeMap} from 'rxjs/operators'\nimport {TAG_DOCUMENT_NAME} from '../constants'\n\nconst checkTagName = (client: SanityClient, name: string) => {\n return function <T>(source: Observable<T>): Observable<boolean> {\n return source.pipe(\n mergeMap(() => {\n return from(\n client.fetch(groq`count(*[_type == \"${TAG_DOCUMENT_NAME}\" && name.current == $name])`, {\n name\n })\n ) as Observable<number>\n }),\n mergeMap((existingTagCount: number) => {\n if (existingTagCount > 0) {\n return throwError({\n message: 'Tag already exists',\n statusCode: 409\n } as HttpError)\n }\n\n return of(true)\n })\n )\n }\n}\n\nexport default checkTagName\n","import type {TagSelectOption, TagItem} from '../types'\n\nconst getTagSelectOptions = (tags: TagItem[]): TagSelectOption[] => {\n return tags.reduce((acc: TagSelectOption[], val) => {\n const tag = val?.tag\n if (tag) {\n acc.push({\n label: tag?.name?.current,\n value: tag?._id\n })\n }\n return acc\n }, [])\n}\n\nexport default getTagSelectOptions\n","import {createAction} from '@reduxjs/toolkit'\nimport type {AssetItem, HttpError, Tag} from '../../types'\n\nexport const ASSETS_ACTIONS = {\n tagsAddComplete: createAction(\n 'actions/tagsAddComplete',\n function prepare({assets, tag}: {assets: AssetItem[]; tag: Tag}) {\n return {payload: {assets, tag}}\n }\n ),\n tagsAddError: createAction(\n 'actions/tagsAddError',\n function prepare({assets, error, tag}: {assets: AssetItem[]; error: HttpError; tag: Tag}) {\n return {payload: {assets, error, tag}}\n }\n ),\n tagsAddRequest: createAction(\n 'actions/tagsAddRequest',\n function prepare({assets, tag}: {assets: AssetItem[]; tag: Tag}) {\n return {payload: {assets, tag}}\n }\n ),\n tagsRemoveComplete: createAction(\n 'actions/tagsRemoveComplete',\n function prepare({assets, tag}: {assets: AssetItem[]; tag: Tag}) {\n return {payload: {assets, tag}}\n }\n ),\n tagsRemoveError: createAction(\n 'actions/tagsRemoveError',\n function prepare({assets, error, tag}: {assets: AssetItem[]; error: HttpError; tag: Tag}) {\n return {payload: {assets, error, tag}}\n }\n ),\n tagsRemoveRequest: createAction(\n 'actions/tagsRemoveRequest',\n function prepare({assets, tag}: {assets: AssetItem[]; tag: Tag}) {\n return {payload: {assets, tag}}\n }\n )\n}\n","import {createAction} from '@reduxjs/toolkit'\n\nexport const DIALOG_ACTIONS = {\n showTagCreate: createAction('dialog/showTagCreate'),\n showTagEdit: createAction('dialog/showTagEdit', function prepare({tagId}: {tagId: string}) {\n return {\n payload: {tagId}\n }\n })\n}\n","import {createSelector, createSlice, isAnyOf, type PayloadAction} from '@reduxjs/toolkit'\nimport type {ClientError, Transaction} from '@sanity/client'\nimport type {Asset, HttpError, MyEpic, TagSelectOption, Tag, TagItem} from '../../types'\nimport groq from 'groq'\nimport type {Selector} from 'react-redux'\nimport {ofType} from 'redux-observable'\nimport {from, Observable, of} from 'rxjs'\nimport {bufferTime, catchError, filter, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators'\nimport {TAG_DOCUMENT_NAME} from '../../constants'\nimport checkTagName from '../../operators/checkTagName'\nimport debugThrottle from '../../operators/debugThrottle'\nimport getTagSelectOptions from '../../utils/getTagSelectOptions'\nimport {ASSETS_ACTIONS} from '../assets/actions'\nimport {DIALOG_ACTIONS} from '../dialog/actions'\nimport type {RootReducerState} from '../types'\n\ntype TagsReducerState = {\n allIds: string[]\n byIds: Record<string, TagItem>\n creating: boolean\n creatingError?: HttpError\n fetchCount: number\n fetching: boolean\n fetchingError?: HttpError\n // totalCount: number\n panelVisible: boolean\n}\n\nconst initialState = {\n allIds: [],\n byIds: {},\n creating: false,\n creatingError: undefined,\n fetchCount: -1,\n fetching: false,\n fetchingError: undefined,\n panelVisible: true\n} as TagsReducerState\n\nconst tagsSlice = createSlice({\n name: 'tags',\n initialState,\n extraReducers: builder => {\n builder\n .addCase(DIALOG_ACTIONS.showTagCreate, state => {\n delete state.creatingError\n })\n .addCase(DIALOG_ACTIONS.showTagEdit, (state, action) => {\n const {tagId} = action.payload\n delete state.byIds[tagId].error\n })\n .addMatcher(\n isAnyOf(\n ASSETS_ACTIONS.tagsAddComplete,\n ASSETS_ACTIONS.tagsAddError,\n ASSETS_ACTIONS.tagsRemoveComplete,\n ASSETS_ACTIONS.tagsRemoveError\n ),\n (state, action) => {\n const {tag} = action.payload\n state.byIds[tag._id].updating = false\n }\n )\n .addMatcher(\n isAnyOf(ASSETS_ACTIONS.tagsAddRequest, ASSETS_ACTIONS.tagsRemoveRequest),\n (state, action) => {\n const {tag} = action.payload\n state.byIds[tag._id].updating = true\n }\n )\n },\n reducers: {\n createComplete(state, action: PayloadAction<{assetId?: string; tag: Tag}>) {\n const {tag} = action.payload\n state.creating = false\n if (!state.allIds.includes(tag._id)) {\n state.allIds.push(tag._id)\n }\n state.byIds[tag._id] = {\n _type: 'tag',\n picked: false,\n tag,\n updating: false\n }\n },\n createError(state, action: PayloadAction<{error: HttpError; name: string}>) {\n state.creating = false\n state.creatingError = action.payload.error\n },\n createRequest(state, _action: PayloadAction<{assetId?: string; name: string}>) {\n state.creating = true\n delete state.creatingError\n },\n deleteComplete(state, action: PayloadAction<{tagId: string}>) {\n const {tagId} = action.payload\n const deleteIndex = state.allIds.indexOf(tagId)\n if (deleteIndex >= 0) {\n state.allIds.splice(deleteIndex, 1)\n }\n delete state.byIds[tagId]\n },\n deleteError(state, action: PayloadAction<{error: HttpError; tag: Tag}>) {\n const {error, tag} = action.payload\n\n const tagId = tag?._id\n state.byIds[tagId].error = error\n state.byIds[tagId].updating = false\n },\n deleteRequest(state, action: PayloadAction<{tag: Tag}>) {\n const tagId = action.payload?.tag?._id\n state.byIds[tagId].picked = false\n state.byIds[tagId].updating = true\n\n Object.keys(state.byIds).forEach(key => {\n delete state.byIds[key].error\n })\n },\n fetchComplete(state, action: PayloadAction<{tags: Tag[]}>) {\n const {tags} = action.payload\n\n tags?.forEach(tag => {\n state.allIds.push(tag._id)\n state.byIds[tag._id] = {\n _type: 'tag',\n picked: false,\n tag,\n updating: false\n }\n })\n\n state.fetching = false\n state.fetchCount = tags.length || 0\n delete state.fetchingError\n },\n fetchError(state, action: PayloadAction<{error: HttpError}>) {\n const {error} = action.payload\n state.fetching = false\n state.fetchingError = error\n },\n fetchRequest: {\n reducer: (state, _action: PayloadAction<{query: string}>) => {\n state.fetching = true\n delete state.fetchingError\n },\n prepare: () => {\n // Construct query\n const query = groq`\n {\n \"items\": *[\n _type == \"${TAG_DOCUMENT_NAME}\"\n && !(_id in path(\"drafts.**\"))\n ] {\n _createdAt,\n _updatedAt,\n _id,\n _rev,\n _type,\n name\n } | order(name.current asc),\n }\n `\n return {payload: {query}}\n }\n },\n // Queue batch tag creation\n listenerCreateQueue(_state, _action: PayloadAction<{tag: Tag}>) {\n //\n },\n // Apply created tags (via sanity real-time events)\n listenerCreateQueueComplete(state, action: PayloadAction<{tags: Tag[]}>) {\n const {tags} = action.payload\n\n tags?.forEach(tag => {\n state.byIds[tag._id] = {\n _type: 'tag',\n picked: false,\n tag,\n updating: false\n }\n if (!state.allIds.includes(tag._id)) {\n state.allIds.push(tag._id)\n }\n })\n },\n // Queue batch tag deletion\n listenerDeleteQueue(_state, _action: PayloadAction<{tagId: string}>) {\n //\n },\n // Apply deleted tags (via sanity real-time events)\n listenerDeleteQueueComplete(state, action: PayloadAction<{tagIds: string[]}>) {\n const {tagIds} = action.payload\n\n tagIds?.forEach(tagId => {\n const deleteIndex = state.allIds.indexOf(tagId)\n if (deleteIndex >= 0) {\n state.allIds.splice(deleteIndex, 1)\n }\n delete state.byIds[tagId]\n })\n },\n // Queue batch tag updates\n listenerUpdateQueue(_state, _action: PayloadAction<{tag: Tag}>) {\n //\n },\n // Apply updated tags (via sanity real-time events)\n listenerUpdateQueueComplete(state, action: PayloadAction<{tags: Tag[]}>) {\n const {tags} = action.payload\n\n tags?.forEach(tag => {\n if (state.byIds[tag._id]) {\n state.byIds[tag._id].tag = tag\n }\n })\n },\n // Set tag panel visibility\n panelVisibleSet(state, action: PayloadAction<{panelVisible: boolean}>) {\n const {panelVisible} = action.payload\n state.panelVisible = panelVisible\n },\n // Sort all tags by name\n sort(state) {\n state.allIds.sort((a, b) => {\n const tagA = state.byIds[a].tag.name.current\n const tagB = state.byIds[b].tag.name.current\n\n if (tagA < tagB) {\n return -1\n } else if (tagA > tagB) {\n return 1\n }\n return 0\n })\n },\n updateComplete(state, action: PayloadAction<{closeDialogId?: string; tag: Tag}>) {\n const {tag} = action.payload\n state.byIds[tag._id].tag = tag\n state.byIds[tag._id].updating = false\n },\n updateError(state, action: PayloadAction<{tag: Tag; error: HttpError}>) {\n const {error, tag} = action.payload\n const tagId = tag?._id\n state.byIds[tagId].error = error\n state.byIds[tagId].updating = false\n },\n updateRequest(\n state,\n action: PayloadAction<{\n closeDialogId?: string\n formData: Record<string, any>\n tag: Tag\n }>\n ) {\n const {tag} = action.payload\n state.byIds[tag?._id].updating = true\n }\n }\n})\n\n// Epics\n\n// On tag create request:\n// - async check to see if tag already exists\n// - throw if tag already exists\n// - otherwise, create new tag\nexport const tagsCreateEpic: MyEpic = (action$, state$, {client}) =>\n action$.pipe(\n filter(tagsSlice.actions.createRequest.match),\n withLatestFrom(state$),\n mergeMap(([action, state]) => {\n const {assetId, name} = action.payload\n\n return of(action).pipe(\n debugThrottle(state.debug.badConnection),\n checkTagName(client, name),\n mergeMap(() =>\n client.observable.create({\n _type: TAG_DOCUMENT_NAME,\n name: {\n _type: 'slug',\n current: name\n }\n })\n ),\n mergeMap(result => of(tagsSlice.actions.createComplete({assetId, tag: result as Tag}))),\n catchError((error: ClientError) =>\n of(\n tagsSlice.actions.createError({\n error: {\n message: error?.message || 'Internal error',\n statusCode: error?.statusCode || 500\n },\n name\n })\n )\n )\n )\n })\n )\n\n// On tag delete request\n// - find referenced assets\n// - remove tag from referenced assets in a sanity transaction\nexport const tagsDeleteEpic: MyEpic = (action$, state$, {client}) =>\n action$.pipe(\n filter(tagsSlice.actions.deleteRequest.match),\n withLatestFrom(state$),\n mergeMap(([action, state]) => {\n const {tag} = action.payload\n return of(action).pipe(\n // Optionally throttle\n debugThrottle(state.debug.badConnection),\n // Fetch assets which reference this tag\n mergeMap(() =>\n client.observable.fetch<Asset[]>(\n groq`*[\n _type in [\"sanity.fileAsset\", \"sanity.imageAsset\"]\n && references(*[_type == \"media.tag\" && name.current == $tagName]._id)\n ] {\n _id,\n _rev,\n opt\n }`,\n {tagName: tag.name.current}\n )\n ),\n // Create transaction which remove tag references from all matched assets and delete tag\n mergeMap(assets => {\n const patches = assets.map(asset => ({\n id: asset._id,\n patch: {\n // this will cause the transaction to fail if the document has been modified since it was fetched.\n ifRevisionID: asset._rev,\n unset: [`opt.media.tags[_ref == \"${tag._id}\"]`]\n }\n }))\n\n const transaction: Transaction = patches.reduce(\n (tx, patch) => tx.patch(patch.id, patch.patch),\n client.transaction()\n )\n\n transaction.delete(tag._id)\n\n return from(transaction.commit())\n }),\n // Dispatch complete action\n mergeMap(() => of(tagsSlice.actions.deleteComplete({tagId: tag._id}))),\n catchError((error: ClientError) =>\n of(\n tagsSlice.actions.deleteError({\n error: {\n message: error?.message || 'Internal error',\n statusCode: error?.statusCode || 500\n },\n tag\n })\n )\n )\n )\n })\n )\n\n// Async fetch tags\nexport const tagsFetchEpic: MyEpic = (action$, state$, {client}) =>\n action$.pipe(\n filter(tagsSlice.actions.fetchRequest.match),\n withLatestFrom(state$),\n switchMap(([action, state]) => {\n const {query} = action.payload\n\n return of(action).pipe(\n // Optionally throttle\n debugThrottle(state.debug.badConnection),\n // Fetch tags\n mergeMap(() =>\n client.observable.fetch<{\n items: Tag[]\n }>(query)\n ),\n // Dispatch complete action\n mergeMap(result => {\n const {items} = result\n return of(tagsSlice.actions.fetchComplete({tags: items}))\n }),\n catchError((error: ClientError) =>\n of(\n tagsSlice.actions.fetchError({\n error: {\n message: error?.message || 'Internal error',\n statusCode: error?.statusCode || 500\n }\n })\n )\n )\n )\n })\n )\n\n// TODO: merge all buffer epics\n// Buffer tag creation via sanity subscriber\nexport const tagsListenerCreateQueueEpic: MyEpic = action$ =>\n action$.pipe(\n filter(tagsSlice.actions.listenerCreateQueue.match),\n bufferTime(2000),\n filter(actions => actions.length > 0),\n mergeMap(actions => {\n const tags = actions?.map(action => action.payload.tag)\n return of(tagsSlice.actions.listenerCreateQueueComplete({tags}))\n })\n )\n\n// TODO: merge all buffer epics\n// Buffer tag deletion via sanity subscriber\nexport const tagsListenerDeleteQueueEpic: MyEpic = action$ =>\n action$.pipe(\n filter(tagsSlice.actions.listenerDeleteQueue.match),\n bufferTime(2000),\n filter(actions => actions.length > 0),\n mergeMap(actions => {\n const tagIds = actions?.map(action => action.payload.tagId)\n return of(tagsSlice.actions.listenerDeleteQueueComplete({tagIds}))\n })\n )\n\n// TODO: merge all buffer epics\n// Buffer tag update via sanity subscriber\nexport const tagsListenerUpdateQueueEpic: MyEpic = action$ =>\n action$.pipe(\n filter(tagsSlice.actions.listenerUpdateQueue.match),\n bufferTime(2000),\n filter(actions => actions.length > 0),\n mergeMap(actions => {\n const tags = actions?.map(action => action.payload.tag)\n return of(tagsSlice.actions.listenerUpdateQueueComplete({tags}))\n })\n )\n\n// On successful tag creation or updates:\n// - Re-sort all tags\nexport const tagsSortEpic: MyEpic = action$ =>\n action$.pipe(\n ofType(\n tagsSlice.actions.listenerCreateQueueComplete.type,\n tagsSlice.actions.listenerUpdateQueueComplete.type\n ),\n bufferTime(1000),\n filter(actions => actions.length > 0),\n mergeMap(() => of(tagsSlice.actions.sort()))\n )\n\n// On tag update request\n// - check if tag name already exists\n// - throw if tag already exists\n// - otherwise, patch document\nexport const tagsUpdateEpic: MyEpic = (action$, state$, {client}) =>\n action$.pipe(\n filter(tagsSlice.actions.updateRequest.match),\n withLatestFrom(state$),\n mergeMap(([action, state]) => {\n const {closeDialogId, formData, tag} = action.payload\n\n return of(action).pipe(\n // Optionally throttle\n debugThrottle(state.debug.badConnection),\n // Check if tag name is available, throw early if not\n checkTagName(client, formData?.name?.current),\n // Patch document (Update tag)\n mergeMap(\n () =>\n from(\n client\n .patch(tag._id)\n .set({name: {_type: 'slug', current: formData?.name.current}})\n .commit()\n ) as Observable<Tag>\n ),\n // Dispatch complete action\n mergeMap((updatedTag: Tag) => {\n return of(\n tagsSlice.actions.updateComplete({\n closeDialogId,\n tag: updatedTag\n })\n )\n }),\n catchError((error: ClientError) =>\n of(\n tagsSlice.actions.updateError({\n error: {\n message: error?.message || 'Internal error',\n statusCode: error?.statusCode || 500\n },\n tag\n })\n )\n )\n )\n })\n )\n\n// Selectors\n\nconst selectTagsByIds = (state: RootReducerState) => state.tags.byIds\n\nconst selectTagsAllIds = (state: RootReducerState) => state.tags.allIds\n\nexport const selectTags: Selector<RootReducerState, TagItem[]> = createSelector(\n [selectTagsByIds, selectTagsAllIds],\n (byIds, allIds) => allIds.map(id => byIds[id])\n)\n\nexport const selectTagById = createSelector(\n [selectTagsByIds, (_state: RootReducerState, tagId: string) => tagId],\n (byIds, tagId) => byIds[tagId]\n)\n\n// TODO: use createSelector\n// Map tag references to react-select options, skipping over items with no linked tags\nexport const selectTagSelectOptions =\n (asset?: Asset) =>\n (state: RootReducerState): TagSelectOption[] | null => {\n const tags = asset?.opt?.media?.tags?.reduce((acc: TagItem[], v) => {\n const tagItem = state.tags.byIds[v._ref]\n if (tagItem?.tag) {\n acc.push(tagItem)\n }\n return acc\n }, [])\n\n if (tags && tags?.length > 0) {\n return getTagSelectOptions(tags)\n }\n\n return null\n }\n\nexport const tagsActions = {...tagsSlice.actions}\n\nexport default tagsSlice.reducer\n","import {type PayloadAction, createSelector, createSlice} from '@reduxjs/toolkit'\nimport type {MyEpic, SearchFacetInputProps, SearchFacetOperatorType, WithId} from '../../types'\nimport {EMPTY, of} from 'rxjs'\nimport {filter, mergeMap, withLatestFrom} from 'rxjs/operators'\nimport {uuid} from '@sanity/uuid'\n\nimport {tagsActions} from '../tags'\nimport type {RootReducerState} from '../types'\n\n// TODO: don't store non-serializable data in the search store\n// (The main offender is `fieldModifier` which is currently a function)\n\ntype SearchState = {\n facets: WithId<SearchFacetInputProps>[]\n query: string\n}\n\nconst initialState = {\n facets: [],\n query: ''\n} as SearchState\n\nconst searchSlice = createSlice({\n name: 'search',\n initialState,\n reducers: {\n // Add search facet\n facetsAdd(state, action: PayloadAction<{facet: SearchFacetInputProps}>) {\n state.facets.push({...action.payload.facet, id: uuid()})\n },\n // Clear all search facets\n facetsClear(state) {\n state.facets = []\n },\n // Remove search facet by name\n facetsRemoveByName(state, action: PayloadAction<{facetName: string}>) {\n state.facets = state.facets.filter(facet => facet.name !== action.payload.facetName)\n },\n // Remove search facet by name\n facetsRemoveByTag(state, action: PayloadAction<{tagId: string}>) {\n state.facets = state.facets.filter(\n facet =>\n !(\n facet.name === 'tag' &&\n facet.type === 'searchable' &&\n (facet.operatorType === 'references' || facet.operatorType === 'doesNotReference') &&\n facet.value?.value === action.payload.tagId\n )\n )\n },\n // Remove search facet by name\n facetsRemoveById(state, action: PayloadAction<{facetId: string}>) {\n state.facets = state.facets.filter(facet => facet.id !== action.payload.facetId)\n },\n // Update an existing search facet\n facetsUpdate(\n state,\n action: PayloadAction<{\n modifier?: string\n name: string\n operatorType?: SearchFacetOperatorType\n value?: any // TODO: type correctly\n }>\n ) {\n const {modifier, name, operatorType, value} = action.payload\n\n const facet = state.facets.find(f => f.name === name)\n\n if (!facet) {\n return\n }\n\n if (facet.type === 'number' && modifier) {\n facet.modifier = modifier\n }\n if (operatorType) {\n facet.operatorType = operatorType\n }\n if (typeof value !== 'undefined') {\n facet.value = value\n }\n\n state.facets = state.facets.filter(f => f.name !== facet.name || f.id === facet.id)\n },\n // Update an existing search facet\n facetsUpdateById(\n state,\n action: PayloadAction<{\n modifier?: string\n id: string\n operatorType?: SearchFacetOperatorType\n value?: any // TODO: type correctly\n }>\n ) {\n const {modifier, id, operatorType, value} = action.payload\n\n state.facets.forEach((facet, index) => {\n if (facet.id === id) {\n if (facet.type === 'number' && modifier) {\n facet.modifier = modifier\n }\n if (operatorType) {\n facet.operatorType = operatorType\n }\n if (typeof value !== 'undefined') {\n state.facets[index].value = value\n }\n }\n })\n },\n // Update existing search query\n querySet(state, action: PayloadAction<{searchQuery: string}>) {\n state.query = action.payload?.searchQuery\n }\n }\n})\n\n// Epics\n\n// On tag update success -> update existing tag search facet (if present)\nexport const searchFacetTagUpdateEpic: MyEpic = (action$, state$) =>\n action$.pipe(\n filter(tagsActions.updateComplete.match),\n withLatestFrom(state$),\n mergeMap(([action, state]) => {\n const {tag} = action.payload\n\n const currentSearchFacetTag = state.search.facets?.find(facet => facet.name === 'tag')\n const tagItem = state.tags.byIds[tag._id]\n\n if (currentSearchFacetTag?.type === 'searchable') {\n if (currentSearchFacetTag.value?.value === tag._id) {\n return of(\n searchSlice.actions.facetsUpdate({\n name: 'tag',\n value: {\n label: tagItem?.tag?.name?.current,\n value: tagItem?.tag?._id\n }\n })\n )\n }\n }\n\n return EMPTY\n })\n )\n\n// Selectors\nexport const selectIsSearchFacetTag = createSelector(\n [\n (state: RootReducerState) => state.search.facets,\n (_state: RootReducerState, tagId: string) => tagId\n ],\n (searchFacets, tagId) =>\n searchFacets.some(\n facet =>\n facet.name === 'tag' &&\n facet.type === 'searchable' &&\n (facet.operatorType === 'references' || facet.operatorType === 'doesNotReference') &&\n facet.value?.value === tagId\n )\n)\n\nexport const searchActions = {...searchSlice.actions}\n\nexport default searchSlice.reducer\n","import {createAction} from '@reduxjs/toolkit'\nimport type {SanityAssetDocument, SanityImageAssetDocument} from '@sa