sanity-linked-media-asset
Version:
Expose and override image-asset fields (title, alt, …) directly in Sanity Studio.
324 lines (320 loc) • 13.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
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;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
LinkedMediaAssetField: () => LinkedMediaAssetField_default,
getLinkedMediaAssetFields: () => getLinkedMediaAssetFields
});
module.exports = __toCommonJS(index_exports);
// src/LinkedMediaAssetField.tsx
var import_react = __toESM(require("react"));
var import_sanity = require("sanity");
var import_ui = require("@sanity/ui");
var import_icons = require("@sanity/icons");
var import_icons2 = require("@sanity/icons");
function useImageFieldAndAsset(path) {
var _a;
const imageField = (0, import_sanity.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] = import_react.default.useState(
void 0
);
const [assetInputValue, setAssetInputValue] = import_react.default.useState(void 0);
import_react.default.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]);
import_react.default.useEffect(() => {
setAssetInputValue(assetValue != null ? assetValue : "");
}, [assetValue]);
return [
assetValue,
setAssetValue,
assetInputValue,
setAssetInputValue
];
}
var LinkedMediaAssetField = (props) => {
const { value, onChange, elementProps, path } = props;
const toast = (0, import_ui.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 = (0, import_sanity.useClient)({ apiVersion });
const { assetRef, fieldName } = useImageFieldAndAsset(path);
const [assetValue, setAssetValue, assetInputValue, setAssetInputValue] = useAssetFieldValue(assetRef, fieldName, client);
const [updatingAsset, setUpdatingAsset] = import_react.default.useState(false);
const [useSharedAsset, setUseSharedAsset] = import_react.default.useState(!value);
import_react.default.useEffect(() => {
setUseSharedAsset(!value);
}, [value]);
return /* @__PURE__ */ import_react.default.createElement(import_ui.Card, { padding: 2, radius: 2 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Stack, { space: 2 }, assetRef === void 0 ? /* @__PURE__ */ import_react.default.createElement(import_ui.Card, { padding: 4, tone: "caution", radius: 2, shadow: 1 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Flex, { align: "center", gap: 3 }, /* @__PURE__ */ import_react.default.createElement(
import_icons.HelpCircleIcon,
{
style: { color: "var(--card-muted-fg-color, #aaa)" }
}
), /* @__PURE__ */ import_react.default.createElement(import_ui.Text, { size: 2 }, "Select or upload an asset to enable linked asset fields."))) : /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, assetValue !== void 0 && /* @__PURE__ */ import_react.default.createElement(
AssetFieldInput,
{
assetInputValue,
setAssetInputValue,
assetRef,
assetValue,
setAssetValue,
fieldName,
client,
updatingAsset,
setUpdatingAsset,
toast,
useSharedAsset,
setUseSharedAsset,
onChange
}
), !useSharedAsset && /* @__PURE__ */ import_react.default.createElement(import_ui.Flex, { direction: "row", align: "flex-end", gap: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Box, { flex: 1 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Stack, { space: 2 }, /* @__PURE__ */ import_react.default.createElement(import_ui.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__ */ import_react.default.createElement(import_ui.Card, { padding: 3, tone: "critical" }, /* @__PURE__ */ import_react.default.createElement(import_ui.Text, { size: 1, weight: "bold" }, "ERROR: renderDefault missing"), /* @__PURE__ */ import_react.default.createElement(import_ui.Text, { size: 1 }, "This field requires Sanity Studio v3+ and correct schema registration.")))), /* @__PURE__ */ import_react.default.createElement(import_ui.Box, null, /* @__PURE__ */ import_react.default.createElement(
import_ui.Tooltip,
{
content: /* @__PURE__ */ import_react.default.createElement(import_ui.Box, { padding: 2 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Text, { size: 2 }, "Copy asset value to local field")),
fallbackPlacements: ["right", "left"],
placement: "top",
portal: true
},
/* @__PURE__ */ import_react.default.createElement("span", { style: { display: "inline-block" } }, /* @__PURE__ */ import_react.default.createElement(
import_ui.Button,
{
icon: import_icons2.CopyIcon,
mode: "bleed",
"aria-label": "Copy from asset",
onClick: () => onChange((0, import_sanity.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__ */ import_react.default.createElement(import_ui.Card, { padding: 0 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Stack, { space: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Flex, { align: "center", gap: 3 }, /* @__PURE__ */ import_react.default.createElement(import_ui.Text, { size: 1, weight: "medium" }, "Asset"), /* @__PURE__ */ import_react.default.createElement(
import_ui.Switch,
{
checked: useSharedAsset,
onChange: (e) => {
setUseSharedAsset(e.currentTarget.checked);
if (e.currentTarget.checked) {
onChange((0, import_sanity.unset)());
} else {
onChange((0, import_sanity.set)(assetInputValue || ""));
}
},
id: "use-shared-asset-switch",
"aria-label": "Use shared asset value for this field",
style: { marginRight: 2 }
}
), /* @__PURE__ */ import_react.default.createElement(
import_ui.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__ */ import_react.default.createElement(
import_ui.Tooltip,
{
content: /* @__PURE__ */ import_react.default.createElement(import_ui.Box, { padding: 2 }, /* @__PURE__ */ import_react.default.createElement(import_ui.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__ */ import_react.default.createElement(
import_ui.Box,
{
style: { display: "flex", alignItems: "center", cursor: "help" }
},
/* @__PURE__ */ import_react.default.createElement(
import_icons.HelpCircleIcon,
{
style: { color: "var(--card-muted-fg-color, #aaa)" }
}
)
)
)), /* @__PURE__ */ import_react.default.createElement(
import_ui.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
var import_sanity2 = require("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) => (0, import_sanity2.defineField)({
name,
type: "string",
// @ts-ignore - TODO repo structure issue, TS can't resolve this import
components: { input: LinkedMediaAssetField_default }
})
);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
LinkedMediaAssetField,
getLinkedMediaAssetFields
});