@tunghtml/strapi-plugin-dynamic-enum 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 +21 -0
- package/README.md +80 -0
- package/admin/src/components/DynamicEnumInput.jsx +133 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/package.json +48 -0
- package/server/register.js +7 -0
- package/strapi-admin.js +66 -0
- package/strapi-server.js +9 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tunghtml
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Strapi Plugin Dynamic Enum
|
|
2
|
+
|
|
3
|
+
A Strapi v5 custom field plugin that provides an enum field with the ability to dynamically create new options on the fly.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 Single select dropdown (like standard enum)
|
|
8
|
+
- ✨ Create new options directly from the input field
|
|
9
|
+
- 🔍 Search/filter existing options
|
|
10
|
+
- 💾 Automatically includes saved values in options list
|
|
11
|
+
- 🎨 Clean UX with Strapi Design System
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @tunghtml/strapi-plugin-dynamic-enum
|
|
17
|
+
# or
|
|
18
|
+
yarn add @tunghtml/strapi-plugin-dynamic-enum
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
Add the plugin to your `config/plugins.js`:
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
module.exports = {
|
|
27
|
+
// ...
|
|
28
|
+
'dynamic-enum': {
|
|
29
|
+
enabled: true,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
1. Go to **Content-Type Builder**
|
|
37
|
+
2. Select a content type or create a new one
|
|
38
|
+
3. Add a new field and select **Dynamic Enum** from custom fields
|
|
39
|
+
4. In the field settings, add initial options as JSON array:
|
|
40
|
+
```json
|
|
41
|
+
["Option 1", "Option 2", "Option 3"]
|
|
42
|
+
```
|
|
43
|
+
5. Save and use!
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
When editing an entry:
|
|
48
|
+
|
|
49
|
+
- Select from existing options in the dropdown
|
|
50
|
+
- Type to search/filter options
|
|
51
|
+
- If you type a value that doesn't exist, the plugin will show a "Create '...'" option
|
|
52
|
+
- Click to create and select the new option immediately
|
|
53
|
+
- Newly created options are automatically added to the list for future use
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
Initial options: `["Pending", "In Progress", "Completed"]`
|
|
58
|
+
|
|
59
|
+
User types "Cancelled" → Plugin shows "Create 'Cancelled'" → User clicks → "Cancelled" is now available as an option.
|
|
60
|
+
|
|
61
|
+
## Data Storage
|
|
62
|
+
|
|
63
|
+
Values are stored as strings in the database, just like standard enum fields.
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- Strapi v5.x
|
|
68
|
+
- Node.js >= 18.0.0
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
|
73
|
+
|
|
74
|
+
## Author
|
|
75
|
+
|
|
76
|
+
[tunghtml](https://github.com/finnwasabi)
|
|
77
|
+
|
|
78
|
+
## Repository
|
|
79
|
+
|
|
80
|
+
[https://github.com/finnwasabi/strapi-plugin-dynamic-enum](https://github.com/finnwasabi/strapi-plugin-dynamic-enum)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Combobox,
|
|
3
|
+
ComboboxOption,
|
|
4
|
+
DesignSystemProvider,
|
|
5
|
+
Field,
|
|
6
|
+
} from '@strapi/design-system';
|
|
7
|
+
import { useNotification } from '@strapi/strapi/admin';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { useIntl } from 'react-intl';
|
|
10
|
+
import { useTheme } from 'styled-components';
|
|
11
|
+
|
|
12
|
+
const DynamicEnumInput = ({
|
|
13
|
+
name,
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
intlLabel,
|
|
17
|
+
label,
|
|
18
|
+
required,
|
|
19
|
+
error,
|
|
20
|
+
disabled = false,
|
|
21
|
+
attribute,
|
|
22
|
+
}) => {
|
|
23
|
+
const theme = useTheme();
|
|
24
|
+
const { formatMessage } = useIntl();
|
|
25
|
+
const { toggleNotification } = useNotification();
|
|
26
|
+
|
|
27
|
+
const [options, setOptions] = useState(() => {
|
|
28
|
+
const initialOptions = attribute?.options?.enum || attribute?.options || [];
|
|
29
|
+
let opts = [];
|
|
30
|
+
if (typeof initialOptions === 'string') {
|
|
31
|
+
try {
|
|
32
|
+
opts = JSON.parse(initialOptions);
|
|
33
|
+
} catch {
|
|
34
|
+
opts = [];
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
opts = Array.isArray(initialOptions) ? initialOptions : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add current value to options if it doesn't exist
|
|
41
|
+
if (value && !opts.includes(value)) {
|
|
42
|
+
opts = [...opts, value];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return opts;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const [searchValue, setSearchValue] = useState('');
|
|
49
|
+
|
|
50
|
+
// Add current value to options if it doesn't exist (for when loading saved data)
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (value && !options.includes(value)) {
|
|
53
|
+
setOptions((prev) => [...prev, value]);
|
|
54
|
+
}
|
|
55
|
+
}, [value, options]);
|
|
56
|
+
|
|
57
|
+
const handleInputChange = (value) => {
|
|
58
|
+
// Handle both string and event object
|
|
59
|
+
const newValue =
|
|
60
|
+
typeof value === 'string' ? value : value?.target?.value || '';
|
|
61
|
+
setSearchValue(newValue);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleCreateOption = (newValue) => {
|
|
65
|
+
const trimmedValue = String(newValue || '').trim();
|
|
66
|
+
|
|
67
|
+
if (!trimmedValue) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.includes(trimmedValue)) {
|
|
72
|
+
onChange({ target: { name, value: trimmedValue, type: 'string' } });
|
|
73
|
+
setSearchValue(''); // Reset search
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const updatedOptions = [...options, trimmedValue];
|
|
78
|
+
setOptions(updatedOptions);
|
|
79
|
+
onChange({ target: { name, value: trimmedValue, type: 'string' } });
|
|
80
|
+
setSearchValue(''); // Reset search
|
|
81
|
+
|
|
82
|
+
toggleNotification({
|
|
83
|
+
type: 'success',
|
|
84
|
+
message: `Created and selected "${trimmedValue}"`,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleChange = (selectedValue) => {
|
|
89
|
+
onChange({ target: { name, value: selectedValue, type: 'string' } });
|
|
90
|
+
setSearchValue(''); // Reset search after selection
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const searchStr = String(searchValue || '');
|
|
94
|
+
|
|
95
|
+
const filteredOptions = options.filter((option) =>
|
|
96
|
+
option.toLowerCase().includes(searchStr.toLowerCase())
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<DesignSystemProvider theme={theme}>
|
|
101
|
+
<Field.Root
|
|
102
|
+
name={name}
|
|
103
|
+
required={required}
|
|
104
|
+
error={error}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
>
|
|
107
|
+
<Field.Label>
|
|
108
|
+
{intlLabel?.id ? formatMessage(intlLabel) : label || name}
|
|
109
|
+
</Field.Label>
|
|
110
|
+
|
|
111
|
+
<Combobox
|
|
112
|
+
value={value || ''}
|
|
113
|
+
onChange={handleChange}
|
|
114
|
+
onInputChange={handleInputChange}
|
|
115
|
+
placeholder="Select or type to create"
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
creatable
|
|
118
|
+
onCreateOption={handleCreateOption}
|
|
119
|
+
>
|
|
120
|
+
{filteredOptions.map((option) => (
|
|
121
|
+
<ComboboxOption key={option} value={option}>
|
|
122
|
+
{option}
|
|
123
|
+
</ComboboxOption>
|
|
124
|
+
))}
|
|
125
|
+
</Combobox>
|
|
126
|
+
|
|
127
|
+
<Field.Error />
|
|
128
|
+
</Field.Root>
|
|
129
|
+
</DesignSystemProvider>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default DynamicEnumInput;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tunghtml/strapi-plugin-dynamic-enum",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Strapi custom field plugin for enum with dynamic option creation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"strapi",
|
|
7
|
+
"strapi-plugin",
|
|
8
|
+
"custom-field",
|
|
9
|
+
"enum",
|
|
10
|
+
"dynamic-enum"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/finnwasabi/strapi-plugin-dynamic-enum.git"
|
|
15
|
+
},
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "tunghtml",
|
|
18
|
+
"url": "https://github.com/finnwasabi"
|
|
19
|
+
},
|
|
20
|
+
"maintainers": [
|
|
21
|
+
{
|
|
22
|
+
"name": "tunghtml",
|
|
23
|
+
"url": "https://github.com/finnwasabi"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"strapi": {
|
|
27
|
+
"name": "dynamic-enum",
|
|
28
|
+
"displayName": "Dynamic Enum",
|
|
29
|
+
"description": "Custom field for enum with ability to create new options",
|
|
30
|
+
"kind": "plugin"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@strapi/design-system": "2.1.2",
|
|
34
|
+
"@strapi/icons": "2.1.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@strapi/strapi": "5.33.4",
|
|
38
|
+
"react": "^18.0.0",
|
|
39
|
+
"react-dom": "^18.0.0",
|
|
40
|
+
"react-redux": "^9.0.0",
|
|
41
|
+
"styled-components": "^6.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0 <=22.x.x",
|
|
45
|
+
"npm": ">=6.0.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|
package/strapi-admin.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import PluginIcon from './admin/src/components/PluginIcon';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
register(app) {
|
|
5
|
+
app.customFields.register({
|
|
6
|
+
name: 'dynamic-enum',
|
|
7
|
+
pluginId: 'dynamic-enum',
|
|
8
|
+
type: 'string',
|
|
9
|
+
intlLabel: {
|
|
10
|
+
id: 'dynamic-enum.label',
|
|
11
|
+
defaultMessage: 'Dynamic Enum',
|
|
12
|
+
},
|
|
13
|
+
intlDescription: {
|
|
14
|
+
id: 'dynamic-enum.description',
|
|
15
|
+
defaultMessage: 'Single select with ability to create new options',
|
|
16
|
+
},
|
|
17
|
+
icon: PluginIcon,
|
|
18
|
+
components: {
|
|
19
|
+
Input: async () => import('./admin/src/components/DynamicEnumInput'),
|
|
20
|
+
},
|
|
21
|
+
options: {
|
|
22
|
+
base: [
|
|
23
|
+
{
|
|
24
|
+
sectionTitle: {
|
|
25
|
+
id: 'dynamic-enum.options.base.settings',
|
|
26
|
+
defaultMessage: 'Settings',
|
|
27
|
+
},
|
|
28
|
+
items: [
|
|
29
|
+
{
|
|
30
|
+
name: 'options',
|
|
31
|
+
type: 'json',
|
|
32
|
+
intlLabel: {
|
|
33
|
+
id: 'dynamic-enum.options.enum',
|
|
34
|
+
defaultMessage: 'Initial Options',
|
|
35
|
+
},
|
|
36
|
+
description: {
|
|
37
|
+
id: 'dynamic-enum.options.enum.description',
|
|
38
|
+
defaultMessage:
|
|
39
|
+
'Enter initial enum values as JSON array: ["Option 1", "Option 2"]',
|
|
40
|
+
},
|
|
41
|
+
placeholder: {
|
|
42
|
+
id: 'dynamic-enum.options.enum.placeholder',
|
|
43
|
+
defaultMessage: '["Option 1", "Option 2", "Option 3"]',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'required',
|
|
48
|
+
type: 'checkbox',
|
|
49
|
+
intlLabel: {
|
|
50
|
+
id: 'dynamic-enum.options.requiredField',
|
|
51
|
+
defaultMessage: 'Required field',
|
|
52
|
+
},
|
|
53
|
+
description: {
|
|
54
|
+
id: 'dynamic-enum.options.requiredField.description',
|
|
55
|
+
defaultMessage:
|
|
56
|
+
"You won't be able to create an entry if this field is empty",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
advanced: [],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|