@liiift-studio/sanity-font-manager 2.3.18 → 2.4.0
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/README.md +437 -437
- package/dist/index.js +103 -48
- package/dist/index.mjs +103 -48
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +640 -639
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +673 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadScriptsComponent.jsx +537 -537
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +70 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/generateCssFile.js +205 -205
- package/src/utils/generateFontData.js +145 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +99 -99
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/processFontFiles.js +487 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +115 -26
- package/src/utils/utils.js +24 -24
|
@@ -1,190 +1,190 @@
|
|
|
1
|
-
// Font-specific wrapper around KeyValueReferenceInput — maps variable font instances to static font documents with autofill
|
|
2
|
-
|
|
3
|
-
import React, { useState, useCallback } from 'react';
|
|
4
|
-
import { Button, Flex, Dialog, Box, Stack, Text } from '@sanity/ui';
|
|
5
|
-
import { SyncIcon, DocumentTextIcon } from '@sanity/icons';
|
|
6
|
-
import { set, useFormValue } from 'sanity';
|
|
7
|
-
import { KeyValueReferenceInput } from './KeyValueReferenceInput.jsx';
|
|
8
|
-
import { useSanityClient } from '../hooks/useSanityClient.js';
|
|
9
|
-
import { parseVariableFontInstances } from '../utils/parseVariableFontInstances.js';
|
|
10
|
-
import { nanoid } from 'nanoid';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Wraps KeyValueReferenceInput with font-specific autofill for variable font instance mapping.
|
|
14
|
-
* Autofill with Matching calls parseVariableFontInstances to resolve instance names to static fonts.
|
|
15
|
-
* Autofill Keys Only populates keys from variableInstances JSON without reference matching.
|
|
16
|
-
* @param {Array} value - Current array of { _key, key, value } pairs
|
|
17
|
-
* @param {Function} onChange - Sanity onChange callback
|
|
18
|
-
* @param {Object} props - Remaining props forwarded to KeyValueReferenceInput
|
|
19
|
-
*/
|
|
20
|
-
export function VariableInstanceReferencesInput(props) {
|
|
21
|
-
const { value = [], onChange } = props;
|
|
22
|
-
|
|
23
|
-
const [isAutofilling, setIsAutofilling] = useState(false);
|
|
24
|
-
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
25
|
-
const [pendingAction, setPendingAction] = useState(null);
|
|
26
|
-
|
|
27
|
-
const formDocument = useFormValue([]);
|
|
28
|
-
const sanityClient = useSanityClient();
|
|
29
|
-
|
|
30
|
-
/** Fetches static (non-variable) fonts scoped to the same typeface for the reference picker */
|
|
31
|
-
const fetchReferences = useCallback(async (client, doc) => {
|
|
32
|
-
const typefaceName = doc?.typefaceName;
|
|
33
|
-
if (!typefaceName) {
|
|
34
|
-
return client.fetch(`*[_type == 'font' && variableFont != true]{_id, title}`, {});
|
|
35
|
-
}
|
|
36
|
-
return client.fetch(
|
|
37
|
-
`*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{_id, title}`,
|
|
38
|
-
{ typefaceName }
|
|
39
|
-
);
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
/** Runs parseVariableFontInstances to match instance names to static font documents */
|
|
43
|
-
const performAutofillWithMatching = useCallback(async (mode) => {
|
|
44
|
-
setIsAutofilling(true);
|
|
45
|
-
try {
|
|
46
|
-
if (!formDocument?.variableInstances) {
|
|
47
|
-
console.warn('Cannot autofill: no variableInstances data on this document');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const mappings = await parseVariableFontInstances(formDocument, sanityClient);
|
|
51
|
-
if (mappings.length === 0) {
|
|
52
|
-
console.warn('No variable instances could be parsed from this font');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const updatedPairs = mode === 'replace'
|
|
56
|
-
? mappings
|
|
57
|
-
: [...value, ...mappings.filter(m => !value.some(p => p.key === m.key))];
|
|
58
|
-
onChange(set(updatedPairs));
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error('Error during autofill with matching:', err);
|
|
61
|
-
} finally {
|
|
62
|
-
setIsAutofilling(false);
|
|
63
|
-
}
|
|
64
|
-
}, [formDocument, sanityClient, value, onChange]);
|
|
65
|
-
|
|
66
|
-
/** Populates key names from variableInstances JSON without reference matching */
|
|
67
|
-
const performAutofillKeysOnly = useCallback(async (mode) => {
|
|
68
|
-
setIsAutofilling(true);
|
|
69
|
-
try {
|
|
70
|
-
if (!formDocument?.variableInstances) {
|
|
71
|
-
console.warn('Cannot autofill: no variableInstances data on this document');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
let instances;
|
|
75
|
-
try {
|
|
76
|
-
instances = JSON.parse(formDocument.variableInstances);
|
|
77
|
-
} catch {
|
|
78
|
-
console.error('Invalid variableInstances JSON on this document');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const keys = Object.keys(instances);
|
|
82
|
-
if (keys.length === 0) {
|
|
83
|
-
console.warn('No variable instances found in JSON');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const keyOnlyPairs = keys.map(key => ({ key, value: null, _key: nanoid() }));
|
|
87
|
-
const updatedPairs = mode === 'replace'
|
|
88
|
-
? keyOnlyPairs
|
|
89
|
-
: [...value, ...keyOnlyPairs.filter(p => !value.some(existing => existing.key === p.key))];
|
|
90
|
-
onChange(set(updatedPairs));
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error('Error during keys-only autofill:', err);
|
|
93
|
-
} finally {
|
|
94
|
-
setIsAutofilling(false);
|
|
95
|
-
}
|
|
96
|
-
}, [formDocument, value, onChange]);
|
|
97
|
-
|
|
98
|
-
/** Triggers autofill with matching — prompts confirmation if pairs already exist */
|
|
99
|
-
const handleAutofillWithMatching = useCallback(() => {
|
|
100
|
-
if (value.length > 0) {
|
|
101
|
-
setPendingAction('matching');
|
|
102
|
-
setShowConfirmDialog(true);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
performAutofillWithMatching('replace');
|
|
106
|
-
}, [value, performAutofillWithMatching]);
|
|
107
|
-
|
|
108
|
-
/** Triggers keys-only autofill — prompts confirmation if pairs already exist */
|
|
109
|
-
const handleAutofillKeysOnly = useCallback(() => {
|
|
110
|
-
if (value.length > 0) {
|
|
111
|
-
setPendingAction('keysOnly');
|
|
112
|
-
setShowConfirmDialog(true);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
performAutofillKeysOnly('replace');
|
|
116
|
-
}, [value, performAutofillKeysOnly]);
|
|
117
|
-
|
|
118
|
-
/** Handles the replace/merge choice from the confirmation dialog */
|
|
119
|
-
const handleConfirmChoice = useCallback(async (choice) => {
|
|
120
|
-
setShowConfirmDialog(false);
|
|
121
|
-
if (pendingAction === 'matching') await performAutofillWithMatching(choice);
|
|
122
|
-
else if (pendingAction === 'keysOnly') await performAutofillKeysOnly(choice);
|
|
123
|
-
setPendingAction(null);
|
|
124
|
-
}, [pendingAction, performAutofillWithMatching, performAutofillKeysOnly]);
|
|
125
|
-
|
|
126
|
-
/** Cancels the confirmation dialog */
|
|
127
|
-
const handleConfirmCancel = useCallback(() => {
|
|
128
|
-
setShowConfirmDialog(false);
|
|
129
|
-
setPendingAction(null);
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
// Only show autofill buttons when the document is a variable font with parsed instance data
|
|
133
|
-
const showAutofill = !!(formDocument?.variableFont && formDocument?.variableInstances);
|
|
134
|
-
|
|
135
|
-
const topActions = showAutofill ? (
|
|
136
|
-
<Flex gap={2}>
|
|
137
|
-
<Button
|
|
138
|
-
tone="primary"
|
|
139
|
-
mode="ghost"
|
|
140
|
-
onClick={handleAutofillWithMatching}
|
|
141
|
-
icon={SyncIcon}
|
|
142
|
-
text="Autofill with Matching"
|
|
143
|
-
disabled={isAutofilling}
|
|
144
|
-
loading={isAutofilling}
|
|
145
|
-
/>
|
|
146
|
-
<Button
|
|
147
|
-
tone="default"
|
|
148
|
-
mode="ghost"
|
|
149
|
-
onClick={handleAutofillKeysOnly}
|
|
150
|
-
icon={DocumentTextIcon}
|
|
151
|
-
text="Autofill Keys Only"
|
|
152
|
-
disabled={isAutofilling}
|
|
153
|
-
loading={isAutofilling}
|
|
154
|
-
/>
|
|
155
|
-
</Flex>
|
|
156
|
-
) : null;
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<>
|
|
160
|
-
<KeyValueReferenceInput
|
|
161
|
-
{...props}
|
|
162
|
-
referenceType="font"
|
|
163
|
-
fetchReferences={fetchReferences}
|
|
164
|
-
topActions={topActions}
|
|
165
|
-
/>
|
|
166
|
-
|
|
167
|
-
{showConfirmDialog && (
|
|
168
|
-
<Dialog
|
|
169
|
-
header="Existing entries found"
|
|
170
|
-
id="autofill-confirm-dialog"
|
|
171
|
-
onClose={handleConfirmCancel}
|
|
172
|
-
width={1}
|
|
173
|
-
>
|
|
174
|
-
<Box padding={4}>
|
|
175
|
-
<Stack space={4}>
|
|
176
|
-
<Text>
|
|
177
|
-
You already have {value.length} {value.length === 1 ? 'entry' : 'entries'}. How would you like to proceed?
|
|
178
|
-
</Text>
|
|
179
|
-
<Flex gap={2} justify="flex-end">
|
|
180
|
-
<Button text="Cancel" mode="ghost" onClick={handleConfirmCancel} />
|
|
181
|
-
<Button text="Merge (Add New)" tone="primary" mode="ghost" onClick={() => handleConfirmChoice('merge')} />
|
|
182
|
-
<Button text="Replace All" tone="critical" onClick={() => handleConfirmChoice('replace')} />
|
|
183
|
-
</Flex>
|
|
184
|
-
</Stack>
|
|
185
|
-
</Box>
|
|
186
|
-
</Dialog>
|
|
187
|
-
)}
|
|
188
|
-
</>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
1
|
+
// Font-specific wrapper around KeyValueReferenceInput — maps variable font instances to static font documents with autofill
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback } from 'react';
|
|
4
|
+
import { Button, Flex, Dialog, Box, Stack, Text } from '@sanity/ui';
|
|
5
|
+
import { SyncIcon, DocumentTextIcon } from '@sanity/icons';
|
|
6
|
+
import { set, useFormValue } from 'sanity';
|
|
7
|
+
import { KeyValueReferenceInput } from './KeyValueReferenceInput.jsx';
|
|
8
|
+
import { useSanityClient } from '../hooks/useSanityClient.js';
|
|
9
|
+
import { parseVariableFontInstances } from '../utils/parseVariableFontInstances.js';
|
|
10
|
+
import { nanoid } from 'nanoid';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps KeyValueReferenceInput with font-specific autofill for variable font instance mapping.
|
|
14
|
+
* Autofill with Matching calls parseVariableFontInstances to resolve instance names to static fonts.
|
|
15
|
+
* Autofill Keys Only populates keys from variableInstances JSON without reference matching.
|
|
16
|
+
* @param {Array} value - Current array of { _key, key, value } pairs
|
|
17
|
+
* @param {Function} onChange - Sanity onChange callback
|
|
18
|
+
* @param {Object} props - Remaining props forwarded to KeyValueReferenceInput
|
|
19
|
+
*/
|
|
20
|
+
export function VariableInstanceReferencesInput(props) {
|
|
21
|
+
const { value = [], onChange } = props;
|
|
22
|
+
|
|
23
|
+
const [isAutofilling, setIsAutofilling] = useState(false);
|
|
24
|
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
25
|
+
const [pendingAction, setPendingAction] = useState(null);
|
|
26
|
+
|
|
27
|
+
const formDocument = useFormValue([]);
|
|
28
|
+
const sanityClient = useSanityClient();
|
|
29
|
+
|
|
30
|
+
/** Fetches static (non-variable) fonts scoped to the same typeface for the reference picker */
|
|
31
|
+
const fetchReferences = useCallback(async (client, doc) => {
|
|
32
|
+
const typefaceName = doc?.typefaceName;
|
|
33
|
+
if (!typefaceName) {
|
|
34
|
+
return client.fetch(`*[_type == 'font' && variableFont != true]{_id, title}`, {});
|
|
35
|
+
}
|
|
36
|
+
return client.fetch(
|
|
37
|
+
`*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{_id, title}`,
|
|
38
|
+
{ typefaceName }
|
|
39
|
+
);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
/** Runs parseVariableFontInstances to match instance names to static font documents */
|
|
43
|
+
const performAutofillWithMatching = useCallback(async (mode) => {
|
|
44
|
+
setIsAutofilling(true);
|
|
45
|
+
try {
|
|
46
|
+
if (!formDocument?.variableInstances) {
|
|
47
|
+
console.warn('Cannot autofill: no variableInstances data on this document');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const mappings = await parseVariableFontInstances(formDocument, sanityClient);
|
|
51
|
+
if (mappings.length === 0) {
|
|
52
|
+
console.warn('No variable instances could be parsed from this font');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const updatedPairs = mode === 'replace'
|
|
56
|
+
? mappings
|
|
57
|
+
: [...value, ...mappings.filter(m => !value.some(p => p.key === m.key))];
|
|
58
|
+
onChange(set(updatedPairs));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Error during autofill with matching:', err);
|
|
61
|
+
} finally {
|
|
62
|
+
setIsAutofilling(false);
|
|
63
|
+
}
|
|
64
|
+
}, [formDocument, sanityClient, value, onChange]);
|
|
65
|
+
|
|
66
|
+
/** Populates key names from variableInstances JSON without reference matching */
|
|
67
|
+
const performAutofillKeysOnly = useCallback(async (mode) => {
|
|
68
|
+
setIsAutofilling(true);
|
|
69
|
+
try {
|
|
70
|
+
if (!formDocument?.variableInstances) {
|
|
71
|
+
console.warn('Cannot autofill: no variableInstances data on this document');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let instances;
|
|
75
|
+
try {
|
|
76
|
+
instances = JSON.parse(formDocument.variableInstances);
|
|
77
|
+
} catch {
|
|
78
|
+
console.error('Invalid variableInstances JSON on this document');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const keys = Object.keys(instances);
|
|
82
|
+
if (keys.length === 0) {
|
|
83
|
+
console.warn('No variable instances found in JSON');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const keyOnlyPairs = keys.map(key => ({ key, value: null, _key: nanoid() }));
|
|
87
|
+
const updatedPairs = mode === 'replace'
|
|
88
|
+
? keyOnlyPairs
|
|
89
|
+
: [...value, ...keyOnlyPairs.filter(p => !value.some(existing => existing.key === p.key))];
|
|
90
|
+
onChange(set(updatedPairs));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('Error during keys-only autofill:', err);
|
|
93
|
+
} finally {
|
|
94
|
+
setIsAutofilling(false);
|
|
95
|
+
}
|
|
96
|
+
}, [formDocument, value, onChange]);
|
|
97
|
+
|
|
98
|
+
/** Triggers autofill with matching — prompts confirmation if pairs already exist */
|
|
99
|
+
const handleAutofillWithMatching = useCallback(() => {
|
|
100
|
+
if (value.length > 0) {
|
|
101
|
+
setPendingAction('matching');
|
|
102
|
+
setShowConfirmDialog(true);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
performAutofillWithMatching('replace');
|
|
106
|
+
}, [value, performAutofillWithMatching]);
|
|
107
|
+
|
|
108
|
+
/** Triggers keys-only autofill — prompts confirmation if pairs already exist */
|
|
109
|
+
const handleAutofillKeysOnly = useCallback(() => {
|
|
110
|
+
if (value.length > 0) {
|
|
111
|
+
setPendingAction('keysOnly');
|
|
112
|
+
setShowConfirmDialog(true);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
performAutofillKeysOnly('replace');
|
|
116
|
+
}, [value, performAutofillKeysOnly]);
|
|
117
|
+
|
|
118
|
+
/** Handles the replace/merge choice from the confirmation dialog */
|
|
119
|
+
const handleConfirmChoice = useCallback(async (choice) => {
|
|
120
|
+
setShowConfirmDialog(false);
|
|
121
|
+
if (pendingAction === 'matching') await performAutofillWithMatching(choice);
|
|
122
|
+
else if (pendingAction === 'keysOnly') await performAutofillKeysOnly(choice);
|
|
123
|
+
setPendingAction(null);
|
|
124
|
+
}, [pendingAction, performAutofillWithMatching, performAutofillKeysOnly]);
|
|
125
|
+
|
|
126
|
+
/** Cancels the confirmation dialog */
|
|
127
|
+
const handleConfirmCancel = useCallback(() => {
|
|
128
|
+
setShowConfirmDialog(false);
|
|
129
|
+
setPendingAction(null);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Only show autofill buttons when the document is a variable font with parsed instance data
|
|
133
|
+
const showAutofill = !!(formDocument?.variableFont && formDocument?.variableInstances);
|
|
134
|
+
|
|
135
|
+
const topActions = showAutofill ? (
|
|
136
|
+
<Flex gap={2}>
|
|
137
|
+
<Button
|
|
138
|
+
tone="primary"
|
|
139
|
+
mode="ghost"
|
|
140
|
+
onClick={handleAutofillWithMatching}
|
|
141
|
+
icon={SyncIcon}
|
|
142
|
+
text="Autofill with Matching"
|
|
143
|
+
disabled={isAutofilling}
|
|
144
|
+
loading={isAutofilling}
|
|
145
|
+
/>
|
|
146
|
+
<Button
|
|
147
|
+
tone="default"
|
|
148
|
+
mode="ghost"
|
|
149
|
+
onClick={handleAutofillKeysOnly}
|
|
150
|
+
icon={DocumentTextIcon}
|
|
151
|
+
text="Autofill Keys Only"
|
|
152
|
+
disabled={isAutofilling}
|
|
153
|
+
loading={isAutofilling}
|
|
154
|
+
/>
|
|
155
|
+
</Flex>
|
|
156
|
+
) : null;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<KeyValueReferenceInput
|
|
161
|
+
{...props}
|
|
162
|
+
referenceType="font"
|
|
163
|
+
fetchReferences={fetchReferences}
|
|
164
|
+
topActions={topActions}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
{showConfirmDialog && (
|
|
168
|
+
<Dialog
|
|
169
|
+
header="Existing entries found"
|
|
170
|
+
id="autofill-confirm-dialog"
|
|
171
|
+
onClose={handleConfirmCancel}
|
|
172
|
+
width={1}
|
|
173
|
+
>
|
|
174
|
+
<Box padding={4}>
|
|
175
|
+
<Stack space={4}>
|
|
176
|
+
<Text>
|
|
177
|
+
You already have {value.length} {value.length === 1 ? 'entry' : 'entries'}. How would you like to proceed?
|
|
178
|
+
</Text>
|
|
179
|
+
<Flex gap={2} justify="flex-end">
|
|
180
|
+
<Button text="Cancel" mode="ghost" onClick={handleConfirmCancel} />
|
|
181
|
+
<Button text="Merge (Add New)" tone="primary" mode="ghost" onClick={() => handleConfirmChoice('merge')} />
|
|
182
|
+
<Button text="Replace All" tone="critical" onClick={() => handleConfirmChoice('replace')} />
|
|
183
|
+
</Flex>
|
|
184
|
+
</Stack>
|
|
185
|
+
</Box>
|
|
186
|
+
</Dialog>
|
|
187
|
+
)}
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -1,92 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Returns the Sanity client instance from the studio context
|
|
2
|
-
import {useMemo} from 'react'
|
|
3
|
-
import {useClient} from 'sanity'
|
|
4
|
-
|
|
5
|
-
/** Returns a memoized Sanity client pinned to api version 2021-10-23 */
|
|
6
|
-
export function useSanityClient() {
|
|
7
|
-
const client = useClient({apiVersion: '2021-10-23'})
|
|
8
|
-
return useMemo(() => client, [client])
|
|
9
|
-
}
|
|
1
|
+
// Returns the Sanity client instance from the studio context
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
import {useClient} from 'sanity'
|
|
4
|
+
|
|
5
|
+
/** Returns a memoized Sanity client pinned to api version 2021-10-23 */
|
|
6
|
+
export function useSanityClient() {
|
|
7
|
+
const client = useClient({apiVersion: '2021-10-23'})
|
|
8
|
+
return useMemo(() => client, [client])
|
|
9
|
+
}
|