Skip to content

Commit

Permalink
Implement custom field
Browse files Browse the repository at this point in the history
  • Loading branch information
Freyb committed Jun 2, 2023
1 parent b9c3889 commit 410ee5a
Show file tree
Hide file tree
Showing 17 changed files with 305 additions and 72 deletions.
103 changes: 103 additions & 0 deletions admin/src/components/SelectorField/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Field,
FieldLabel,
FieldError,
FieldHint,
Select,
Option,
Loader,
Stack,
} from '@strapi/design-system';
import { useIntl } from 'react-intl';
import useConfig from '../../hooks/useConfig';

const SelectorField = ({
value,
onChange,
name,
intlLabel,
labelAction,
required,
attribute,
hint,
placeholder,
disabled,
error,
}) => {
// const { options = {} } = attribute;
const { config, isLoading: configIsLoading } = useConfig();

const { formatMessage } = useIntl();

console.log('Tags', config);

return (
<Field
name={name}
id={name}
error={error}
required={required}
hint={hint && formatMessage(hint)}
>
<Stack spacing={1}>
<FieldLabel action={labelAction}>{formatMessage(intlLabel)}</FieldLabel>

{configIsLoading ? (
<Loader small>Loading content...</Loader>
) : (
<Select
placeholder={placeholder && formatMessage(placeholder)}
aria-label={formatMessage(intlLabel)}
aria-disabled={disabled}
disabled={disabled}
value={value}
onChange={(newCountry) => {
onChange({
target: {
name,
value: newCountry,
type: attribute.type,
},
});
}}
>
<Option value="">None</Option>
{Object.entries(config.tags).map(([tagKey, tagProperties]) => (
<Option key={tagKey} value={tagKey}>
{tagProperties.label}
</Option>
))}
</Select>
)}
<FieldHint />
<FieldError />
</Stack>
</Field>
);
};

SelectorField.defaultProps = {
description: null,
disabled: false,
error: null,
labelAction: null,
required: false,
value: '',
};

SelectorField.propTypes = {
intlLabel: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
attribute: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.object,
disabled: PropTypes.bool,
error: PropTypes.string,
labelAction: PropTypes.object,
required: PropTypes.bool,
value: PropTypes.string,
};

export default SelectorField;
30 changes: 30 additions & 0 deletions admin/src/components/SelectorIcon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import styled from 'styled-components';
import { Icon, Flex } from '@strapi/design-system';
import { List } from '@strapi/icons';

const IconBox = styled(Flex)`
background-color: #f0f0ff; /* primary100 */
border: 1px solid #d9d8ff; /* primary200 */
svg > path {
fill: #4945ff; /* primary600 */
}
`;

const SelectorIcon = () => {
return (
<IconBox
justifyContent="center"
alignItems="center"
width={7}
height={6}
hasRadius
aria-hidden
>
<Icon as={List} />
</IconBox>
);
};

export default SelectorIcon;
37 changes: 37 additions & 0 deletions admin/src/hooks/useConfig/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import { request, useNotification } from '@strapi/helper-plugin';
import pluginId from '../../pluginId';

const fetchConfig = async () => {
const data = await request(`/${pluginId}/config`, {
method: 'GET',
});

return data ?? {};
};

const useConfig = () => {
const toggleNotification = useNotification();
const [isLoading, setIsLoading] = useState(true);
const [config, setConfig] = useState({});

useEffect(() => {
fetchConfig()
.then((config) => {
setConfig(config);
})
.catch(() => {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
})
.finally(() => {
setIsLoading(false);
});
}, [toggleNotification]);

return { config, isLoading };
};

export default useConfig;
61 changes: 42 additions & 19 deletions admin/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,54 @@ import { prefixPluginTranslations } from '@strapi/helper-plugin';
import pluginPkg from '../../package.json';
import pluginId from './pluginId';
import Initializer from './components/Initializer';
import PluginIcon from './components/PluginIcon';
import SelectorIcon from './components/SelectorIcon';
import getTrad from './utils/getTrad';

const name = pluginPkg.strapi.name;

export default {
register(app) {
app.addMenuLink({
to: `/plugins/${pluginId}`,
icon: PluginIcon,
app.customFields.register({
name: 'content-tags',
pluginId,
type: 'text',
icon: SelectorIcon,
intlLabel: {
id: `${pluginId}.plugin.name`,
defaultMessage: name,
id: getTrad('customField.label'),
defaultMessage: 'Tags',
},
Component: async () => {
const component = await import(/* webpackChunkName: "[request]" */ './pages/App');

return component;
intlDescription: {
id: getTrad('customField.description'),
defaultMessage: 'Assign tags to entities',
},
components: {
Input: async () => import('./components/SelectorField'),
},
options: {
advanced: [
{
sectionTitle: {
id: 'global.settings',
defaultMessage: 'Settings',
},
items: [
{
name: 'required',
type: 'checkbox',
intlLabel: {
id: 'form.attribute.item.requiredField',
defaultMessage: 'Required field',
},
description: {
id: 'form.attribute.item.requiredField.description',
defaultMessage:
"You won't be able to create an entry if this field is empty",
},
},
],
},
],
},
permissions: [
// Uncomment to set the permissions of the plugin here
// {
// action: '', // the action name should be plugin::plugin-name.actionType
// subject: null,
// },
],
});
app.registerPlugin({
id: pluginId,
Expand All @@ -36,7 +59,7 @@ export default {
});
},

bootstrap(app) {},
bootstrap(_app) {},
async registerTrads({ locales }) {
const importedTrads = await Promise.all(
locales.map((locale) => {
Expand All @@ -55,7 +78,7 @@ export default {
locale,
};
});
})
}),
);

return Promise.resolve(importedTrads);
Expand Down
2 changes: 0 additions & 2 deletions admin/src/pages/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { NotFound } from '@strapi/helper-plugin';
import pluginId from '../../pluginId';
import HomePage from '../HomePage';

const App = () => {
return (
<div>
<Switch>
<Route path={`/plugins/${pluginId}`} component={HomePage} exact />
<Route component={NotFound} />
</Switch>
</div>
Expand Down
20 changes: 0 additions & 20 deletions admin/src/pages/HomePage/index.js

This file was deleted.

8 changes: 5 additions & 3 deletions admin/src/pluginId.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pluginPkg from '../../package.json';
const pluginPkg = require('../../package.json');

const pluginId = pluginPkg.name.replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, '');
const pluginName = pluginPkg.name;
// eslint-disable-next-line prettier/prettier
const pluginId = pluginName.replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, '');

export default pluginId;
module.exports = pluginId;
5 changes: 4 additions & 1 deletion admin/src/translations/en.json
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{}
{
"customField.label": "Tags",
"customField.description": "Assign tags to entities"
}
42 changes: 40 additions & 2 deletions server/config/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
'use strict';

const { ValidationError } = require('@strapi/utils').errors;

module.exports = {
default: {},
validator() {},
default: {
colors: {
'dark-blue': '#0000FF',
'dark-green': '#00FF00',
'dark-red': '#FF0000',
black: '#000000',
},
tags: {
done: { label: 'Done', color: 'dark-green' },
inProgress: { label: 'In progress', color: 'dark-blue' },
error: { label: 'Error', color: 'dark-red' },
},
},
validator(config) {
// colors
if (!config.colors) {
throw new ValidationError('You must define colors prop');
}
// tags
if (!config.tags) {
throw new ValidationError('You must define tags prop');
}
Object.entries(config.tags).forEach(([tagKey, tagProperties]) => {
if (
!tagProperties.hasOwnProperty('label') ||
!tagProperties.hasOwnProperty('color')
) {
throw new ValidationError(
`Missing properties on tags.${tagKey}: Define label and color`,
);
}
if (!Object.keys(config.colors).includes(tagProperties.color)) {
throw new ValidationError(
`Undefined color key at tags.${tagKey}.color`,
);
}
});
},
};
14 changes: 14 additions & 0 deletions server/controllers/config-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';
const pluginId = require('../../admin/src/pluginId');

module.exports = ({ strapi }) => {
const configService = strapi.plugin(pluginId).service('configService');

const getConfig = async (ctx) => {
ctx.body = await configService.getConfig();
};

return {
getConfig,
};
};
4 changes: 2 additions & 2 deletions server/controllers/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const myController = require('./my-controller');
const configController = require('./config-controller');

module.exports = {
myController,
configController,
};
10 changes: 0 additions & 10 deletions server/controllers/my-controller.js

This file was deleted.

Loading

0 comments on commit 410ee5a

Please sign in to comment.