@liiift-studio/sanity-font-manager 2.3.9 → 2.3.11
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 +157 -0
- package/dist/index.mjs +153 -0
- package/package.json +1 -1
- package/src/components/NestedObjectArraySelector.jsx +146 -0
- package/src/hooks/useNestedObjects.js +92 -0
- package/src/index.js +3 -1
- package/src/utils/utils.js +8 -0
package/dist/index.js
CHANGED
|
@@ -30,11 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
32
|
BatchUploadFonts: () => BatchUploadFonts,
|
|
33
|
+
DISCOUNT_REQUIREMENT_TYPES: () => DISCOUNT_REQUIREMENT_TYPES,
|
|
34
|
+
DISCOUNT_REQUIREMENT_TYPES_OBJECT: () => DISCOUNT_REQUIREMENT_TYPES_OBJECT,
|
|
33
35
|
FontScriptUploaderComponent: () => FontScriptUploaderComponent,
|
|
34
36
|
GenerateCollectionsPairsComponent: () => GenerateCollectionsPairsComponent,
|
|
35
37
|
HtmlDescription: () => HtmlDescription,
|
|
36
38
|
KeyValueInput: () => KeyValueInput,
|
|
37
39
|
KeyValueReferenceInput: () => KeyValueReferenceInput,
|
|
40
|
+
NestedObjectArraySelector: () => NestedObjectArraySelector,
|
|
38
41
|
PriceInput: () => PriceInput_default,
|
|
39
42
|
PrimaryCollectionGeneratorTypeface: () => PrimaryCollectionGeneratorTypeface,
|
|
40
43
|
RegenerateSubfamiliesComponent: () => RegenerateSubfamiliesComponent,
|
|
@@ -80,6 +83,7 @@ __export(index_exports, {
|
|
|
80
83
|
updateFontPrices: () => updateFontPrices,
|
|
81
84
|
updateTypefaceDocument: () => updateTypefaceDocument,
|
|
82
85
|
uploadFontFiles: () => uploadFontFiles,
|
|
86
|
+
useNestedObjects: () => useNestedObjects,
|
|
83
87
|
useSanityClient: () => useSanityClient
|
|
84
88
|
});
|
|
85
89
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2970,6 +2974,10 @@ var SCRIPTS = (process.env.SANITY_STUDIO_SCRIPTS || "").split(",").map((script)
|
|
|
2970
2974
|
var SCRIPTS_OBJECT = SCRIPTS.map((script) => {
|
|
2971
2975
|
return { title: script[0].toUpperCase() + script.slice(1), value: script };
|
|
2972
2976
|
});
|
|
2977
|
+
var DISCOUNT_REQUIREMENT_TYPES = (process.env.SANITY_STUDIO_DISCOUNT_REQ_TYPES || "").split(",").map((type) => type.trim()).filter(Boolean);
|
|
2978
|
+
var DISCOUNT_REQUIREMENT_TYPES_OBJECT = DISCOUNT_REQUIREMENT_TYPES.map((type) => {
|
|
2979
|
+
return { title: type[0].toUpperCase() + type.slice(1), value: type };
|
|
2980
|
+
});
|
|
2973
2981
|
|
|
2974
2982
|
// src/components/UploadScriptsComponent.jsx
|
|
2975
2983
|
var UploadScriptsComponent = (props) => {
|
|
@@ -4121,6 +4129,151 @@ var StyleCountInput = (props) => {
|
|
|
4121
4129
|
return /* @__PURE__ */ import_react18.default.createElement(import_ui16.Text, { size: 1 }, count);
|
|
4122
4130
|
};
|
|
4123
4131
|
|
|
4132
|
+
// src/components/NestedObjectArraySelector.jsx
|
|
4133
|
+
var import_react20 = __toESM(require("react"));
|
|
4134
|
+
var import_ui17 = require("@sanity/ui");
|
|
4135
|
+
|
|
4136
|
+
// src/hooks/useNestedObjects.js
|
|
4137
|
+
var import_react19 = require("react");
|
|
4138
|
+
var import_sanity15 = require("sanity");
|
|
4139
|
+
function useNestedObjects({
|
|
4140
|
+
sourceType,
|
|
4141
|
+
nestedField,
|
|
4142
|
+
titleField,
|
|
4143
|
+
valueField,
|
|
4144
|
+
filter = "",
|
|
4145
|
+
sortBy = ""
|
|
4146
|
+
}) {
|
|
4147
|
+
const client = (0, import_sanity15.useClient)({ apiVersion: "2023-01-01" });
|
|
4148
|
+
const [objects, setObjects] = (0, import_react19.useState)([]);
|
|
4149
|
+
const [loading, setLoading] = (0, import_react19.useState)(true);
|
|
4150
|
+
const [error, setError] = (0, import_react19.useState)(null);
|
|
4151
|
+
(0, import_react19.useEffect)(() => {
|
|
4152
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
4153
|
+
setError(new Error("Missing required configuration"));
|
|
4154
|
+
setLoading(false);
|
|
4155
|
+
return;
|
|
4156
|
+
}
|
|
4157
|
+
const fetchData = async () => {
|
|
4158
|
+
try {
|
|
4159
|
+
setLoading(true);
|
|
4160
|
+
setError(null);
|
|
4161
|
+
const filterClause = filter ? ` && ${filter}` : "";
|
|
4162
|
+
const query = `
|
|
4163
|
+
*[_type == "${sourceType}"${filterClause}] {
|
|
4164
|
+
"${nestedField}": ${nestedField}[] {
|
|
4165
|
+
"title": ${titleField},
|
|
4166
|
+
"value": ${valueField}
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
`;
|
|
4170
|
+
const result = await client.fetch(query);
|
|
4171
|
+
if (!result || result.length === 0) {
|
|
4172
|
+
setObjects([]);
|
|
4173
|
+
setLoading(false);
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
const flattened = result.flatMap((doc) => doc[nestedField] || []);
|
|
4177
|
+
const uniqueMap = /* @__PURE__ */ new Map();
|
|
4178
|
+
flattened.forEach((item) => {
|
|
4179
|
+
if (item.value && item.title) uniqueMap.set(item.value, item);
|
|
4180
|
+
});
|
|
4181
|
+
let unique = Array.from(uniqueMap.values());
|
|
4182
|
+
if (sortBy) {
|
|
4183
|
+
const [sortField, sortOrder = "asc"] = sortBy.split(" ");
|
|
4184
|
+
unique = unique.sort((a, b) => {
|
|
4185
|
+
const aVal = a[sortField] || a.title;
|
|
4186
|
+
const bVal = b[sortField] || b.title;
|
|
4187
|
+
const comparison = aVal.localeCompare(bVal);
|
|
4188
|
+
return sortOrder === "desc" ? -comparison : comparison;
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
setObjects(unique);
|
|
4192
|
+
} catch (err) {
|
|
4193
|
+
console.error("useNestedObjects fetch error:", err);
|
|
4194
|
+
setError(err);
|
|
4195
|
+
} finally {
|
|
4196
|
+
setLoading(false);
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
fetchData();
|
|
4200
|
+
}, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
|
|
4201
|
+
return { objects, loading, error };
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// src/components/NestedObjectArraySelector.jsx
|
|
4205
|
+
var import_sanity16 = require("sanity");
|
|
4206
|
+
var NestedObjectArraySelector = import_react20.default.forwardRef((props, ref) => {
|
|
4207
|
+
const { value = [], onChange, schemaType } = props;
|
|
4208
|
+
const options = (schemaType == null ? void 0 : schemaType.options) || {};
|
|
4209
|
+
const {
|
|
4210
|
+
sourceType,
|
|
4211
|
+
nestedField,
|
|
4212
|
+
titleField,
|
|
4213
|
+
valueField,
|
|
4214
|
+
filter = "",
|
|
4215
|
+
sortBy = "title asc",
|
|
4216
|
+
emptyMessage = "No options found",
|
|
4217
|
+
searchPlaceholder = "Search..."
|
|
4218
|
+
} = options;
|
|
4219
|
+
const [searchTerm, setSearchTerm] = (0, import_react20.useState)("");
|
|
4220
|
+
const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
|
|
4221
|
+
const filteredObjects = (0, import_react20.useMemo)(() => {
|
|
4222
|
+
if (!searchTerm) return objects;
|
|
4223
|
+
const lower = searchTerm.toLowerCase();
|
|
4224
|
+
return objects.filter((obj) => {
|
|
4225
|
+
var _a;
|
|
4226
|
+
return (_a = obj.title) == null ? void 0 : _a.toLowerCase().includes(lower);
|
|
4227
|
+
});
|
|
4228
|
+
}, [objects, searchTerm]);
|
|
4229
|
+
const handleToggle = (itemValue) => {
|
|
4230
|
+
const currentValue = value || [];
|
|
4231
|
+
const isSelected = currentValue.includes(itemValue);
|
|
4232
|
+
if (isSelected) {
|
|
4233
|
+
const newValue = currentValue.filter((v) => v !== itemValue);
|
|
4234
|
+
onChange(newValue.length > 0 ? (0, import_sanity16.set)(newValue) : (0, import_sanity16.unset)());
|
|
4235
|
+
} else {
|
|
4236
|
+
onChange((0, import_sanity16.set)([...currentValue, itemValue]));
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
4240
|
+
return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)"));
|
|
4241
|
+
}
|
|
4242
|
+
if (loading) {
|
|
4243
|
+
return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Flex, { align: "center", justify: "center", padding: 4 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Spinner, null), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { marginLeft: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Loading options..."))));
|
|
4244
|
+
}
|
|
4245
|
+
if (error) {
|
|
4246
|
+
return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1 }, "Error loading options: ", error.message));
|
|
4247
|
+
}
|
|
4248
|
+
if (objects.length === 0) {
|
|
4249
|
+
return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 3, tone: "transparent", border: true }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, emptyMessage));
|
|
4250
|
+
}
|
|
4251
|
+
return /* @__PURE__ */ import_react20.default.createElement(import_ui17.Card, { padding: 0, border: true, ref }, objects.length > 5 && /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 3, style: { borderBottom: "1px solid var(--card-border-color)" } }, /* @__PURE__ */ import_react20.default.createElement(
|
|
4252
|
+
"input",
|
|
4253
|
+
{
|
|
4254
|
+
type: "text",
|
|
4255
|
+
placeholder: searchPlaceholder,
|
|
4256
|
+
value: searchTerm,
|
|
4257
|
+
onChange: (e) => setSearchTerm(e.target.value),
|
|
4258
|
+
style: { width: "100%", padding: "8px 12px", border: "1px solid var(--card-border-color)", borderRadius: "4px", fontSize: "13px", fontFamily: "inherit" }
|
|
4259
|
+
}
|
|
4260
|
+
)), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Stack, { space: 0 }, filteredObjects.length === 0 ? /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, 'No results found for "', searchTerm, '"')) : filteredObjects.map((obj, index) => {
|
|
4261
|
+
const isSelected = value == null ? void 0 : value.includes(obj.value);
|
|
4262
|
+
const isLast = index === filteredObjects.length - 1;
|
|
4263
|
+
return /* @__PURE__ */ import_react20.default.createElement(
|
|
4264
|
+
import_ui17.Box,
|
|
4265
|
+
{
|
|
4266
|
+
key: obj.value,
|
|
4267
|
+
padding: 3,
|
|
4268
|
+
style: { borderBottom: isLast ? "none" : "1px solid var(--card-border-color)", cursor: "pointer", backgroundColor: isSelected ? "var(--card-muted-fg-color)" : "transparent", transition: "background-color 0.2s" },
|
|
4269
|
+
onClick: () => handleToggle(obj.value)
|
|
4270
|
+
},
|
|
4271
|
+
/* @__PURE__ */ import_react20.default.createElement(import_ui17.Flex, { align: "center", gap: 3 }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Checkbox, { checked: isSelected, readOnly: true, style: { pointerEvents: "none" } }), /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, weight: isSelected ? "semibold" : "regular" }, obj.title))
|
|
4272
|
+
);
|
|
4273
|
+
})), (value == null ? void 0 : value.length) > 0 && /* @__PURE__ */ import_react20.default.createElement(import_ui17.Box, { padding: 2, paddingX: 3, style: { borderTop: "1px solid var(--card-border-color)", backgroundColor: "var(--card-muted-fg-color)" } }, /* @__PURE__ */ import_react20.default.createElement(import_ui17.Text, { size: 1, muted: true }, value.length, " selected")));
|
|
4274
|
+
});
|
|
4275
|
+
NestedObjectArraySelector.displayName = "NestedObjectArraySelector";
|
|
4276
|
+
|
|
4124
4277
|
// src/utils/getEmptyFontKit.js
|
|
4125
4278
|
var fontkit7 = __toESM(require("fontkit"));
|
|
4126
4279
|
var import_slugify4 = __toESM(require("slugify"));
|
|
@@ -6877,11 +7030,14 @@ function createStylesField({
|
|
|
6877
7030
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6878
7031
|
0 && (module.exports = {
|
|
6879
7032
|
BatchUploadFonts,
|
|
7033
|
+
DISCOUNT_REQUIREMENT_TYPES,
|
|
7034
|
+
DISCOUNT_REQUIREMENT_TYPES_OBJECT,
|
|
6880
7035
|
FontScriptUploaderComponent,
|
|
6881
7036
|
GenerateCollectionsPairsComponent,
|
|
6882
7037
|
HtmlDescription,
|
|
6883
7038
|
KeyValueInput,
|
|
6884
7039
|
KeyValueReferenceInput,
|
|
7040
|
+
NestedObjectArraySelector,
|
|
6885
7041
|
PriceInput,
|
|
6886
7042
|
PrimaryCollectionGeneratorTypeface,
|
|
6887
7043
|
RegenerateSubfamiliesComponent,
|
|
@@ -6927,5 +7083,6 @@ function createStylesField({
|
|
|
6927
7083
|
updateFontPrices,
|
|
6928
7084
|
updateTypefaceDocument,
|
|
6929
7085
|
uploadFontFiles,
|
|
7086
|
+
useNestedObjects,
|
|
6930
7087
|
useSanityClient
|
|
6931
7088
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -2884,6 +2884,10 @@ var SCRIPTS = (process.env.SANITY_STUDIO_SCRIPTS || "").split(",").map((script)
|
|
|
2884
2884
|
var SCRIPTS_OBJECT = SCRIPTS.map((script) => {
|
|
2885
2885
|
return { title: script[0].toUpperCase() + script.slice(1), value: script };
|
|
2886
2886
|
});
|
|
2887
|
+
var DISCOUNT_REQUIREMENT_TYPES = (process.env.SANITY_STUDIO_DISCOUNT_REQ_TYPES || "").split(",").map((type) => type.trim()).filter(Boolean);
|
|
2888
|
+
var DISCOUNT_REQUIREMENT_TYPES_OBJECT = DISCOUNT_REQUIREMENT_TYPES.map((type) => {
|
|
2889
|
+
return { title: type[0].toUpperCase() + type.slice(1), value: type };
|
|
2890
|
+
});
|
|
2887
2891
|
|
|
2888
2892
|
// src/components/UploadScriptsComponent.jsx
|
|
2889
2893
|
var UploadScriptsComponent = (props) => {
|
|
@@ -4035,6 +4039,151 @@ var StyleCountInput = (props) => {
|
|
|
4035
4039
|
return /* @__PURE__ */ React17.createElement(Text15, { size: 1 }, count);
|
|
4036
4040
|
};
|
|
4037
4041
|
|
|
4042
|
+
// src/components/NestedObjectArraySelector.jsx
|
|
4043
|
+
import React18, { useMemo as useMemo5, useState as useState14 } from "react";
|
|
4044
|
+
import { Stack as Stack13, Card as Card6, Text as Text16, Checkbox, Box as Box6, Spinner as Spinner4, Flex as Flex11 } from "@sanity/ui";
|
|
4045
|
+
|
|
4046
|
+
// src/hooks/useNestedObjects.js
|
|
4047
|
+
import { useState as useState13, useEffect as useEffect7 } from "react";
|
|
4048
|
+
import { useClient as useClient2 } from "sanity";
|
|
4049
|
+
function useNestedObjects({
|
|
4050
|
+
sourceType,
|
|
4051
|
+
nestedField,
|
|
4052
|
+
titleField,
|
|
4053
|
+
valueField,
|
|
4054
|
+
filter = "",
|
|
4055
|
+
sortBy = ""
|
|
4056
|
+
}) {
|
|
4057
|
+
const client = useClient2({ apiVersion: "2023-01-01" });
|
|
4058
|
+
const [objects, setObjects] = useState13([]);
|
|
4059
|
+
const [loading, setLoading] = useState13(true);
|
|
4060
|
+
const [error, setError] = useState13(null);
|
|
4061
|
+
useEffect7(() => {
|
|
4062
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
4063
|
+
setError(new Error("Missing required configuration"));
|
|
4064
|
+
setLoading(false);
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
const fetchData = async () => {
|
|
4068
|
+
try {
|
|
4069
|
+
setLoading(true);
|
|
4070
|
+
setError(null);
|
|
4071
|
+
const filterClause = filter ? ` && ${filter}` : "";
|
|
4072
|
+
const query = `
|
|
4073
|
+
*[_type == "${sourceType}"${filterClause}] {
|
|
4074
|
+
"${nestedField}": ${nestedField}[] {
|
|
4075
|
+
"title": ${titleField},
|
|
4076
|
+
"value": ${valueField}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
`;
|
|
4080
|
+
const result = await client.fetch(query);
|
|
4081
|
+
if (!result || result.length === 0) {
|
|
4082
|
+
setObjects([]);
|
|
4083
|
+
setLoading(false);
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
const flattened = result.flatMap((doc) => doc[nestedField] || []);
|
|
4087
|
+
const uniqueMap = /* @__PURE__ */ new Map();
|
|
4088
|
+
flattened.forEach((item) => {
|
|
4089
|
+
if (item.value && item.title) uniqueMap.set(item.value, item);
|
|
4090
|
+
});
|
|
4091
|
+
let unique = Array.from(uniqueMap.values());
|
|
4092
|
+
if (sortBy) {
|
|
4093
|
+
const [sortField, sortOrder = "asc"] = sortBy.split(" ");
|
|
4094
|
+
unique = unique.sort((a, b) => {
|
|
4095
|
+
const aVal = a[sortField] || a.title;
|
|
4096
|
+
const bVal = b[sortField] || b.title;
|
|
4097
|
+
const comparison = aVal.localeCompare(bVal);
|
|
4098
|
+
return sortOrder === "desc" ? -comparison : comparison;
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
setObjects(unique);
|
|
4102
|
+
} catch (err) {
|
|
4103
|
+
console.error("useNestedObjects fetch error:", err);
|
|
4104
|
+
setError(err);
|
|
4105
|
+
} finally {
|
|
4106
|
+
setLoading(false);
|
|
4107
|
+
}
|
|
4108
|
+
};
|
|
4109
|
+
fetchData();
|
|
4110
|
+
}, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
|
|
4111
|
+
return { objects, loading, error };
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// src/components/NestedObjectArraySelector.jsx
|
|
4115
|
+
import { set as set8, unset as unset3 } from "sanity";
|
|
4116
|
+
var NestedObjectArraySelector = React18.forwardRef((props, ref) => {
|
|
4117
|
+
const { value = [], onChange, schemaType } = props;
|
|
4118
|
+
const options = (schemaType == null ? void 0 : schemaType.options) || {};
|
|
4119
|
+
const {
|
|
4120
|
+
sourceType,
|
|
4121
|
+
nestedField,
|
|
4122
|
+
titleField,
|
|
4123
|
+
valueField,
|
|
4124
|
+
filter = "",
|
|
4125
|
+
sortBy = "title asc",
|
|
4126
|
+
emptyMessage = "No options found",
|
|
4127
|
+
searchPlaceholder = "Search..."
|
|
4128
|
+
} = options;
|
|
4129
|
+
const [searchTerm, setSearchTerm] = useState14("");
|
|
4130
|
+
const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
|
|
4131
|
+
const filteredObjects = useMemo5(() => {
|
|
4132
|
+
if (!searchTerm) return objects;
|
|
4133
|
+
const lower = searchTerm.toLowerCase();
|
|
4134
|
+
return objects.filter((obj) => {
|
|
4135
|
+
var _a;
|
|
4136
|
+
return (_a = obj.title) == null ? void 0 : _a.toLowerCase().includes(lower);
|
|
4137
|
+
});
|
|
4138
|
+
}, [objects, searchTerm]);
|
|
4139
|
+
const handleToggle = (itemValue) => {
|
|
4140
|
+
const currentValue = value || [];
|
|
4141
|
+
const isSelected = currentValue.includes(itemValue);
|
|
4142
|
+
if (isSelected) {
|
|
4143
|
+
const newValue = currentValue.filter((v) => v !== itemValue);
|
|
4144
|
+
onChange(newValue.length > 0 ? set8(newValue) : unset3());
|
|
4145
|
+
} else {
|
|
4146
|
+
onChange(set8([...currentValue, itemValue]));
|
|
4147
|
+
}
|
|
4148
|
+
};
|
|
4149
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
4150
|
+
return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)"));
|
|
4151
|
+
}
|
|
4152
|
+
if (loading) {
|
|
4153
|
+
return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, border: true }, /* @__PURE__ */ React18.createElement(Flex11, { align: "center", justify: "center", padding: 4 }, /* @__PURE__ */ React18.createElement(Spinner4, null), /* @__PURE__ */ React18.createElement(Box6, { marginLeft: 3 }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Loading options..."))));
|
|
4154
|
+
}
|
|
4155
|
+
if (error) {
|
|
4156
|
+
return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "critical", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1 }, "Error loading options: ", error.message));
|
|
4157
|
+
}
|
|
4158
|
+
if (objects.length === 0) {
|
|
4159
|
+
return /* @__PURE__ */ React18.createElement(Card6, { padding: 3, tone: "transparent", border: true }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, emptyMessage));
|
|
4160
|
+
}
|
|
4161
|
+
return /* @__PURE__ */ React18.createElement(Card6, { padding: 0, border: true, ref }, objects.length > 5 && /* @__PURE__ */ React18.createElement(Box6, { padding: 3, style: { borderBottom: "1px solid var(--card-border-color)" } }, /* @__PURE__ */ React18.createElement(
|
|
4162
|
+
"input",
|
|
4163
|
+
{
|
|
4164
|
+
type: "text",
|
|
4165
|
+
placeholder: searchPlaceholder,
|
|
4166
|
+
value: searchTerm,
|
|
4167
|
+
onChange: (e) => setSearchTerm(e.target.value),
|
|
4168
|
+
style: { width: "100%", padding: "8px 12px", border: "1px solid var(--card-border-color)", borderRadius: "4px", fontSize: "13px", fontFamily: "inherit" }
|
|
4169
|
+
}
|
|
4170
|
+
)), /* @__PURE__ */ React18.createElement(Stack13, { space: 0 }, filteredObjects.length === 0 ? /* @__PURE__ */ React18.createElement(Box6, { padding: 3 }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, 'No results found for "', searchTerm, '"')) : filteredObjects.map((obj, index) => {
|
|
4171
|
+
const isSelected = value == null ? void 0 : value.includes(obj.value);
|
|
4172
|
+
const isLast = index === filteredObjects.length - 1;
|
|
4173
|
+
return /* @__PURE__ */ React18.createElement(
|
|
4174
|
+
Box6,
|
|
4175
|
+
{
|
|
4176
|
+
key: obj.value,
|
|
4177
|
+
padding: 3,
|
|
4178
|
+
style: { borderBottom: isLast ? "none" : "1px solid var(--card-border-color)", cursor: "pointer", backgroundColor: isSelected ? "var(--card-muted-fg-color)" : "transparent", transition: "background-color 0.2s" },
|
|
4179
|
+
onClick: () => handleToggle(obj.value)
|
|
4180
|
+
},
|
|
4181
|
+
/* @__PURE__ */ React18.createElement(Flex11, { align: "center", gap: 3 }, /* @__PURE__ */ React18.createElement(Checkbox, { checked: isSelected, readOnly: true, style: { pointerEvents: "none" } }), /* @__PURE__ */ React18.createElement(Text16, { size: 1, weight: isSelected ? "semibold" : "regular" }, obj.title))
|
|
4182
|
+
);
|
|
4183
|
+
})), (value == null ? void 0 : value.length) > 0 && /* @__PURE__ */ React18.createElement(Box6, { padding: 2, paddingX: 3, style: { borderTop: "1px solid var(--card-border-color)", backgroundColor: "var(--card-muted-fg-color)" } }, /* @__PURE__ */ React18.createElement(Text16, { size: 1, muted: true }, value.length, " selected")));
|
|
4184
|
+
});
|
|
4185
|
+
NestedObjectArraySelector.displayName = "NestedObjectArraySelector";
|
|
4186
|
+
|
|
4038
4187
|
// src/utils/getEmptyFontKit.js
|
|
4039
4188
|
import * as fontkit7 from "fontkit";
|
|
4040
4189
|
import slugify4 from "slugify";
|
|
@@ -6790,11 +6939,14 @@ function createStylesField({
|
|
|
6790
6939
|
}
|
|
6791
6940
|
export {
|
|
6792
6941
|
BatchUploadFonts,
|
|
6942
|
+
DISCOUNT_REQUIREMENT_TYPES,
|
|
6943
|
+
DISCOUNT_REQUIREMENT_TYPES_OBJECT,
|
|
6793
6944
|
FontScriptUploaderComponent,
|
|
6794
6945
|
GenerateCollectionsPairsComponent,
|
|
6795
6946
|
HtmlDescription,
|
|
6796
6947
|
KeyValueInput,
|
|
6797
6948
|
KeyValueReferenceInput,
|
|
6949
|
+
NestedObjectArraySelector,
|
|
6798
6950
|
PriceInput_default as PriceInput,
|
|
6799
6951
|
PrimaryCollectionGeneratorTypeface,
|
|
6800
6952
|
RegenerateSubfamiliesComponent,
|
|
@@ -6840,5 +6992,6 @@ export {
|
|
|
6840
6992
|
updateFontPrices,
|
|
6841
6993
|
updateTypefaceDocument,
|
|
6842
6994
|
uploadFontFiles,
|
|
6995
|
+
useNestedObjects,
|
|
6843
6996
|
useSanityClient
|
|
6844
6997
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/sanity-font-manager",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.11",
|
|
4
4
|
"description": "Sanity Studio plugin — full font management suite with batch upload, format conversion, metadata extraction, CSS generation, collection/pair generation, and script variant support. Supports Sanity v3, v4, and v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Generic checkbox-list selector for nested objects from Sanity documents — configured via schema options
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState } from 'react';
|
|
4
|
+
import { Stack, Card, Text, Checkbox, Box, Spinner, Flex } from '@sanity/ui';
|
|
5
|
+
import { useNestedObjects } from '../hooks/useNestedObjects';
|
|
6
|
+
import { set, unset } from 'sanity';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanity input component that renders a searchable checkbox list of items fetched
|
|
10
|
+
* from nested arrays inside Sanity documents.
|
|
11
|
+
*
|
|
12
|
+
* Configure via schema options:
|
|
13
|
+
* ```js
|
|
14
|
+
* components: { input: NestedObjectArraySelector },
|
|
15
|
+
* options: {
|
|
16
|
+
* sourceType: 'licenseGroup', // document type to query
|
|
17
|
+
* nestedField: 'sections', // array field to extract
|
|
18
|
+
* titleField: 'title', // GROQ expression for display text
|
|
19
|
+
* valueField: 'slug.current', // GROQ expression for stored value
|
|
20
|
+
* filter: 'state == "published"', // optional GROQ filter
|
|
21
|
+
* sortBy: 'title asc', // optional sort field + direction
|
|
22
|
+
* emptyMessage: 'No options found', // optional empty state text
|
|
23
|
+
* searchPlaceholder: 'Search...', // optional search input placeholder
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const NestedObjectArraySelector = React.forwardRef((props, ref) => {
|
|
28
|
+
const { value = [], onChange, schemaType } = props;
|
|
29
|
+
|
|
30
|
+
const options = schemaType?.options || {};
|
|
31
|
+
const {
|
|
32
|
+
sourceType,
|
|
33
|
+
nestedField,
|
|
34
|
+
titleField,
|
|
35
|
+
valueField,
|
|
36
|
+
filter = '',
|
|
37
|
+
sortBy = 'title asc',
|
|
38
|
+
emptyMessage = 'No options found',
|
|
39
|
+
searchPlaceholder = 'Search...',
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
43
|
+
const { objects, loading, error } = useNestedObjects({ sourceType, nestedField, titleField, valueField, filter, sortBy });
|
|
44
|
+
|
|
45
|
+
const filteredObjects = useMemo(() => {
|
|
46
|
+
if (!searchTerm) return objects;
|
|
47
|
+
const lower = searchTerm.toLowerCase();
|
|
48
|
+
return objects.filter(obj => obj.title?.toLowerCase().includes(lower));
|
|
49
|
+
}, [objects, searchTerm]);
|
|
50
|
+
|
|
51
|
+
const handleToggle = (itemValue) => {
|
|
52
|
+
const currentValue = value || [];
|
|
53
|
+
const isSelected = currentValue.includes(itemValue);
|
|
54
|
+
if (isSelected) {
|
|
55
|
+
const newValue = currentValue.filter(v => v !== itemValue);
|
|
56
|
+
onChange(newValue.length > 0 ? set(newValue) : unset());
|
|
57
|
+
} else {
|
|
58
|
+
onChange(set([...currentValue, itemValue]));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
63
|
+
return (
|
|
64
|
+
<Card padding={3} tone="critical" border>
|
|
65
|
+
<Text size={1}>Configuration error: Missing required options (sourceType, nestedField, titleField, or valueField)</Text>
|
|
66
|
+
</Card>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (loading) {
|
|
71
|
+
return (
|
|
72
|
+
<Card padding={3} border>
|
|
73
|
+
<Flex align="center" justify="center" padding={4}>
|
|
74
|
+
<Spinner />
|
|
75
|
+
<Box marginLeft={3}><Text size={1}>Loading options...</Text></Box>
|
|
76
|
+
</Flex>
|
|
77
|
+
</Card>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return (
|
|
83
|
+
<Card padding={3} tone="critical" border>
|
|
84
|
+
<Text size={1}>Error loading options: {error.message}</Text>
|
|
85
|
+
</Card>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (objects.length === 0) {
|
|
90
|
+
return (
|
|
91
|
+
<Card padding={3} tone="transparent" border>
|
|
92
|
+
<Text size={1} muted>{emptyMessage}</Text>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Card padding={0} border ref={ref}>
|
|
99
|
+
{objects.length > 5 && (
|
|
100
|
+
<Box padding={3} style={{ borderBottom: '1px solid var(--card-border-color)' }}>
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
placeholder={searchPlaceholder}
|
|
104
|
+
value={searchTerm}
|
|
105
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
106
|
+
style={{ width: '100%', padding: '8px 12px', border: '1px solid var(--card-border-color)', borderRadius: '4px', fontSize: '13px', fontFamily: 'inherit' }}
|
|
107
|
+
/>
|
|
108
|
+
</Box>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
<Stack space={0}>
|
|
112
|
+
{filteredObjects.length === 0 ? (
|
|
113
|
+
<Box padding={3}>
|
|
114
|
+
<Text size={1} muted>No results found for "{searchTerm}"</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
) : (
|
|
117
|
+
filteredObjects.map((obj, index) => {
|
|
118
|
+
const isSelected = value?.includes(obj.value);
|
|
119
|
+
const isLast = index === filteredObjects.length - 1;
|
|
120
|
+
return (
|
|
121
|
+
<Box
|
|
122
|
+
key={obj.value}
|
|
123
|
+
padding={3}
|
|
124
|
+
style={{ borderBottom: isLast ? 'none' : '1px solid var(--card-border-color)', cursor: 'pointer', backgroundColor: isSelected ? 'var(--card-muted-fg-color)' : 'transparent', transition: 'background-color 0.2s' }}
|
|
125
|
+
onClick={() => handleToggle(obj.value)}
|
|
126
|
+
>
|
|
127
|
+
<Flex align="center" gap={3}>
|
|
128
|
+
<Checkbox checked={isSelected} readOnly style={{ pointerEvents: 'none' }} />
|
|
129
|
+
<Text size={1} weight={isSelected ? 'semibold' : 'regular'}>{obj.title}</Text>
|
|
130
|
+
</Flex>
|
|
131
|
+
</Box>
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
)}
|
|
135
|
+
</Stack>
|
|
136
|
+
|
|
137
|
+
{value?.length > 0 && (
|
|
138
|
+
<Box padding={2} paddingX={3} style={{ borderTop: '1px solid var(--card-border-color)', backgroundColor: 'var(--card-muted-fg-color)' }}>
|
|
139
|
+
<Text size={1} muted>{value.length} selected</Text>
|
|
140
|
+
</Box>
|
|
141
|
+
)}
|
|
142
|
+
</Card>
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
NestedObjectArraySelector.displayName = 'NestedObjectArraySelector';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Hook for fetching nested objects from Sanity documents — used by NestedObjectArraySelector
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useClient } from 'sanity';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetches and flattens nested arrays from a Sanity document type into a list of selectable items.
|
|
8
|
+
* @param {Object} config
|
|
9
|
+
* @param {string} config.sourceType - Document type to query (e.g. 'licenseGroup')
|
|
10
|
+
* @param {string} config.nestedField - Array field to extract from (e.g. 'sections')
|
|
11
|
+
* @param {string} config.titleField - GROQ expression for display text (e.g. 'title')
|
|
12
|
+
* @param {string} config.valueField - GROQ expression for stored value (e.g. 'slug.current')
|
|
13
|
+
* @param {string} [config.filter] - Optional GROQ filter clause (e.g. 'state == "published"')
|
|
14
|
+
* @param {string} [config.sortBy] - Optional sort field and direction (e.g. 'title asc')
|
|
15
|
+
* @returns {{ objects: Array, loading: boolean, error: Error|null }}
|
|
16
|
+
*/
|
|
17
|
+
export function useNestedObjects({
|
|
18
|
+
sourceType,
|
|
19
|
+
nestedField,
|
|
20
|
+
titleField,
|
|
21
|
+
valueField,
|
|
22
|
+
filter = '',
|
|
23
|
+
sortBy = '',
|
|
24
|
+
}) {
|
|
25
|
+
const client = useClient({ apiVersion: '2023-01-01' });
|
|
26
|
+
const [objects, setObjects] = useState([]);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!sourceType || !nestedField || !titleField || !valueField) {
|
|
32
|
+
setError(new Error('Missing required configuration'));
|
|
33
|
+
setLoading(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fetchData = async () => {
|
|
38
|
+
try {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
|
|
42
|
+
const filterClause = filter ? ` && ${filter}` : '';
|
|
43
|
+
const query = `
|
|
44
|
+
*[_type == "${sourceType}"${filterClause}] {
|
|
45
|
+
"${nestedField}": ${nestedField}[] {
|
|
46
|
+
"title": ${titleField},
|
|
47
|
+
"value": ${valueField}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const result = await client.fetch(query);
|
|
53
|
+
|
|
54
|
+
if (!result || result.length === 0) {
|
|
55
|
+
setObjects([]);
|
|
56
|
+
setLoading(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Flatten nested arrays from all documents and deduplicate by value
|
|
61
|
+
const flattened = result.flatMap(doc => doc[nestedField] || []);
|
|
62
|
+
const uniqueMap = new Map();
|
|
63
|
+
flattened.forEach(item => {
|
|
64
|
+
if (item.value && item.title) uniqueMap.set(item.value, item);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let unique = Array.from(uniqueMap.values());
|
|
68
|
+
|
|
69
|
+
if (sortBy) {
|
|
70
|
+
const [sortField, sortOrder = 'asc'] = sortBy.split(' ');
|
|
71
|
+
unique = unique.sort((a, b) => {
|
|
72
|
+
const aVal = a[sortField] || a.title;
|
|
73
|
+
const bVal = b[sortField] || b.title;
|
|
74
|
+
const comparison = aVal.localeCompare(bVal);
|
|
75
|
+
return sortOrder === 'desc' ? -comparison : comparison;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setObjects(unique);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('useNestedObjects fetch error:', err);
|
|
82
|
+
setError(err);
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
fetchData();
|
|
89
|
+
}, [sourceType, nestedField, titleField, valueField, filter, sortBy, client]);
|
|
90
|
+
|
|
91
|
+
return { objects, loading, error };
|
|
92
|
+
}
|
package/src/index.js
CHANGED
|
@@ -17,9 +17,11 @@ export { VariableInstanceReferencesInput } from './components/VariableInstanceRe
|
|
|
17
17
|
export { PrimaryCollectionGeneratorTypeface } from './components/PrimaryCollectionGeneratorTypeface.jsx';
|
|
18
18
|
export { SetOTF } from './components/SetOTF.jsx';
|
|
19
19
|
export { StyleCountInput } from './components/StyleCountInput.jsx';
|
|
20
|
+
export { NestedObjectArraySelector } from './components/NestedObjectArraySelector.jsx';
|
|
20
21
|
|
|
21
22
|
// Hooks
|
|
22
23
|
export { useSanityClient } from './hooks/useSanityClient.js';
|
|
24
|
+
export { useNestedObjects } from './hooks/useNestedObjects.js';
|
|
23
25
|
|
|
24
26
|
// Core utilities
|
|
25
27
|
export { default as generateCssFile } from './utils/generateCssFile.js';
|
|
@@ -28,7 +30,7 @@ export { default as generateFontFile } from './utils/generateFontFile.js';
|
|
|
28
30
|
export { default as generateSubset } from './utils/generateSubset.js';
|
|
29
31
|
export { default as parseVariableFontInstances } from './utils/parseVariableFontInstances.js';
|
|
30
32
|
export { getEmptyFontKit } from './utils/getEmptyFontKit.js';
|
|
31
|
-
export { SCRIPTS, SCRIPTS_OBJECT, HtmlDescription } from './utils/utils.js';
|
|
33
|
+
export { SCRIPTS, SCRIPTS_OBJECT, HtmlDescription, DISCOUNT_REQUIREMENT_TYPES, DISCOUNT_REQUIREMENT_TYPES_OBJECT } from './utils/utils.js';
|
|
32
34
|
|
|
33
35
|
// Font processing utilities
|
|
34
36
|
export {
|
package/src/utils/utils.js
CHANGED
|
@@ -14,3 +14,11 @@ export const SCRIPTS = (process.env.SANITY_STUDIO_SCRIPTS || '').split(',').map(
|
|
|
14
14
|
export const SCRIPTS_OBJECT = SCRIPTS.map((script) => {
|
|
15
15
|
return {title: script[0].toUpperCase() + script.slice(1), value: script}
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
/** Discount requirement types — comma-separated SANITY_STUDIO_DISCOUNT_REQ_TYPES env var */
|
|
19
|
+
export const DISCOUNT_REQUIREMENT_TYPES = (process.env.SANITY_STUDIO_DISCOUNT_REQ_TYPES || '').split(',').map((type) => type.trim()).filter(Boolean);
|
|
20
|
+
|
|
21
|
+
/** DISCOUNT_REQUIREMENT_TYPES as Sanity select option objects */
|
|
22
|
+
export const DISCOUNT_REQUIREMENT_TYPES_OBJECT = DISCOUNT_REQUIREMENT_TYPES.map((type) => {
|
|
23
|
+
return {title: type[0].toUpperCase() + type.slice(1), value: type}
|
|
24
|
+
});
|