@liiift-studio/sanity-utilities 1.0.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 ADDED
@@ -0,0 +1,102 @@
1
+ # @liiift-studio/sanity-utilities
2
+
3
+ Centralized Sanity utilities package for Liiift Studio foundry projects.
4
+
5
+ ## Overview
6
+
7
+ This package provides a collection of powerful utilities for Sanity Studio that help manage and manipulate content across foundry projects. All utilities are designed with safety features including danger mode locks and confirmation dialogs for destructive operations.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @liiift-studio/sanity-utilities
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Basic Usage
18
+
19
+ Import and use the utilities desk in your Sanity config:
20
+
21
+ ```js
22
+ import { UtilitiesDesk } from '@liiift-studio/sanity-utilities';
23
+
24
+ // In your sanity.config.js desk structure
25
+ S.listItem()
26
+ .title('Utilities')
27
+ .child(
28
+ S.component(UtilitiesDesk).title('Utilities')
29
+ )
30
+ ```
31
+
32
+ ### Available Components
33
+
34
+ All utility components are exported individually if you need custom implementations:
35
+
36
+ ```js
37
+ import {
38
+ ConvertIdsToSlug,
39
+ ConvertToWeakReferences,
40
+ DangerModeWarning,
41
+ DeleteUnusedAssets,
42
+ DuplicateAndRename,
43
+ ExportData,
44
+ GetFontData,
45
+ SearchAddData,
46
+ SearchAndDelete,
47
+ UtilitiesDesk
48
+ } from '@liiift-studio/sanity-utilities';
49
+ ```
50
+
51
+ ## Utilities Included
52
+
53
+ ### 1. Convert IDs to Slug
54
+ Converts font document IDs from auto-generated to slug-based IDs, updating all references across your content.
55
+
56
+ ### 2. Convert to Weak References
57
+ Transforms strong document references into weak references across matching documents.
58
+
59
+ ### 3. Delete Unused Assets
60
+ Scans for unreferenced image and file assets and allows bulk deletion with preview thumbnails.
61
+
62
+ ### 4. Duplicate and Rename
63
+ Copies field values to new field names across all documents of a specific type.
64
+
65
+ ### 5. Export Data
66
+ Exports documents to CSV or JSON formats with optional reference population.
67
+
68
+ ### 6. Get Font Data
69
+ Analyzes uploaded font files using fontkit, displaying metadata and OpenType properties.
70
+
71
+ ### 7. Search & Add Data
72
+ Bulk add or replace field data with multiple modes:
73
+ - Full Replace
74
+ - Find & Replace
75
+ - Prepend
76
+ - Append
77
+ - Transform (toLowerCase, toUpperCase, trim)
78
+
79
+ ### 8. Search & Delete
80
+ Searches for and deletes documents matching specific criteria with confirmation dialogs.
81
+
82
+ ## Safety Features
83
+
84
+ - **Danger Mode Locks**: Destructive operations require unlocking danger mode
85
+ - **Warning Modals**: 48-hour suppression option for danger mode warnings
86
+ - **Confirmation Dialogs**: Double confirmation for bulk delete operations
87
+ - **Preview Features**: See what will be affected before taking action
88
+
89
+ ## Requirements
90
+
91
+ - Sanity Studio v3
92
+ - React 18+
93
+ - @sanity/ui v1 or v2
94
+ - @sanity/icons v2 or v3
95
+
96
+ ## License
97
+
98
+ UNLICENSED - Internal use only for Liiift Studio projects
99
+
100
+ ## Support
101
+
102
+ For issues or questions, please contact the Liiift Studio development team.
package/SETUP.md ADDED
@@ -0,0 +1,166 @@
1
+ # Setup & Publishing Guide
2
+
3
+ ## Initial Setup
4
+
5
+ ### 1. Install Dependencies
6
+
7
+ This package has no production dependencies (only peer dependencies). No installation needed.
8
+
9
+ ### 2. Configure NPM Authentication
10
+
11
+ Create or update `.npmrc` in your project root:
12
+
13
+ ```
14
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
15
+ ```
16
+
17
+ Set the NPM_TOKEN environment variable with your npm authentication token.
18
+
19
+ ### 3. Verify Package Contents
20
+
21
+ Check what will be published:
22
+
23
+ ```bash
24
+ npm pack --dry-run
25
+ ```
26
+
27
+ ## Publishing
28
+
29
+ ### Version Bump
30
+
31
+ Follow semantic versioning:
32
+
33
+ ```bash
34
+ # Patch release (bug fixes)
35
+ npm version patch
36
+
37
+ # Minor release (new features, backward compatible)
38
+ npm version minor
39
+
40
+ # Major release (breaking changes)
41
+ npm version major
42
+ ```
43
+
44
+ ### Publish to NPM
45
+
46
+ ```bash
47
+ npm publish
48
+ ```
49
+
50
+ ## Integration in Consumer Projects
51
+
52
+ ### 1. Install the Package
53
+
54
+ In your Sanity project (e.g., Darden-Studio):
55
+
56
+ ```bash
57
+ npm install @liiift-studio/sanity-utilities
58
+ ```
59
+
60
+ ### 2. Update sanity.config.js
61
+
62
+ Import and use the utilities desk:
63
+
64
+ ```js
65
+ import { UtilitiesDesk } from '@liiift-studio/sanity-utilities';
66
+
67
+ export default defineConfig({
68
+ // ... other config
69
+
70
+ structure: (S) =>
71
+ S.list()
72
+ .title('Content')
73
+ .items([
74
+ // ... other list items
75
+
76
+ S.listItem()
77
+ .title('Utilities')
78
+ .child(
79
+ S.component(UtilitiesDesk).title('Utilities')
80
+ ),
81
+ ]),
82
+ });
83
+ ```
84
+
85
+ ### 3. Remove Old Utility Files (if migrating)
86
+
87
+ After confirming the package works, remove the old utility files:
88
+
89
+ ```bash
90
+ rm -rf sanity/schemas/components/utilities/
91
+ ```
92
+
93
+ Update any imports that referenced the old location.
94
+
95
+ ## Updating the Package
96
+
97
+ ### Making Changes
98
+
99
+ 1. Make your changes to the component files
100
+ 2. Test in a consumer project (use `npm link` for local testing)
101
+ 3. Bump the version: `npm version patch|minor|major`
102
+ 4. Publish: `npm publish`
103
+
104
+ ### Testing Locally Before Publishing
105
+
106
+ In the package directory:
107
+
108
+ ```bash
109
+ npm link
110
+ ```
111
+
112
+ In the consumer project:
113
+
114
+ ```bash
115
+ npm link @liiift-studio/sanity-utilities
116
+ ```
117
+
118
+ Test your changes, then unlink:
119
+
120
+ ```bash
121
+ npm unlink @liiift-studio/sanity-utilities
122
+ npm install @liiift-studio/sanity-utilities
123
+ ```
124
+
125
+ ## Updating Consumer Projects
126
+
127
+ After publishing a new version:
128
+
129
+ ```bash
130
+ npm install @liiift-studio/sanity-utilities@latest
131
+ ```
132
+
133
+ Or update the version in package.json and run:
134
+
135
+ ```bash
136
+ npm install
137
+ ```
138
+
139
+ ## Troubleshooting
140
+
141
+ ### Package Not Found
142
+
143
+ Ensure you're authenticated with npm and have access to the @liiift-studio scope.
144
+
145
+ ### Import Errors
146
+
147
+ Check that peer dependencies are installed in the consumer project:
148
+ - @sanity/icons
149
+ - @sanity/ui
150
+ - react
151
+ - sanity
152
+
153
+ ### Component Not Rendering
154
+
155
+ Verify the imports match the exports in index.js. Use named imports:
156
+
157
+ ```js
158
+ import { UtilitiesDesk } from '@liiift-studio/sanity-utilities';
159
+ ```
160
+
161
+ Not default imports:
162
+
163
+ ```js
164
+ // ❌ Wrong
165
+ import UtilitiesDesk from '@liiift-studio/sanity-utilities';
166
+ ```
@@ -0,0 +1,151 @@
1
+ // Component for converting document IDs to slug-based IDs
2
+ import { Stack, Grid, Heading, Text, Button, Select } from '@sanity/ui'
3
+ import { LockIcon, UnlockIcon } from '@sanity/icons'
4
+ import { useState, useEffect } from 'react'
5
+ import DangerModeWarning, { shouldShowDangerWarning } from './DangerModeWarning'
6
+
7
+ /**
8
+ * Convert IDs to Slug Component
9
+ * Converts document IDs to slug-based IDs for better URL handling
10
+ * @param {Object} props - Component props
11
+ * @param {SanityClient} props.client - Sanity client instance
12
+ */
13
+ const ConvertIdsToSlug = (props) => {
14
+ const {client} = props;
15
+ const [typefaces, setTypefaces] = useState([]);
16
+ const [targetTypeface, setTargetTypeface] = useState('');
17
+ const [dangerMode, setDangerMode] = useState(false);
18
+ const [convertMessage, setConvertMessage] = useState('');
19
+ const [showWarningModal, setShowWarningModal] = useState(false);
20
+
21
+ /**
22
+ * Handle danger mode toggle with warning modal
23
+ */
24
+ const handleDangerModeToggle = () => {
25
+ if (!dangerMode && shouldShowDangerWarning()) {
26
+ // Trying to enable danger mode, show warning
27
+ setShowWarningModal(true);
28
+ } else {
29
+ // Either disabling danger mode or warning is suppressed
30
+ setDangerMode(!dangerMode);
31
+ }
32
+ };
33
+
34
+ const handleWarningConfirm = () => {
35
+ setShowWarningModal(false);
36
+ setDangerMode(true);
37
+ };
38
+
39
+ const handleWarningCancel = () => {
40
+ setShowWarningModal(false);
41
+ };
42
+
43
+ async function getTypefaces(){
44
+ let typefaces = await client.fetch(`*[_type == "typeface" && !(_id in path('drafts.**'))]`);
45
+ setTypefaces(typefaces);
46
+ }
47
+
48
+ useEffect(() => {
49
+ getTypefaces();
50
+ }, [])
51
+
52
+ async function updateIdsToSlug(){
53
+ console.log(`Scanning ${targetTypeface}`)
54
+
55
+ let typeface = await client.fetch(`*[_type == "typeface" && title match "${targetTypeface}*"][0]`);
56
+ let typefaceIds = [];
57
+ typeface.styles.fonts.forEach(font => {
58
+ typefaceIds.push(font._ref);
59
+ });
60
+ console.log('Target Ids:', typefaceIds);
61
+
62
+ for await (let [index, id] of typefaceIds.entries()) {
63
+ const rootDoc = await client.fetch(`*[_id == "${id}"][0]`);
64
+ const refDocs = await client.fetch(`*[references("${id}")]`);
65
+ const slug = rootDoc?.slug?.current;
66
+
67
+ if (slug) {
68
+ console.log(`[${index}/${typefaceIds.length}] Creating new document: `, slug);
69
+ const newDoc = await client.createOrReplace({ ...rootDoc, _id: slug })
70
+ console.log("New document: ", newDoc);
71
+
72
+ for await (let [refIndex, ref] of refDocs.entries()) {
73
+ let refString = JSON.stringify(ref);
74
+ refString = refString.replaceAll(id, slug);
75
+ let refObject = JSON.parse(refString);
76
+
77
+ console.log(`[${index}/${typefaceIds.length}][${refIndex}/${refDocs.length}] Updating document: `, ref._id);
78
+ await client
79
+ .patch(ref._id)
80
+ .set(refObject)
81
+ .commit();
82
+ }
83
+
84
+ // Delete the old document
85
+ console.log("Deleting old document: ", id);
86
+ await client.delete(id)
87
+
88
+ console.log("Updated all instances of ", id, " to ", slug);
89
+
90
+ } else {
91
+ console.log('No Slug Found', rootDoc);
92
+ }
93
+ }
94
+ }
95
+
96
+ return (
97
+ <>
98
+ <DangerModeWarning
99
+ isOpen={showWarningModal}
100
+ onConfirm={handleWarningConfirm}
101
+ onCancel={handleWarningCancel}
102
+ utilityName="Convert IDs to Slug"
103
+ />
104
+
105
+ <Stack style={{paddingTop: "4em", paddingBottom: "2em", position: "relative"}}>
106
+ <Heading as="h3" size={3}>{dangerMode ? "Convert " : ""}Typeface's Fonts IDs to their own Slugs</Heading>
107
+ <Text muted size={1} style={{paddingTop: "2em", maxWidth: "calc(100% - 100px)"}}>
108
+ Migrate font document IDs from auto-generated IDs to slug-based IDs for cleaner URLs and better content management. Automatically updates all references.
109
+ </Text>
110
+ <Button
111
+ mode={dangerMode?"ghost":"bleed"}
112
+ tone="critical"
113
+ icon={dangerMode?UnlockIcon:LockIcon}
114
+ onClick={handleDangerModeToggle}
115
+ style={{cursor: "pointer", position: "absolute", bottom: "1.5em", right: "0"}}
116
+ />
117
+ </Stack>
118
+
119
+ {dangerMode && (
120
+ <Stack style={{ position: "relative" }} >
121
+ <Select
122
+ style={{
123
+ borderRadius: "3px",
124
+ }}
125
+ onChange={(event) => { setTargetTypeface(event.currentTarget.value) }}
126
+ value={typefaces ? typefaces[0] : ""}
127
+ >
128
+ {typefaces && typefaces.map((typeface, index) => (
129
+ <option key={`typeface-${index}`} value={typeface.title}>{typeface.title}</option>
130
+ ))}
131
+ </Select>
132
+ <p style={{opacity: "0.5"}}>Make sure you publish your updates first!<br/><br/></p>
133
+ <Button
134
+ flex={12}
135
+ tone="critical"
136
+ onClick={updateIdsToSlug}
137
+ text={"Convert"}
138
+ />
139
+ </Stack>
140
+ )}
141
+
142
+ {convertMessage !== "" && (
143
+ <Stack>
144
+ <p style={{padding: ".5em 0em 1em", opacity: "0.75"}} dangerouslySetInnerHTML={{__html: convertMessage}}></p>
145
+ </Stack>
146
+ )}
147
+ </>
148
+ )
149
+ }
150
+
151
+ export default ConvertIdsToSlug
@@ -0,0 +1,230 @@
1
+ // Component for converting strong references to weak references
2
+ import { Stack, Grid, Heading, Text, Button, TextInput, Select } from '@sanity/ui'
3
+ import { CollapseIcon, ExpandIcon, LockIcon, UnlockIcon } from '@sanity/icons'
4
+ import { useState, useEffect } from 'react'
5
+ import DangerModeWarning, { shouldShowDangerWarning } from './DangerModeWarning'
6
+
7
+ /**
8
+ * Convert to Weak References Component
9
+ * Converts strong references to weak references across documents
10
+ * @param {Object} props - Component props
11
+ * @param {SanityClient} props.client - Sanity client instance
12
+ */
13
+ const ConvertToWeakReferences = (props) => {
14
+ const { client } = props;
15
+ const [ convertValue, setConvertValue ] = useState('');
16
+ const [ convertible, setConvertible ] = useState([]);
17
+ const [ convertibleMessage, setConvertibleMessage ] = useState('');
18
+ const [ convertType, setConvertType ] = useState('typeface');
19
+ const [ excludeValue, setExcludeValue ] = useState('');
20
+ const [ exclude, setExclude ] = useState(false);
21
+ const [ dangerMode, setDangerMode ] = useState(false);
22
+ const [showWarningModal, setShowWarningModal] = useState(false);
23
+
24
+ /**
25
+ * Handle danger mode toggle with warning modal
26
+ */
27
+ const handleDangerModeToggle = () => {
28
+ if (!dangerMode && shouldShowDangerWarning()) {
29
+ // Trying to enable danger mode, show warning
30
+ setShowWarningModal(true);
31
+ } else {
32
+ // Either disabling danger mode or warning is suppressed
33
+ setDangerMode(!dangerMode);
34
+ }
35
+ };
36
+
37
+ const handleWarningConfirm = () => {
38
+ setShowWarningModal(false);
39
+ setDangerMode(true);
40
+ };
41
+
42
+ const handleWarningCancel = () => {
43
+ setShowWarningModal(false);
44
+ };
45
+
46
+ useEffect(() => {
47
+ if (!exclude) setExcludeValue("");
48
+ }, [exclude]);
49
+
50
+ async function searchFor(value) {
51
+ const items = await client.fetch(`
52
+ *[
53
+ _type == "${convertType}"
54
+ && title match "${value}*"
55
+ ${excludeValue !== "" ? ` && !(title match "*${excludeValue}*")` : ""}
56
+ ]
57
+ `)
58
+ setConvertible(items)
59
+ }
60
+
61
+ useEffect(() => {
62
+ searchFor(convertValue)
63
+ }, [convertValue, convertType, excludeValue])
64
+
65
+ function convert(){
66
+ setConvertibleMessage('Updating data...');
67
+ client
68
+ .fetch(`
69
+ *[
70
+ _type == "${convertType}"
71
+ && title match "${convertValue}*"
72
+ ${excludeValue !== "" ? ` && !(title match "*${excludeValue}*")` : ""}
73
+ ]
74
+ `)
75
+ .then( async (items) => {
76
+ let updateDataCount = 0;
77
+
78
+ // convert items to string
79
+ let itemsString = JSON.stringify(items);
80
+
81
+ // search for all `"_type":"reference"` and replace with `"_type":"reference","_weak":true`
82
+ itemsString = itemsString.replace(/"_type":"reference"/g, '"_type":"reference","_weak":true');
83
+
84
+ // convert string back to object
85
+ let itemsObject = JSON.parse(itemsString);
86
+
87
+ for (const item of itemsObject) {
88
+ try {
89
+ setConvertibleMessage(`Updating: ${item?.title ? item.title : item._id}`);
90
+ client.patch(item._id).set(item).commit()
91
+ } catch (e) {
92
+ console.error(e.message)
93
+ setConvertibleMessage('Error: ' + e.message);
94
+ }
95
+ await new Promise(r => setTimeout(r, 50));
96
+
97
+ updateDataCount++;
98
+ if (updateDataCount == itemsObject.length - 1) {
99
+ setConvertibleMessage('All Updated!');
100
+ setTimeout(()=>{
101
+ setConvertibleMessage("");
102
+ }, 2000)
103
+ }
104
+ }
105
+ })
106
+ .catch( (err)=>{ console.error(err) })
107
+ }
108
+
109
+ return (
110
+ <>
111
+ <DangerModeWarning
112
+ isOpen={showWarningModal}
113
+ onConfirm={handleWarningConfirm}
114
+ onCancel={handleWarningCancel}
115
+ utilityName="Convert to Weak References"
116
+ />
117
+
118
+ <Stack style={{paddingTop: "4em", paddingBottom: "2em", position: "relative"}}>
119
+ <Heading as="h3" size={3}>{dangerMode ? "Convert to " : ""}Weak References</Heading>
120
+ <Text muted size={1} style={{paddingTop: "2em", maxWidth: "calc(100% - 100px)"}}>
121
+ Transform strong document references into weak references across matching documents. Weak references don't prevent deletion and are useful for non-critical relationships.
122
+ </Text>
123
+ <div style={{position: "absolute", bottom: "1.5em", right: "0"}}>
124
+ <Button
125
+ mode={exclude?"ghost":"bleed"}
126
+ tone="positive"
127
+ icon={exclude?CollapseIcon:ExpandIcon}
128
+ onClick={() => { setExclude(!exclude) }}
129
+ style={{cursor: "pointer", marginLeft: ".5em"}}
130
+ />
131
+ <Button
132
+ mode={dangerMode?"ghost":"bleed"}
133
+ tone="critical"
134
+ icon={dangerMode?UnlockIcon:LockIcon}
135
+ onClick={handleDangerModeToggle}
136
+ style={{cursor: "pointer", marginLeft: ".5em"}}
137
+ />
138
+ </div>
139
+ </Stack>
140
+
141
+ <Stack style={{ position: "relative" }} >
142
+ <Grid columns={exclude ? [3] : [2]} gap={0}
143
+ style={{
144
+ position: "relative",
145
+ }}
146
+ >
147
+ <TextInput
148
+ style={{
149
+ borderRadius: "3px 0 0 0",
150
+ }}
151
+ onChange={(event) => { setConvertValue(event.currentTarget.value) }}
152
+ placeholder="Name"
153
+ value={convertValue}
154
+ />
155
+ {!!exclude &&
156
+ <TextInput
157
+ style={{
158
+ display: exclude ? "" : "none",
159
+ }}
160
+ onChange={(event) => { setExcludeValue(event.currentTarget.value) }}
161
+ placeholder="Excluding"
162
+ value={excludeValue}
163
+ />
164
+ }
165
+ <Select
166
+ style={{
167
+ borderRadius: "0 3px 0 0",
168
+ }}
169
+ onChange={(event) => { setConvertType(event.currentTarget.value) }}
170
+ value={convertType}
171
+ >
172
+ <option value="typeface">Typeface</option>
173
+ <option value="collection">Collection</option>
174
+ <option value="pair">Pair</option>
175
+ <option value="font">Font</option>
176
+ <option value="license">License</option>
177
+ <option value="order">Order</option>
178
+ <option value="account">Account</option>
179
+ <option value="cart">Cart</option>
180
+ <option value="page">Page</option>
181
+ <option value="blogpost">Blogpost</option>
182
+ </Select>
183
+ </Grid>
184
+ </Stack>
185
+
186
+ { convertibleMessage!="" && (
187
+ <Stack>
188
+ <p style={{padding: ".5em 0em 1em", opacity: "0.75"}} dangerouslySetInnerHTML={{__html: convertibleMessage}}></p>
189
+ </Stack>
190
+ )}
191
+
192
+ { convertible.length > 0 && (
193
+ <>
194
+ <div
195
+ style={{
196
+ maxHeight: "400px",
197
+ marginTop: "5px",
198
+ border: "1px solid rgba(255,255,255,0.1)",
199
+ overflow: "auto",
200
+ paddingBottom: "1rem",
201
+ borderRadius: "3px",
202
+ }}
203
+ >
204
+ { convertible.map((item, index) => (
205
+ <a
206
+ target="_blank"
207
+ key={`item-${index}`}
208
+ className="link"
209
+ href={`${window.location.origin}/desk/${(convertType === "typeface" || convertType === "licenseGroup") ? "orderable-" : ""}${convertType};${item._id}`}
210
+ >
211
+ <Stack>
212
+ <Text size={1} style={{padding: "1em 1em .5em"}}>{item.title}</Text>
213
+ </Stack>
214
+ </a>
215
+ ))}
216
+ </div>
217
+ <div style={{pointerEvents: "none", textAlign: "right", top: "-30px", paddingRight: "10px", position: "relative", height: "30px"}}>{ convertible.length} items</div>
218
+
219
+ {dangerMode && (
220
+ <Stack>
221
+ <Button text="Convert" tone="critical" onClick={() => { convert() }}/>
222
+ </Stack>
223
+ )}
224
+ </>
225
+ )}
226
+ </>
227
+ )
228
+ }
229
+
230
+ export default ConvertToWeakReferences