sanity-plugin-singleton-management 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/LICENSE +22 -0
- package/README.md +193 -0
- package/dist/index.cjs +55 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.esm.d.ts +40 -0
- package/dist/index.esm.js +56 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +106 -0
- package/sanity.json +8 -0
- package/src/actions.ts +14 -0
- package/src/helpers/index.ts +27 -0
- package/src/index.ts +13 -0
- package/src/newDocumentOptions.ts +19 -0
- package/src/pluginConfig.ts +14 -0
- package/src/structure/index.ts +84 -0
- package/src/types.ts +32 -0
- package/v2-incompatible.js +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RC Maples
|
|
4
|
+
Copyright (c) 2024 RD Pennell
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
[](https://github.com/rcmaples/sanity-plugin-singleton-management/actions/workflows/ci.yml) [](https://codecov.io/github/rcmaples/sanity-plugin-singleton-management)
|
|
2
|
+
|
|
3
|
+
# sanity-plugin-singleton-tools
|
|
4
|
+
|
|
5
|
+
> This is compatible with v4 and v3 of Sanity Studio.
|
|
6
|
+
|
|
7
|
+
## What does this plugin do?
|
|
8
|
+
|
|
9
|
+
This plugin adds convenience functions to reduce the overhead of creating [singleton](https://www.sanity.io/docs/studio/structure-builder-cheat-sheet#k5cd7ca204386) documents in the [Sanity Studio](https://www.sanity.io).
|
|
10
|
+
|
|
11
|
+
In short, this does the following:
|
|
12
|
+
|
|
13
|
+
- Limits a singleton document's actions to Publish, Unpublish, and Discard Changes.
|
|
14
|
+
- Removes the ability to create new versions of the singleton document in both the global Create menu and Structure.
|
|
15
|
+
- Adds simple methods for customizing the way your singletons are listed in your Studio's Structure.
|
|
16
|
+
|
|
17
|
+
## Why Use This Plugin vs. Native Sanity Approaches?
|
|
18
|
+
|
|
19
|
+
While Sanity Studio supports singleton documents through the Structure Builder API, this
|
|
20
|
+
plugin eliminates the repetitive boilerplate and provides additional safeguards:
|
|
21
|
+
|
|
22
|
+
### Native Sanity Approach
|
|
23
|
+
```js
|
|
24
|
+
// Requires manual Structure Builder configuration for each singleton
|
|
25
|
+
export const structure = (S) =>
|
|
26
|
+
S.list()
|
|
27
|
+
.title('Content')
|
|
28
|
+
.items([
|
|
29
|
+
S.listItem()
|
|
30
|
+
.title('Site Settings')
|
|
31
|
+
.child(
|
|
32
|
+
S.document()
|
|
33
|
+
.schemaType('siteSettings')
|
|
34
|
+
.documentId('siteSettings')
|
|
35
|
+
),
|
|
36
|
+
// Must manually filter out singletons from main list
|
|
37
|
+
...S.documentTypeListItems().filter(listItem =>
|
|
38
|
+
!['siteSettings'].includes(listItem.getId()))
|
|
39
|
+
])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Issues:**
|
|
43
|
+
|
|
44
|
+
- Repetitive boilerplate for each singleton
|
|
45
|
+
- No prevention of duplicate document creation
|
|
46
|
+
- Users can still create new versions via global Create menu
|
|
47
|
+
- Manual filtering required to prevent duplicates in document lists
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
**With This Plugin:**
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// Schema configuration
|
|
54
|
+
export const siteSettings = {
|
|
55
|
+
name: 'siteSettings',
|
|
56
|
+
type: 'document',
|
|
57
|
+
options: { singleton: true } // That's it!
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Structure configuration
|
|
61
|
+
export const structure = (S, context) =>
|
|
62
|
+
S.list()
|
|
63
|
+
.items([
|
|
64
|
+
...singletonDocumentListItems({ S, context }), // Auto-generates all singletons
|
|
65
|
+
...filteredDocumentListItems({ S, context }) // Auto-filters singletons from main list
|
|
66
|
+
])
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Benefits:
|
|
70
|
+
- ✅ Minimal configuration with `singleton: true`
|
|
71
|
+
- ✅ Automatic action restrictions (no duplicate creation)
|
|
72
|
+
- ✅ Global Create menu integration
|
|
73
|
+
- ✅ Helper functions eliminate boilerplate
|
|
74
|
+
- ✅ Consistent singleton behavior across your studio
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npm install sanity-plugin-singleton-management
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Migration from sanity-plugin-singleton-tools
|
|
83
|
+
|
|
84
|
+
This plugin is a modernized, actively maintained fork of `sanity-plugin-singleton-tools` with identical API compatibility. Migration is straightforward:
|
|
85
|
+
|
|
86
|
+
### Steps
|
|
87
|
+
1. **Uninstall the old package**:
|
|
88
|
+
```sh
|
|
89
|
+
npm uninstall sanity-plugin-singleton-tools
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. **Install this package**:
|
|
93
|
+
```sh
|
|
94
|
+
npm install sanity-plugin-singleton-management
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
3. **Update your import statements**:
|
|
98
|
+
```diff
|
|
99
|
+
// sanity.config.js
|
|
100
|
+
- import { singletonTools } from 'sanity-plugin-singleton-tools'
|
|
101
|
+
+ import { singletonTools } from 'sanity-plugin-singleton-management'
|
|
102
|
+
|
|
103
|
+
// structure.js
|
|
104
|
+
- import { singletonDocumentListItem } from 'sanity-plugin-singleton-tools'
|
|
105
|
+
+ import { singletonDocumentListItem } from 'sanity-plugin-singleton-management'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**That's it!** No other code changes are required. Your existing schema configurations and structure customizations will work exactly the same.
|
|
109
|
+
|
|
110
|
+
### What's New
|
|
111
|
+
- ✅ **React 18/19 Support**: Compatible with modern React versions
|
|
112
|
+
- ✅ **Sanity v3/v4 Support**: Works with both Sanity Studio versions
|
|
113
|
+
- ✅ **Modern Tooling**: ESM/CommonJS dual package, better TypeScript support
|
|
114
|
+
- ✅ **Comprehensive Tests**: 100% test coverage for reliability
|
|
115
|
+
- ✅ **Active Maintenance**: Regular updates and dependency management
|
|
116
|
+
- ✅ **Node 18+ Support**: Modern Node.js compatibility
|
|
117
|
+
|
|
118
|
+
## Usage
|
|
119
|
+
|
|
120
|
+
### 1. Add the plugin to your `sanity.config`
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
//sanity.config.js
|
|
124
|
+
import { defineConfig } from "sanity";
|
|
125
|
+
import { singletonTools } from "sanity-plugin-singleton-management";
|
|
126
|
+
|
|
127
|
+
export default defineConfig({
|
|
128
|
+
//...
|
|
129
|
+
plugins: [singletonTools()],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Configure your singleton's schema
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
//mySingleton.js
|
|
137
|
+
export const mySingleton = {
|
|
138
|
+
name: "mySingleton",
|
|
139
|
+
title: "My Singleton",
|
|
140
|
+
type: "document",
|
|
141
|
+
options: {
|
|
142
|
+
singleton: true, // Identify this document as a singleton
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 3. Customize how your singleton is shown in your Structure:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
// structure.js
|
|
151
|
+
import {
|
|
152
|
+
singletonDocumentListItem,
|
|
153
|
+
singletonDocumentListItems,
|
|
154
|
+
filteredDocumentListItems,
|
|
155
|
+
} from "sanity-plugin-singleton-management";
|
|
156
|
+
import { PlugIcon } from "@sanity/icons";
|
|
157
|
+
|
|
158
|
+
export const structure = (S, context) =>
|
|
159
|
+
S.list()
|
|
160
|
+
.title("Sanity Love Content")
|
|
161
|
+
.items([
|
|
162
|
+
// Create a list item for each singleton document in your schema that links directly to a document view
|
|
163
|
+
...singletonDocumentListItems({ S, context }),
|
|
164
|
+
// Create a list item for a specific singleton
|
|
165
|
+
singletonDocumentListItem({
|
|
166
|
+
S,
|
|
167
|
+
context,
|
|
168
|
+
// Schema type
|
|
169
|
+
type: "mySingleton",
|
|
170
|
+
// Required for showing multiple singletons of the same schema type
|
|
171
|
+
title: "My Singleton",
|
|
172
|
+
// Required for showing multiple singletons of the same schema type
|
|
173
|
+
id: "mySingleton",
|
|
174
|
+
// Specify a custom icon
|
|
175
|
+
icon: PlugIcon,
|
|
176
|
+
}),
|
|
177
|
+
S.divider(),
|
|
178
|
+
// Filter singleton documents out of the default S.documentTypeListItems() to prevent them from being rendered as lists or as duplicates
|
|
179
|
+
...filteredDocumentListItems({ S, context }),
|
|
180
|
+
]);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Appendix
|
|
184
|
+
|
|
185
|
+
Other reading (Sanity docs):
|
|
186
|
+
|
|
187
|
+
- [Singleton documents](https://www.sanity.io/docs/studio/structure-builder-cheat-sheet#k5cd7ca204386)
|
|
188
|
+
- [Filtering out singletons in Structure](https://www.sanity.io/docs/studio/create-a-link-to-a-single-edit-page-in-your-main-document-type-list#fa1e82fd32be)
|
|
189
|
+
- [Creating a link to a single edit page](https://www.sanity.io/docs/studio/create-a-link-to-a-single-edit-page-in-your-main-document-type-list)
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
[MIT](LICENSE) © RD Pennell & RC Maples
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var sanity = require("sanity"), icons = require("@sanity/icons");
|
|
4
|
+
const getSingletonDocuments = (schema) => {
|
|
5
|
+
if (schema?._original?.types)
|
|
6
|
+
return schema._original.types.filter(({ options }) => options?.singleton).map((s) => s.name);
|
|
7
|
+
}, getIsSingleton = (schema, schemaType) => !schema?._original?.types || !schemaType ? !1 : schema._original.types.find(
|
|
8
|
+
({ name }) => name === schemaType
|
|
9
|
+
)?.options?.singleton ?? !1, actions = (prev, { schema, schemaType }) => getIsSingleton(schema, schemaType) ? prev.filter(
|
|
10
|
+
({ action }) => ["publish", "discardChanges", "restore"].includes(action)
|
|
11
|
+
) : prev, newDocumentOptions = (prev, { schema, creationContext: { type, schemaType } }) => {
|
|
12
|
+
const singletons = getSingletonDocuments(schema), filterSingletons = ({ templateId }) => !singletons?.includes(templateId);
|
|
13
|
+
return type === "global" || singletons?.includes(schemaType ?? "") ? prev.filter(filterSingletons) : prev;
|
|
14
|
+
}, singletonTools = sanity.definePlugin(() => ({
|
|
15
|
+
name: "singleton-tools",
|
|
16
|
+
document: {
|
|
17
|
+
newDocumentOptions,
|
|
18
|
+
actions
|
|
19
|
+
}
|
|
20
|
+
})), singletonDocumentListItem = (config) => {
|
|
21
|
+
if (!config?.S || !config?.type || !config.context)
|
|
22
|
+
throw new Error(
|
|
23
|
+
"S, context, and type must be provided to singletonDocumentListItem. Example: singletonDocumentListItem({ S, context, type: 'product' })"
|
|
24
|
+
);
|
|
25
|
+
const { S, type, title, icon, id, context } = config, { schema } = context;
|
|
26
|
+
if (!schema)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Schema is required in context for singletonDocumentListItem"
|
|
29
|
+
);
|
|
30
|
+
const schemaType = schema.get(type), listTitle = title ?? schemaType?.title ?? type, listIcon = icon ?? schemaType?.icon ?? icons.DocumentIcon, listId = id ?? type;
|
|
31
|
+
return S.listItem().title(listTitle).icon(listIcon).child(S.document().schemaType(type).title(listTitle).id(listId));
|
|
32
|
+
}, singletonDocumentListItems = (config) => {
|
|
33
|
+
if (!config.S || !config.context)
|
|
34
|
+
throw new Error(
|
|
35
|
+
"S and context must be provided to singletonDocumentListItems. Example: singletonDocumentListItems({ S, context })"
|
|
36
|
+
);
|
|
37
|
+
const { S, context } = config, { schema } = context;
|
|
38
|
+
return getSingletonDocuments(schema)?.map(
|
|
39
|
+
(schemaType) => singletonDocumentListItem({ S, context, type: schemaType })
|
|
40
|
+
) || [];
|
|
41
|
+
}, filteredDocumentListItems = (config) => {
|
|
42
|
+
if (!config.S || !config.context)
|
|
43
|
+
throw new Error(
|
|
44
|
+
"S and context must be provided to filteredDocumentListItems. Example: filteredDocumentListItems({ S, context })"
|
|
45
|
+
);
|
|
46
|
+
const { S, context } = config, { schema } = context, singletons = getSingletonDocuments(schema);
|
|
47
|
+
return S.documentTypeListItems().filter(
|
|
48
|
+
(type) => !singletons || !singletons.includes(type.getId())
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
exports.filteredDocumentListItems = filteredDocumentListItems;
|
|
52
|
+
exports.singletonDocumentListItem = singletonDocumentListItem;
|
|
53
|
+
exports.singletonDocumentListItems = singletonDocumentListItems;
|
|
54
|
+
exports.singletonTools = singletonTools;
|
|
55
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/helpers/index.ts","../src/actions.ts","../src/newDocumentOptions.ts","../src/pluginConfig.ts","../src/structure/index.ts"],"sourcesContent":["import { Schema } from \"sanity\";\n\nimport { SingletonPluginOptions } from \"../types\";\n\nexport const getSingletonDocuments = (schema: Schema): string[] | undefined => {\n if (!schema?._original?.types) {\n return undefined;\n }\n\n return schema._original.types\n .filter(({ options }) => (options as SingletonPluginOptions)?.singleton)\n .map((s: { name: string }) => s.name);\n};\n\nexport const getIsSingleton = (schema: Schema, schemaType: string): boolean => {\n if (!schema?._original?.types || !schemaType) {\n return false;\n }\n\n const documentSchema = schema._original.types.find(\n ({ name }) => name === schemaType,\n );\n\n return (\n (documentSchema?.options as SingletonPluginOptions)?.singleton ?? false\n );\n};\n","import { DocumentActionsContext, DocumentActionsResolver } from \"sanity\";\n\nimport { getIsSingleton } from \"./helpers\";\n\nexport const actions: DocumentActionsResolver = (\n prev,\n { schema, schemaType }: DocumentActionsContext,\n) => {\n return getIsSingleton(schema, schemaType)\n ? prev.filter(({ action }) =>\n [\"publish\", \"discardChanges\", \"restore\"].includes(action as string),\n )\n : prev;\n};\n","import { NewDocumentOptionsContext, NewDocumentOptionsResolver } from \"sanity\";\n\nimport { getSingletonDocuments } from \"./helpers\";\n\nexport const newDocumentOptions: NewDocumentOptionsResolver = (\n prev,\n { schema, creationContext: { type, schemaType } }: NewDocumentOptionsContext,\n) => {\n const singletons = getSingletonDocuments(schema);\n\n const filterSingletons = ({ templateId }: { templateId: string }) =>\n !singletons?.includes(templateId);\n\n if (type === \"global\") return prev.filter(filterSingletons);\n\n return singletons?.includes(schemaType ?? \"\")\n ? prev.filter(filterSingletons)\n : prev;\n};\n","import { definePlugin } from \"sanity\";\n\nimport { actions } from \"./actions\";\nimport { newDocumentOptions } from \"./newDocumentOptions\";\n\nexport const singletonTools = definePlugin(() => {\n return {\n name: \"singleton-tools\",\n document: {\n newDocumentOptions,\n actions,\n },\n };\n});\n","import { DocumentIcon } from \"@sanity/icons\";\nimport type { ListItemBuilder } from \"sanity/structure\";\n\nimport { getSingletonDocuments } from \"../helpers\";\nimport {\n SingletonDocumentListItemConfig,\n SingletonPluginListItemsConfig,\n} from \"../types\";\n\nconst singletonDocumentListItem = (\n config: SingletonDocumentListItemConfig,\n): ListItemBuilder => {\n if (!config?.S || !config?.type || !config.context) {\n throw new Error(\n \"S, context, and type must be provided to singletonDocumentListItem. \" +\n \"Example: singletonDocumentListItem({ S, context, type: 'product' })\",\n );\n }\n const { S, type, title, icon, id, context } = config;\n const { schema } = context;\n\n if (!schema) {\n throw new Error(\n \"Schema is required in context for singletonDocumentListItem\",\n );\n }\n\n const schemaType = schema.get(type);\n const listTitle = title ?? schemaType?.title ?? type;\n const listIcon = icon ?? schemaType?.icon ?? DocumentIcon;\n const listId = id ?? type;\n\n return S.listItem()\n .title(listTitle)\n .icon(listIcon)\n .child(S.document().schemaType(type).title(listTitle).id(listId));\n};\n\nconst singletonDocumentListItems = (\n config: SingletonPluginListItemsConfig,\n): ListItemBuilder[] => {\n if (!config.S || !config.context) {\n throw new Error(\n \"S and context must be provided to singletonDocumentListItems. \" +\n \"Example: singletonDocumentListItems({ S, context })\",\n );\n }\n\n const { S, context } = config;\n const { schema } = context;\n\n const singletons = getSingletonDocuments(schema);\n\n return (\n singletons?.map((schemaType) =>\n singletonDocumentListItem({ S, context, type: schemaType }),\n ) || []\n );\n};\n\nconst filteredDocumentListItems = (\n config: SingletonPluginListItemsConfig,\n): ListItemBuilder[] => {\n if (!config.S || !config.context) {\n throw new Error(\n \"S and context must be provided to filteredDocumentListItems. \" +\n \"Example: filteredDocumentListItems({ S, context })\",\n );\n }\n const { S, context } = config;\n const { schema } = context;\n\n const singletons = getSingletonDocuments(schema);\n\n return S.documentTypeListItems().filter(\n (type) => !singletons || !singletons.includes(type.getId() as string),\n );\n};\n\nexport {\n filteredDocumentListItems,\n singletonDocumentListItem,\n singletonDocumentListItems,\n};\n"],"names":["definePlugin","DocumentIcon"],"mappings":";;;AAIO,MAAM,wBAAwB,CAAC,WAAyC;AAC7E,MAAK,QAAQ,WAAW;AAIxB,WAAO,OAAO,UAAU,MACrB,OAAO,CAAC,EAAE,QAAA,MAAe,SAAoC,SAAS,EACtE,IAAI,CAAC,MAAwB,EAAE,IAAI;AACxC,GAEa,iBAAiB,CAAC,QAAgB,eACzC,CAAC,QAAQ,WAAW,SAAS,CAAC,aACzB,KAGc,OAAO,UAAU,MAAM;AAAA,EAC5C,CAAC,EAAE,KAAA,MAAW,SAAS;AACzB,GAGmB,SAAoC,aAAa,ICpBzD,UAAmC,CAC9C,MACA,EAAE,QAAQ,WAAA,MAEH,eAAe,QAAQ,UAAU,IACpC,KAAK;AAAA,EAAO,CAAC,EAAE,OAAA,MACb,CAAC,WAAW,kBAAkB,SAAS,EAAE,SAAS,MAAgB;AACpE,IACA,MCRO,qBAAiD,CAC5D,MACA,EAAE,QAAQ,iBAAiB,EAAE,MAAM,WAAA,QAChC;AACH,QAAM,aAAa,sBAAsB,MAAM,GAEzC,mBAAmB,CAAC,EAAE,iBAC1B,CAAC,YAAY,SAAS,UAAU;AAElC,SAAI,SAAS,YAEN,YAAY,SAAS,cAAc,EAAE,IAFd,KAAK,OAAO,gBAAgB,IAItD;AACN,GCba,iBAAiBA,OAAAA,aAAa,OAClC;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,IACR;AAAA,IACA;AAAA,EAAA;AAEJ,EACD,GCJK,4BAA4B,CAChC,WACoB;AACpB,MAAI,CAAC,QAAQ,KAAK,CAAC,QAAQ,QAAQ,CAAC,OAAO;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAE,GAAG,MAAM,OAAO,MAAM,IAAI,YAAY,QACxC,EAAE,OAAA,IAAW;AAEnB,MAAI,CAAC;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,aAAa,OAAO,IAAI,IAAI,GAC5B,YAAY,SAAS,YAAY,SAAS,MAC1C,WAAW,QAAQ,YAAY,QAAQC,oBACvC,SAAS,MAAM;AAErB,SAAO,EAAE,WACN,MAAM,SAAS,EACf,KAAK,QAAQ,EACb,MAAM,EAAE,SAAA,EAAW,WAAW,IAAI,EAAE,MAAM,SAAS,EAAE,GAAG,MAAM,CAAC;AACpE,GAEM,6BAA6B,CACjC,WACsB;AACtB,MAAI,CAAC,OAAO,KAAK,CAAC,OAAO;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAKJ,QAAM,EAAE,GAAG,QAAA,IAAY,QACjB,EAAE,WAAW;AAInB,SAFmB,sBAAsB,MAAM,GAGjC;AAAA,IAAI,CAAC,eACf,0BAA0B,EAAE,GAAG,SAAS,MAAM,YAAY;AAAA,EAAA,KACvD,CAAA;AAET,GAEM,4BAA4B,CAChC,WACsB;AACtB,MAAI,CAAC,OAAO,KAAK,CAAC,OAAO;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAE,GAAG,QAAA,IAAY,QACjB,EAAE,OAAA,IAAW,SAEb,aAAa,sBAAsB,MAAM;AAE/C,SAAO,EAAE,wBAAwB;AAAA,IAC/B,CAAC,SAAS,CAAC,cAAc,CAAC,WAAW,SAAS,KAAK,MAAA,CAAiB;AAAA,EAAA;AAExE;;;;;"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { ConfigContext } from "sanity";
|
|
3
|
+
import type { ListItemBuilder } from "sanity/structure";
|
|
4
|
+
import { Plugin as Plugin_2 } from "sanity";
|
|
5
|
+
import { ReactNode } from "react";
|
|
6
|
+
import { StructureBuilder } from "sanity/structure";
|
|
7
|
+
|
|
8
|
+
export declare const filteredDocumentListItems: (
|
|
9
|
+
config: SingletonPluginListItemsConfig,
|
|
10
|
+
) => ListItemBuilder[];
|
|
11
|
+
|
|
12
|
+
export declare const singletonDocumentListItem: (
|
|
13
|
+
config: SingletonDocumentListItemConfig,
|
|
14
|
+
) => ListItemBuilder;
|
|
15
|
+
|
|
16
|
+
declare interface SingletonDocumentListItemConfig {
|
|
17
|
+
S: StructureBuilder;
|
|
18
|
+
context: ConfigContext;
|
|
19
|
+
type: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
icon?: ComponentType | ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare const singletonDocumentListItems: (
|
|
26
|
+
config: SingletonPluginListItemsConfig,
|
|
27
|
+
) => ListItemBuilder[];
|
|
28
|
+
|
|
29
|
+
declare interface SingletonPluginListItemsConfig {
|
|
30
|
+
S: StructureBuilder;
|
|
31
|
+
context: ConfigContext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare const singletonTools: Plugin_2<void>;
|
|
35
|
+
|
|
36
|
+
export {};
|
|
37
|
+
|
|
38
|
+
declare module "sanity" {
|
|
39
|
+
interface DocumentOptions extends SingletonPluginOptions {}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { ConfigContext } from "sanity";
|
|
3
|
+
import type { ListItemBuilder } from "sanity/structure";
|
|
4
|
+
import { Plugin as Plugin_2 } from "sanity";
|
|
5
|
+
import { ReactNode } from "react";
|
|
6
|
+
import { StructureBuilder } from "sanity/structure";
|
|
7
|
+
|
|
8
|
+
export declare const filteredDocumentListItems: (
|
|
9
|
+
config: SingletonPluginListItemsConfig,
|
|
10
|
+
) => ListItemBuilder[];
|
|
11
|
+
|
|
12
|
+
export declare const singletonDocumentListItem: (
|
|
13
|
+
config: SingletonDocumentListItemConfig,
|
|
14
|
+
) => ListItemBuilder;
|
|
15
|
+
|
|
16
|
+
declare interface SingletonDocumentListItemConfig {
|
|
17
|
+
S: StructureBuilder;
|
|
18
|
+
context: ConfigContext;
|
|
19
|
+
type: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
icon?: ComponentType | ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare const singletonDocumentListItems: (
|
|
26
|
+
config: SingletonPluginListItemsConfig,
|
|
27
|
+
) => ListItemBuilder[];
|
|
28
|
+
|
|
29
|
+
declare interface SingletonPluginListItemsConfig {
|
|
30
|
+
S: StructureBuilder;
|
|
31
|
+
context: ConfigContext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare const singletonTools: Plugin_2<void>;
|
|
35
|
+
|
|
36
|
+
export {};
|
|
37
|
+
|
|
38
|
+
declare module "sanity" {
|
|
39
|
+
interface DocumentOptions extends SingletonPluginOptions {}
|
|
40
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { definePlugin } from "sanity";
|
|
2
|
+
import { DocumentIcon } from "@sanity/icons";
|
|
3
|
+
const getSingletonDocuments = (schema) => {
|
|
4
|
+
if (schema?._original?.types)
|
|
5
|
+
return schema._original.types.filter(({ options }) => options?.singleton).map((s) => s.name);
|
|
6
|
+
}, getIsSingleton = (schema, schemaType) => !schema?._original?.types || !schemaType ? !1 : schema._original.types.find(
|
|
7
|
+
({ name }) => name === schemaType
|
|
8
|
+
)?.options?.singleton ?? !1, actions = (prev, { schema, schemaType }) => getIsSingleton(schema, schemaType) ? prev.filter(
|
|
9
|
+
({ action }) => ["publish", "discardChanges", "restore"].includes(action)
|
|
10
|
+
) : prev, newDocumentOptions = (prev, { schema, creationContext: { type, schemaType } }) => {
|
|
11
|
+
const singletons = getSingletonDocuments(schema), filterSingletons = ({ templateId }) => !singletons?.includes(templateId);
|
|
12
|
+
return type === "global" || singletons?.includes(schemaType ?? "") ? prev.filter(filterSingletons) : prev;
|
|
13
|
+
}, singletonTools = definePlugin(() => ({
|
|
14
|
+
name: "singleton-tools",
|
|
15
|
+
document: {
|
|
16
|
+
newDocumentOptions,
|
|
17
|
+
actions
|
|
18
|
+
}
|
|
19
|
+
})), singletonDocumentListItem = (config) => {
|
|
20
|
+
if (!config?.S || !config?.type || !config.context)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"S, context, and type must be provided to singletonDocumentListItem. Example: singletonDocumentListItem({ S, context, type: 'product' })"
|
|
23
|
+
);
|
|
24
|
+
const { S, type, title, icon, id, context } = config, { schema } = context;
|
|
25
|
+
if (!schema)
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Schema is required in context for singletonDocumentListItem"
|
|
28
|
+
);
|
|
29
|
+
const schemaType = schema.get(type), listTitle = title ?? schemaType?.title ?? type, listIcon = icon ?? schemaType?.icon ?? DocumentIcon, listId = id ?? type;
|
|
30
|
+
return S.listItem().title(listTitle).icon(listIcon).child(S.document().schemaType(type).title(listTitle).id(listId));
|
|
31
|
+
}, singletonDocumentListItems = (config) => {
|
|
32
|
+
if (!config.S || !config.context)
|
|
33
|
+
throw new Error(
|
|
34
|
+
"S and context must be provided to singletonDocumentListItems. Example: singletonDocumentListItems({ S, context })"
|
|
35
|
+
);
|
|
36
|
+
const { S, context } = config, { schema } = context;
|
|
37
|
+
return getSingletonDocuments(schema)?.map(
|
|
38
|
+
(schemaType) => singletonDocumentListItem({ S, context, type: schemaType })
|
|
39
|
+
) || [];
|
|
40
|
+
}, filteredDocumentListItems = (config) => {
|
|
41
|
+
if (!config.S || !config.context)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"S and context must be provided to filteredDocumentListItems. Example: filteredDocumentListItems({ S, context })"
|
|
44
|
+
);
|
|
45
|
+
const { S, context } = config, { schema } = context, singletons = getSingletonDocuments(schema);
|
|
46
|
+
return S.documentTypeListItems().filter(
|
|
47
|
+
(type) => !singletons || !singletons.includes(type.getId())
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
export {
|
|
51
|
+
filteredDocumentListItems,
|
|
52
|
+
singletonDocumentListItem,
|
|
53
|
+
singletonDocumentListItems,
|
|
54
|
+
singletonTools
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/helpers/index.ts","../src/actions.ts","../src/newDocumentOptions.ts","../src/pluginConfig.ts","../src/structure/index.ts"],"sourcesContent":["import { Schema } from \"sanity\";\n\nimport { SingletonPluginOptions } from \"../types\";\n\nexport const getSingletonDocuments = (schema: Schema): string[] | undefined => {\n if (!schema?._original?.types) {\n return undefined;\n }\n\n return schema._original.types\n .filter(({ options }) => (options as SingletonPluginOptions)?.singleton)\n .map((s: { name: string }) => s.name);\n};\n\nexport const getIsSingleton = (schema: Schema, schemaType: string): boolean => {\n if (!schema?._original?.types || !schemaType) {\n return false;\n }\n\n const documentSchema = schema._original.types.find(\n ({ name }) => name === schemaType,\n );\n\n return (\n (documentSchema?.options as SingletonPluginOptions)?.singleton ?? false\n );\n};\n","import { DocumentActionsContext, DocumentActionsResolver } from \"sanity\";\n\nimport { getIsSingleton } from \"./helpers\";\n\nexport const actions: DocumentActionsResolver = (\n prev,\n { schema, schemaType }: DocumentActionsContext,\n) => {\n return getIsSingleton(schema, schemaType)\n ? prev.filter(({ action }) =>\n [\"publish\", \"discardChanges\", \"restore\"].includes(action as string),\n )\n : prev;\n};\n","import { NewDocumentOptionsContext, NewDocumentOptionsResolver } from \"sanity\";\n\nimport { getSingletonDocuments } from \"./helpers\";\n\nexport const newDocumentOptions: NewDocumentOptionsResolver = (\n prev,\n { schema, creationContext: { type, schemaType } }: NewDocumentOptionsContext,\n) => {\n const singletons = getSingletonDocuments(schema);\n\n const filterSingletons = ({ templateId }: { templateId: string }) =>\n !singletons?.includes(templateId);\n\n if (type === \"global\") return prev.filter(filterSingletons);\n\n return singletons?.includes(schemaType ?? \"\")\n ? prev.filter(filterSingletons)\n : prev;\n};\n","import { definePlugin } from \"sanity\";\n\nimport { actions } from \"./actions\";\nimport { newDocumentOptions } from \"./newDocumentOptions\";\n\nexport const singletonTools = definePlugin(() => {\n return {\n name: \"singleton-tools\",\n document: {\n newDocumentOptions,\n actions,\n },\n };\n});\n","import { DocumentIcon } from \"@sanity/icons\";\nimport type { ListItemBuilder } from \"sanity/structure\";\n\nimport { getSingletonDocuments } from \"../helpers\";\nimport {\n SingletonDocumentListItemConfig,\n SingletonPluginListItemsConfig,\n} from \"../types\";\n\nconst singletonDocumentListItem = (\n config: SingletonDocumentListItemConfig,\n): ListItemBuilder => {\n if (!config?.S || !config?.type || !config.context) {\n throw new Error(\n \"S, context, and type must be provided to singletonDocumentListItem. \" +\n \"Example: singletonDocumentListItem({ S, context, type: 'product' })\",\n );\n }\n const { S, type, title, icon, id, context } = config;\n const { schema } = context;\n\n if (!schema) {\n throw new Error(\n \"Schema is required in context for singletonDocumentListItem\",\n );\n }\n\n const schemaType = schema.get(type);\n const listTitle = title ?? schemaType?.title ?? type;\n const listIcon = icon ?? schemaType?.icon ?? DocumentIcon;\n const listId = id ?? type;\n\n return S.listItem()\n .title(listTitle)\n .icon(listIcon)\n .child(S.document().schemaType(type).title(listTitle).id(listId));\n};\n\nconst singletonDocumentListItems = (\n config: SingletonPluginListItemsConfig,\n): ListItemBuilder[] => {\n if (!config.S || !config.context) {\n throw new Error(\n \"S and context must be provided to singletonDocumentListItems. \" +\n \"Example: singletonDocumentListItems({ S, context })\",\n );\n }\n\n const { S, context } = config;\n const { schema } = context;\n\n const singletons = getSingletonDocuments(schema);\n\n return (\n singletons?.map((schemaType) =>\n singletonDocumentListItem({ S, context, type: schemaType }),\n ) || []\n );\n};\n\nconst filteredDocumentListItems = (\n config: SingletonPluginListItemsConfig,\n): ListItemBuilder[] => {\n if (!config.S || !config.context) {\n throw new Error(\n \"S and context must be provided to filteredDocumentListItems. \" +\n \"Example: filteredDocumentListItems({ S, context })\",\n );\n }\n const { S, context } = config;\n const { schema } = context;\n\n const singletons = getSingletonDocuments(schema);\n\n return S.documentTypeListItems().filter(\n (type) => !singletons || !singletons.includes(type.getId() as string),\n );\n};\n\nexport {\n filteredDocumentListItems,\n singletonDocumentListItem,\n singletonDocumentListItems,\n};\n"],"names":[],"mappings":";;AAIO,MAAM,wBAAwB,CAAC,WAAyC;AAC7E,MAAK,QAAQ,WAAW;AAIxB,WAAO,OAAO,UAAU,MACrB,OAAO,CAAC,EAAE,QAAA,MAAe,SAAoC,SAAS,EACtE,IAAI,CAAC,MAAwB,EAAE,IAAI;AACxC,GAEa,iBAAiB,CAAC,QAAgB,eACzC,CAAC,QAAQ,WAAW,SAAS,CAAC,aACzB,KAGc,OAAO,UAAU,MAAM;AAAA,EAC5C,CAAC,EAAE,KAAA,MAAW,SAAS;AACzB,GAGmB,SAAoC,aAAa,ICpBzD,UAAmC,CAC9C,MACA,EAAE,QAAQ,WAAA,MAEH,eAAe,QAAQ,UAAU,IACpC,KAAK;AAAA,EAAO,CAAC,EAAE,OAAA,MACb,CAAC,WAAW,kBAAkB,SAAS,EAAE,SAAS,MAAgB;AACpE,IACA,MCRO,qBAAiD,CAC5D,MACA,EAAE,QAAQ,iBAAiB,EAAE,MAAM,WAAA,QAChC;AACH,QAAM,aAAa,sBAAsB,MAAM,GAEzC,mBAAmB,CAAC,EAAE,iBAC1B,CAAC,YAAY,SAAS,UAAU;AAElC,SAAI,SAAS,YAEN,YAAY,SAAS,cAAc,EAAE,IAFd,KAAK,OAAO,gBAAgB,IAItD;AACN,GCba,iBAAiB,aAAa,OAClC;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,IACR;AAAA,IACA;AAAA,EAAA;AAEJ,EACD,GCJK,4BAA4B,CAChC,WACoB;AACpB,MAAI,CAAC,QAAQ,KAAK,CAAC,QAAQ,QAAQ,CAAC,OAAO;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAE,GAAG,MAAM,OAAO,MAAM,IAAI,YAAY,QACxC,EAAE,OAAA,IAAW;AAEnB,MAAI,CAAC;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,aAAa,OAAO,IAAI,IAAI,GAC5B,YAAY,SAAS,YAAY,SAAS,MAC1C,WAAW,QAAQ,YAAY,QAAQ,cACvC,SAAS,MAAM;AAErB,SAAO,EAAE,WACN,MAAM,SAAS,EACf,KAAK,QAAQ,EACb,MAAM,EAAE,SAAA,EAAW,WAAW,IAAI,EAAE,MAAM,SAAS,EAAE,GAAG,MAAM,CAAC;AACpE,GAEM,6BAA6B,CACjC,WACsB;AACtB,MAAI,CAAC,OAAO,KAAK,CAAC,OAAO;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAKJ,QAAM,EAAE,GAAG,QAAA,IAAY,QACjB,EAAE,WAAW;AAInB,SAFmB,sBAAsB,MAAM,GAGjC;AAAA,IAAI,CAAC,eACf,0BAA0B,EAAE,GAAG,SAAS,MAAM,YAAY;AAAA,EAAA,KACvD,CAAA;AAET,GAEM,4BAA4B,CAChC,WACsB;AACtB,MAAI,CAAC,OAAO,KAAK,CAAC,OAAO;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAE,GAAG,QAAA,IAAY,QACjB,EAAE,OAAA,IAAW,SAEb,aAAa,sBAAsB,MAAM;AAE/C,SAAO,EAAE,wBAAwB;AAAA,IAC/B,CAAC,SAAS,CAAC,cAAc,CAAC,WAAW,SAAS,KAAK,MAAA,CAAiB;AAAA,EAAA;AAExE;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-singleton-management",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A plugin to streamline singleton management in your Sanity Studio",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "RD Pennell <racheal@sanity.io> & RC Maples <rc@rcmaples.io>",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
14
|
+
"import": "./dist/index.esm.js",
|
|
15
|
+
"require": "./dist/index.cjs",
|
|
16
|
+
"default": "./dist/index.esm.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.esm.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"browserslist": "extends @sanity/browserslist-config",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"sanity.json",
|
|
28
|
+
"src",
|
|
29
|
+
"v2-incompatible.js",
|
|
30
|
+
"!src/__tests__",
|
|
31
|
+
"!src/__mocks__"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "npm run clean && npx @sanity/pkg-utils build --strict",
|
|
35
|
+
"clean": "rimraf dist",
|
|
36
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
37
|
+
"link-watch": "plugin-kit link-watch",
|
|
38
|
+
"lint": "eslint .",
|
|
39
|
+
"prepare": "husky",
|
|
40
|
+
"prepublishOnly": "run-s build",
|
|
41
|
+
"watch": "pkg-utils watch --strict",
|
|
42
|
+
"test": "vitest",
|
|
43
|
+
"test:ui": "vitest --ui",
|
|
44
|
+
"test:run": "vitest run",
|
|
45
|
+
"test:coverage": "vitest run --coverage"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@sanity/icons": "^3.7.4",
|
|
49
|
+
"@sanity/incompatible-plugin": "^1.0.4"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@commitlint/cli": "^19.8.1",
|
|
53
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
54
|
+
"@sanity/browserslist-config": "^1.0.5",
|
|
55
|
+
"@sanity/pkg-utils": "^8.1.12",
|
|
56
|
+
"@sanity/plugin-kit": "^4.0.19",
|
|
57
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
58
|
+
"@semantic-release/git": "^10.0.1",
|
|
59
|
+
"@semantic-release/github": "^11.0.6",
|
|
60
|
+
"@semantic-release/npm": "^12.0.2",
|
|
61
|
+
"@testing-library/jest-dom": "^6.4.6",
|
|
62
|
+
"@types/react": "^19.1.13",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
64
|
+
"@typescript-eslint/parser": "^8.44.0",
|
|
65
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
66
|
+
"@vitest/ui": "^3.2.4",
|
|
67
|
+
"eslint": "^9.35.0",
|
|
68
|
+
"eslint-config-prettier": "^10.1.8",
|
|
69
|
+
"eslint-config-sanity": "^7.1.1",
|
|
70
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
71
|
+
"eslint-plugin-react": "^7.34.0",
|
|
72
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
73
|
+
"husky": "^9.1.7",
|
|
74
|
+
"jsdom": "^27.0.0",
|
|
75
|
+
"npm-run-all": "^4.1.5",
|
|
76
|
+
"prettier": "^3.2.5",
|
|
77
|
+
"prettier-plugin-packagejson": "^2.4.12",
|
|
78
|
+
"react": "^19.1.1",
|
|
79
|
+
"react-dom": "^19.1.1",
|
|
80
|
+
"react-is": "^19.1.1",
|
|
81
|
+
"rimraf": "^6.0.1",
|
|
82
|
+
"sanity": "^4.9.0",
|
|
83
|
+
"semantic-release": "^24.2.8",
|
|
84
|
+
"styled-components": "^6.1.15",
|
|
85
|
+
"typescript": "^5.3.3",
|
|
86
|
+
"typescript-eslint": "^8.44.0",
|
|
87
|
+
"vitest": "^3.2.4"
|
|
88
|
+
},
|
|
89
|
+
"overrides": {
|
|
90
|
+
"esbuild": "0.25.0",
|
|
91
|
+
"prismjs": "1.30.0",
|
|
92
|
+
"tmp": "0.2.4"
|
|
93
|
+
},
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"react": "^18 || ^19",
|
|
96
|
+
"sanity": "^3 || ^4"
|
|
97
|
+
},
|
|
98
|
+
"engines": {
|
|
99
|
+
"node": ">=20"
|
|
100
|
+
},
|
|
101
|
+
"repository": {
|
|
102
|
+
"type": "git",
|
|
103
|
+
"url": "https://github.com/rcmaples/sanity-plugin-singleton-management"
|
|
104
|
+
},
|
|
105
|
+
"sideEffects": true
|
|
106
|
+
}
|
package/sanity.json
ADDED
package/src/actions.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DocumentActionsContext, DocumentActionsResolver } from "sanity";
|
|
2
|
+
|
|
3
|
+
import { getIsSingleton } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const actions: DocumentActionsResolver = (
|
|
6
|
+
prev,
|
|
7
|
+
{ schema, schemaType }: DocumentActionsContext,
|
|
8
|
+
) => {
|
|
9
|
+
return getIsSingleton(schema, schemaType)
|
|
10
|
+
? prev.filter(({ action }) =>
|
|
11
|
+
["publish", "discardChanges", "restore"].includes(action as string),
|
|
12
|
+
)
|
|
13
|
+
: prev;
|
|
14
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Schema } from "sanity";
|
|
2
|
+
|
|
3
|
+
import { SingletonPluginOptions } from "../types";
|
|
4
|
+
|
|
5
|
+
export const getSingletonDocuments = (schema: Schema): string[] | undefined => {
|
|
6
|
+
if (!schema?._original?.types) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return schema._original.types
|
|
11
|
+
.filter(({ options }) => (options as SingletonPluginOptions)?.singleton)
|
|
12
|
+
.map((s: { name: string }) => s.name);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getIsSingleton = (schema: Schema, schemaType: string): boolean => {
|
|
16
|
+
if (!schema?._original?.types || !schemaType) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const documentSchema = schema._original.types.find(
|
|
21
|
+
({ name }) => name === schemaType,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
(documentSchema?.options as SingletonPluginOptions)?.singleton ?? false
|
|
26
|
+
);
|
|
27
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { singletonTools } from "./pluginConfig";
|
|
2
|
+
import {
|
|
3
|
+
filteredDocumentListItems,
|
|
4
|
+
singletonDocumentListItem,
|
|
5
|
+
singletonDocumentListItems,
|
|
6
|
+
} from "./structure";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
filteredDocumentListItems,
|
|
10
|
+
singletonDocumentListItem,
|
|
11
|
+
singletonDocumentListItems,
|
|
12
|
+
singletonTools,
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NewDocumentOptionsContext, NewDocumentOptionsResolver } from "sanity";
|
|
2
|
+
|
|
3
|
+
import { getSingletonDocuments } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const newDocumentOptions: NewDocumentOptionsResolver = (
|
|
6
|
+
prev,
|
|
7
|
+
{ schema, creationContext: { type, schemaType } }: NewDocumentOptionsContext,
|
|
8
|
+
) => {
|
|
9
|
+
const singletons = getSingletonDocuments(schema);
|
|
10
|
+
|
|
11
|
+
const filterSingletons = ({ templateId }: { templateId: string }) =>
|
|
12
|
+
!singletons?.includes(templateId);
|
|
13
|
+
|
|
14
|
+
if (type === "global") return prev.filter(filterSingletons);
|
|
15
|
+
|
|
16
|
+
return singletons?.includes(schemaType ?? "")
|
|
17
|
+
? prev.filter(filterSingletons)
|
|
18
|
+
: prev;
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { definePlugin } from "sanity";
|
|
2
|
+
|
|
3
|
+
import { actions } from "./actions";
|
|
4
|
+
import { newDocumentOptions } from "./newDocumentOptions";
|
|
5
|
+
|
|
6
|
+
export const singletonTools = definePlugin(() => {
|
|
7
|
+
return {
|
|
8
|
+
name: "singleton-tools",
|
|
9
|
+
document: {
|
|
10
|
+
newDocumentOptions,
|
|
11
|
+
actions,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DocumentIcon } from "@sanity/icons";
|
|
2
|
+
import type { ListItemBuilder } from "sanity/structure";
|
|
3
|
+
|
|
4
|
+
import { getSingletonDocuments } from "../helpers";
|
|
5
|
+
import {
|
|
6
|
+
SingletonDocumentListItemConfig,
|
|
7
|
+
SingletonPluginListItemsConfig,
|
|
8
|
+
} from "../types";
|
|
9
|
+
|
|
10
|
+
const singletonDocumentListItem = (
|
|
11
|
+
config: SingletonDocumentListItemConfig,
|
|
12
|
+
): ListItemBuilder => {
|
|
13
|
+
if (!config?.S || !config?.type || !config.context) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"S, context, and type must be provided to singletonDocumentListItem. " +
|
|
16
|
+
"Example: singletonDocumentListItem({ S, context, type: 'product' })",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const { S, type, title, icon, id, context } = config;
|
|
20
|
+
const { schema } = context;
|
|
21
|
+
|
|
22
|
+
if (!schema) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Schema is required in context for singletonDocumentListItem",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const schemaType = schema.get(type);
|
|
29
|
+
const listTitle = title ?? schemaType?.title ?? type;
|
|
30
|
+
const listIcon = icon ?? schemaType?.icon ?? DocumentIcon;
|
|
31
|
+
const listId = id ?? type;
|
|
32
|
+
|
|
33
|
+
return S.listItem()
|
|
34
|
+
.title(listTitle)
|
|
35
|
+
.icon(listIcon)
|
|
36
|
+
.child(S.document().schemaType(type).title(listTitle).id(listId));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const singletonDocumentListItems = (
|
|
40
|
+
config: SingletonPluginListItemsConfig,
|
|
41
|
+
): ListItemBuilder[] => {
|
|
42
|
+
if (!config.S || !config.context) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"S and context must be provided to singletonDocumentListItems. " +
|
|
45
|
+
"Example: singletonDocumentListItems({ S, context })",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { S, context } = config;
|
|
50
|
+
const { schema } = context;
|
|
51
|
+
|
|
52
|
+
const singletons = getSingletonDocuments(schema);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
singletons?.map((schemaType) =>
|
|
56
|
+
singletonDocumentListItem({ S, context, type: schemaType }),
|
|
57
|
+
) || []
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const filteredDocumentListItems = (
|
|
62
|
+
config: SingletonPluginListItemsConfig,
|
|
63
|
+
): ListItemBuilder[] => {
|
|
64
|
+
if (!config.S || !config.context) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"S and context must be provided to filteredDocumentListItems. " +
|
|
67
|
+
"Example: filteredDocumentListItems({ S, context })",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const { S, context } = config;
|
|
71
|
+
const { schema } = context;
|
|
72
|
+
|
|
73
|
+
const singletons = getSingletonDocuments(schema);
|
|
74
|
+
|
|
75
|
+
return S.documentTypeListItems().filter(
|
|
76
|
+
(type) => !singletons || !singletons.includes(type.getId() as string),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
filteredDocumentListItems,
|
|
82
|
+
singletonDocumentListItem,
|
|
83
|
+
singletonDocumentListItems,
|
|
84
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { ConfigContext, SanityDocument } from "sanity";
|
|
3
|
+
import { StructureBuilder } from "sanity/structure";
|
|
4
|
+
|
|
5
|
+
export interface SingletonDocumentListItemConfig {
|
|
6
|
+
S: StructureBuilder;
|
|
7
|
+
context: ConfigContext;
|
|
8
|
+
type: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
icon?: ComponentType | ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SingletonPluginListItemsConfig {
|
|
15
|
+
S: StructureBuilder;
|
|
16
|
+
context: ConfigContext;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SingletonPluginOptions {
|
|
20
|
+
singleton?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SanitySingletonDocument extends SanityDocument {
|
|
24
|
+
options?: {
|
|
25
|
+
singleton?: boolean;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare module "sanity" {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
31
|
+
interface DocumentOptions extends SingletonPluginOptions {}
|
|
32
|
+
}
|