sanity-linked-media-asset
Version:
Expose and override image-asset fields (title, alt, …) directly in Sanity Studio.
305 lines (302 loc) • 10.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
// src/LinkedMediaAssetField.tsx
import React from "react";
import {
set,
unset,
useFormValue,
useClient
} from "sanity";
import {
Box,
Flex,
Text,
Button,
TextInput,
Stack,
Card,
Tooltip,
useToast,
Switch
} from "@sanity/ui";
import { HelpCircleIcon } from "@sanity/icons";
import { CopyIcon } from "@sanity/icons";
function useImageFieldAndAsset(path) {
var _a;
const imageField = useFormValue(path.slice(0, -1));
const assetRef = (_a = imageField == null ? void 0 : imageField.asset) == null ? void 0 : _a._ref;
const fieldName = String(path[path.length - 1]);
return { imageField, assetRef, fieldName };
}
function useAssetFieldValue(assetRef, fieldName, client) {
const [assetValue, setAssetValue] = React.useState(
void 0
);
const [assetInputValue, setAssetInputValue] = React.useState(void 0);
React.useEffect(() => {
if (!assetRef) {
setAssetValue(void 0);
setAssetInputValue(void 0);
return;
}
const fetchAsset = async () => {
var _a;
const query = `*[_id == $id][0]{${fieldName}}`;
const params = { id: assetRef };
const asset = await client.fetch(query, params);
setAssetValue(asset == null ? void 0 : asset[fieldName]);
setAssetInputValue((_a = asset == null ? void 0 : asset[fieldName]) != null ? _a : "");
};
fetchAsset();
}, [assetRef, fieldName, client]);
React.useEffect(() => {
setAssetInputValue(assetValue != null ? assetValue : "");
}, [assetValue]);
return [
assetValue,
setAssetValue,
assetInputValue,
setAssetInputValue
];
}
var LinkedMediaAssetField = (props) => {
const { value, onChange, elementProps, path } = props;
const toast = useToast();
const _a = elementProps || {}, { onChange: _ignoredOnChange } = _a, restElementProps = __objRest(_a, ["onChange"]);
const apiVersion = typeof process !== "undefined" && process.env && process.env.SANITY_STUDIO_LINKED_MEDIA_ASSET_API_VERSION ? process.env.SANITY_STUDIO_LINKED_MEDIA_ASSET_API_VERSION : "2023-08-01";
const client = useClient({ apiVersion });
const { assetRef, fieldName } = useImageFieldAndAsset(path);
const [assetValue, setAssetValue, assetInputValue, setAssetInputValue] = useAssetFieldValue(assetRef, fieldName, client);
const [updatingAsset, setUpdatingAsset] = React.useState(false);
const [useSharedAsset, setUseSharedAsset] = React.useState(!value);
React.useEffect(() => {
setUseSharedAsset(!value);
}, [value]);
return /* @__PURE__ */ React.createElement(Card, { padding: 2, radius: 2 }, /* @__PURE__ */ React.createElement(Stack, { space: 2 }, assetRef === void 0 ? /* @__PURE__ */ React.createElement(Card, { padding: 4, tone: "caution", radius: 2, shadow: 1 }, /* @__PURE__ */ React.createElement(Flex, { align: "center", gap: 3 }, /* @__PURE__ */ React.createElement(
HelpCircleIcon,
{
style: { color: "var(--card-muted-fg-color, #aaa)" }
}
), /* @__PURE__ */ React.createElement(Text, { size: 2 }, "Select or upload an asset to enable linked asset fields."))) : /* @__PURE__ */ React.createElement(React.Fragment, null, assetValue !== void 0 && /* @__PURE__ */ React.createElement(
AssetFieldInput,
{
assetInputValue,
setAssetInputValue,
assetRef,
assetValue,
setAssetValue,
fieldName,
client,
updatingAsset,
setUpdatingAsset,
toast,
useSharedAsset,
setUseSharedAsset,
onChange
}
), !useSharedAsset && /* @__PURE__ */ React.createElement(Flex, { direction: "row", align: "flex-end", gap: 3 }, /* @__PURE__ */ React.createElement(Box, { flex: 1 }, /* @__PURE__ */ React.createElement(Stack, { space: 2 }, /* @__PURE__ */ React.createElement(Text, { size: 1, weight: "medium" }, "Local"), props.renderDefault ? props.renderDefault(__spreadProps(__spreadValues({}, props), {
elementProps: props.elementProps
})) : process.env.NODE_ENV === "development" ? (() => {
throw new Error(
"LinkedMediaAssetField: renderDefault is required but was not provided. Check your schema/component registration."
);
})() : /* @__PURE__ */ React.createElement(Card, { padding: 3, tone: "critical" }, /* @__PURE__ */ React.createElement(Text, { size: 1, weight: "bold" }, "ERROR: renderDefault missing"), /* @__PURE__ */ React.createElement(Text, { size: 1 }, "This field requires Sanity Studio v3+ and correct schema registration.")))), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(
Tooltip,
{
content: /* @__PURE__ */ React.createElement(Box, { padding: 2 }, /* @__PURE__ */ React.createElement(Text, { size: 2 }, "Copy asset value to local field")),
fallbackPlacements: ["right", "left"],
placement: "top",
portal: true
},
/* @__PURE__ */ React.createElement("span", { style: { display: "inline-block" } }, /* @__PURE__ */ React.createElement(
Button,
{
icon: CopyIcon,
mode: "bleed",
"aria-label": "Copy from asset",
onClick: () => onChange(set(assetValue)),
disabled: value === assetValue
}
))
))))));
};
var LinkedMediaAssetField_default = LinkedMediaAssetField;
async function handleAssetFieldBlur({
assetRef,
assetInputValue,
assetValue,
setUpdatingAsset,
setAssetValue,
fieldName,
client,
toast
}) {
if (!assetRef || assetInputValue === assetValue) return;
setUpdatingAsset(true);
try {
await client.patch(assetRef).set({ [fieldName]: assetInputValue }).commit();
setAssetValue(assetInputValue);
} catch (err) {
toast.push({
status: "error",
title: "Failed to update asset",
description: err instanceof Error ? err.message : String(err)
});
} finally {
setUpdatingAsset(false);
}
}
function AssetFieldInput({
assetInputValue,
setAssetInputValue,
assetRef,
assetValue,
setAssetValue,
fieldName,
client,
updatingAsset,
setUpdatingAsset,
toast,
useSharedAsset,
setUseSharedAsset,
onChange
}) {
return /* @__PURE__ */ React.createElement(Card, { padding: 0 }, /* @__PURE__ */ React.createElement(Stack, { space: 3 }, /* @__PURE__ */ React.createElement(Flex, { align: "center", gap: 3 }, /* @__PURE__ */ React.createElement(Text, { size: 1, weight: "medium" }, "Asset"), /* @__PURE__ */ React.createElement(
Switch,
{
checked: useSharedAsset,
onChange: (e) => {
setUseSharedAsset(e.currentTarget.checked);
if (e.currentTarget.checked) {
onChange(unset());
} else {
onChange(set(assetInputValue || ""));
}
},
id: "use-shared-asset-switch",
"aria-label": "Use shared asset value for this field",
style: { marginRight: 2 }
}
), /* @__PURE__ */ React.createElement(
Text,
{
size: 1,
as: "label",
htmlFor: "use-shared-asset-switch",
style: { color: "var(--card-muted-fg-color, #aaa)" }
},
useSharedAsset ? "Use value from the shared asset" : "Use value specific to this document"
), /* @__PURE__ */ React.createElement(
Tooltip,
{
content: /* @__PURE__ */ React.createElement(Box, { padding: 2 }, /* @__PURE__ */ React.createElement(Text, { size: 2 }, useSharedAsset ? "This field uses the shared asset value. Turn off to override locally." : "This field overrides the shared asset value locally.")),
fallbackPlacements: ["right", "left"],
placement: "top",
portal: true
},
/* @__PURE__ */ React.createElement(
Box,
{
style: { display: "flex", alignItems: "center", cursor: "help" }
},
/* @__PURE__ */ React.createElement(
HelpCircleIcon,
{
style: { color: "var(--card-muted-fg-color, #aaa)" }
}
)
)
)), /* @__PURE__ */ React.createElement(
TextInput,
{
value: assetInputValue || "",
onChange: (e) => setAssetInputValue(e.currentTarget.value),
onBlur: () => handleAssetFieldBlur({
assetRef,
assetInputValue,
assetValue,
setUpdatingAsset,
setAssetValue,
fieldName,
client,
toast
}),
placeholder: `Asset ${fieldName}`,
style: { minWidth: 120 },
disabled: updatingAsset
}
)));
}
// src/linkedMediaAssetFields.ts
import { defineField } from "sanity";
function getLinkedMediaAssetFields(options = {}) {
const fieldNames = [
"title",
"altText",
"description",
"creditLine"
// Add more fields as needed
];
if (process.env.NODE_ENV !== "production") {
const allowed = new Set(fieldNames);
const unknownKeys = Object.keys(options).filter(
(k) => !allowed.has(k)
);
if (unknownKeys.length > 0) {
throw new Error(
`Unknown option key(s) passed to getLinkedMediaAssetFields: ${unknownKeys.join(
", "
)}.
Accepted keys: ${fieldNames.join(", ")}`
);
}
}
return fieldNames.filter((name) => {
var _a, _b;
if (name === "creditLine") {
return ((_a = options.creditLine) == null ? void 0 : _a.enabled) === true;
}
return ((_b = options[name]) == null ? void 0 : _b.enabled) !== false;
}).map(
(name) => defineField({
name,
type: "string",
// @ts-ignore - TODO repo structure issue, TS can't resolve this import
components: { input: LinkedMediaAssetField_default }
})
);
}
export {
LinkedMediaAssetField_default as LinkedMediaAssetField,
getLinkedMediaAssetFields
};