Merge branch 'main' into chore/user-level-data

This commit is contained in:
ivanThePleasant 2022-11-24 13:06:20 +02:00
commit 0b822b8a48
34 changed files with 1315 additions and 779 deletions

View File

@ -6,6 +6,7 @@ on:
- opened
- transferred
permissions: {}
jobs:
add-to-project:
name: Add issue to Support Team project

View File

@ -17,8 +17,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/check-pr-status
security-lockfile-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: yarn
- uses: ./.github/actions/security/lockfile

View File

@ -17,8 +17,12 @@ defaults:
run:
working-directory: docs
permissions: {}
jobs:
deploy:
permissions:
contents: write # to push pages branch (peaceiris/actions-gh-pages)
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

View File

@ -5,6 +5,9 @@ on:
- cron: '0 0 * * 2-6'
workflow_dispatch:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
publish:
name: 'Publish'

View File

@ -13,6 +13,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
lint:
name: 'lint (node: ${{ matrix.node }})'
@ -25,7 +28,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Run lint
run: yarn run -s lint
@ -42,7 +49,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Run tests
run: yarn run -s test:unit --coverage
@ -65,7 +76,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Build
run: yarn build
@ -109,7 +124,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
with:
@ -144,7 +162,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
with:
@ -179,7 +200,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
with:
@ -198,7 +222,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
env:
@ -240,7 +267,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
with:
@ -278,7 +308,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
with:
@ -301,7 +334,10 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- uses: ./.github/actions/run-api-tests
env:

View File

@ -134,6 +134,11 @@
"custom_field": {
"type": "customField",
"customField": "plugin::color-picker.color"
},
"custom_field_with_default_options": {
"type": "customField",
"regex": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
"customField": "plugin::color-picker.color"
}
}
}

View File

@ -26,6 +26,48 @@ export default {
id: pluginId,
name,
});
const allTypes = [
'biginteger',
'boolean',
'date',
'datetime',
'decimal',
'email',
'enumeration',
'float',
'integer',
'json',
'password',
'richtext',
'string',
'text',
'time',
'uid',
];
allTypes.forEach((type) => {
const upcasedType = type.charAt(0).toUpperCase() + type.slice(1);
const customField = {
type,
pluginId: 'myplugin',
name: `custom${upcasedType}`,
intlLabel: {
id: 'customfieldtest',
defaultMessage: `custom${upcasedType}`,
},
intlDescription: {
id: 'customfieldtest',
defaultMessage: `custom${upcasedType}`,
},
components: {
Input: async () =>
import(/* webpackChunkName: "test-custom-field" */ './components/PluginIcon'),
},
};
app.customFields.register(customField);
});
},
bootstrap() {},
async registerTrads({ locales }) {

View File

@ -1,6 +1,34 @@
'use strict';
module.exports = ({ strapi }) => {
const allTypes = [
'biginteger',
'boolean',
'date',
'datetime',
'decimal',
'email',
'enumeration',
'float',
'integer',
'json',
'password',
'richtext',
'string',
'text',
'time',
'uid',
];
allTypes.forEach((type) => {
const upcasedType = type.charAt(0).toUpperCase() + type.slice(1);
strapi.customFields.register({
type,
name: `custom${upcasedType}`,
plugin: 'myplugin',
});
});
if (strapi.plugin('graphql')) {
require('./graphql')({ strapi });
}

View File

@ -4,7 +4,7 @@
"description": "Generate a new Strapi application.",
"dependencies": {
"@strapi/generate-new": "4.5.2",
"commander": "6.1.0",
"commander": "7.2.0",
"inquirer": "8.2.4"
},
"keywords": [

View File

@ -41,7 +41,7 @@
"@strapi/generate-new": "4.5.2",
"chalk": "4.1.1",
"ci-info": "3.5.0",
"commander": "7.1.0",
"commander": "7.2.0",
"execa": "5.1.1",
"fs-extra": "10.0.0",
"inquirer": "8.2.4",

View File

@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Grid, GridItem } from '@strapi/design-system/Grid';
import Inputs from '../../../components/Inputs';
import FieldComponent from '../../../components/FieldComponent';
const GridRow = ({ columns }) => {
return (
<Grid gap={4}>
{columns.map(({ fieldSchema, labelAction, metadatas, name, size, queryInfos }) => {
const isComponent = fieldSchema.type === 'component';
if (isComponent) {
const { component, max, min, repeatable = false, required = false } = fieldSchema;
return (
<GridItem col={size} s={12} xs={12} key={component}>
<FieldComponent
componentUid={component}
labelAction={labelAction}
isRepeatable={repeatable}
intlLabel={{
id: metadatas.label,
defaultMessage: metadatas.label,
}}
max={max}
min={min}
name={name}
required={required}
/>
</GridItem>
);
}
return (
<GridItem col={size} key={name} s={12} xs={12}>
<Inputs
size={size}
fieldSchema={fieldSchema}
keys={name}
labelAction={labelAction}
metadatas={metadatas}
queryInfos={queryInfos}
/>
</GridItem>
);
})}
</Grid>
);
};
GridRow.propTypes = {
columns: PropTypes.array.isRequired,
};
export default GridRow;

View File

@ -1,6 +1,6 @@
import React, { Suspense, memo, useCallback, useMemo } from 'react';
import React, { Suspense, memo } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import { useSelector } from 'react-redux';
import {
CheckPermissions,
LoadingIndicatorPage,
@ -18,8 +18,7 @@ import Pencil from '@strapi/icons/Pencil';
import { InjectionZone } from '../../../shared/components';
import permissions from '../../../permissions';
import DynamicZone from '../../components/DynamicZone';
import FieldComponent from '../../components/FieldComponent';
import Inputs from '../../components/Inputs';
import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrapper';
import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider';
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
@ -27,64 +26,43 @@ import { getTrad } from '../../utils';
import DraftAndPublishBadge from './DraftAndPublishBadge';
import Informations from './Informations';
import Header from './Header';
import { createAttributesLayout, getFieldsActionMatchingPermissions } from './utils';
import { getFieldsActionMatchingPermissions } from './utils';
import DeleteLink from './DeleteLink';
import GridRow from './GridRow';
import { selectCurrentLayout, selectAttributesLayout } from './selectors';
const cmPermissions = permissions.contentManager;
const ctbPermissions = [{ action: 'plugin::content-type-builder.read', subject: null }];
/* eslint-disable react/no-array-index-key */
const EditView = ({
allowedActions,
isSingleType,
goBack,
layout,
slug,
id,
origin,
userPermissions,
}) => {
const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, userPermissions }) => {
const { trackUsage } = useTracking();
const { formatMessage } = useIntl();
const { createActionAllowedFields, readActionAllowedFields, updateActionAllowedFields } =
useMemo(() => {
return getFieldsActionMatchingPermissions(userPermissions, slug);
}, [userPermissions, slug]);
getFieldsActionMatchingPermissions(userPermissions, slug);
const configurationPermissions = useMemo(() => {
return isSingleType
? cmPermissions.singleTypesConfigurations
: cmPermissions.collectionTypesConfigurations;
}, [isSingleType]);
const { layout, formattedContentTypeLayout } = useSelector((state) => ({
layout: selectCurrentLayout(state),
formattedContentTypeLayout: selectAttributesLayout(state),
}));
const configurationPermissions = isSingleType
? cmPermissions.singleTypesConfigurations
: cmPermissions.collectionTypesConfigurations;
// // FIXME when changing the routing
const configurationsURL = `/content-manager/${
isSingleType ? 'singleType' : 'collectionType'
}/${slug}/configurations/edit`;
const currentContentTypeLayoutData = get(layout, ['contentType'], {});
const DataManagementWrapper = useMemo(
() => (isSingleType ? SingleTypeFormWrapper : CollectionTypeFormWrapper),
[isSingleType]
);
const DataManagementWrapper = isSingleType ? SingleTypeFormWrapper : CollectionTypeFormWrapper;
// Check if a block is a dynamic zone
const isDynamicZone = useCallback((block) => {
const isDynamicZone = (block) => {
return block.every((subBlock) => {
return subBlock.every((obj) => obj.fieldSchema.type === 'dynamiczone');
});
}, []);
const formattedContentTypeLayout = useMemo(() => {
if (!currentContentTypeLayoutData.layouts) {
return [];
}
return createAttributesLayout(
currentContentTypeLayoutData.layouts.edit,
currentContentTypeLayoutData.attributes
);
}, [currentContentTypeLayoutData]);
};
return (
<DataManagementWrapper allLayoutData={layout} slug={slug} id={id} origin={origin}>
@ -171,65 +149,9 @@ const EditView = ({
borderColor="neutral150"
>
<Stack spacing={6}>
{row.map((grid, gridIndex) => {
return (
<Grid gap={4} key={gridIndex}>
{grid.map(
({
fieldSchema,
labelAction,
metadatas,
name,
size,
queryInfos,
}) => {
const isComponent = fieldSchema.type === 'component';
if (isComponent) {
const {
component,
max,
min,
repeatable = false,
required = false,
} = fieldSchema;
return (
<GridItem col={size} s={12} xs={12} key={component}>
<FieldComponent
componentUid={component}
labelAction={labelAction}
isRepeatable={repeatable}
intlLabel={{
id: metadatas.label,
defaultMessage: metadatas.label,
}}
max={max}
min={min}
name={name}
required={required}
/>
</GridItem>
);
}
return (
<GridItem col={size} key={name} s={12} xs={12}>
<Inputs
size={size}
fieldSchema={fieldSchema}
keys={name}
labelAction={labelAction}
metadatas={metadatas}
queryInfos={queryInfos}
/>
</GridItem>
);
}
)}
</Grid>
);
})}
{row.map((grid, gridRowIndex) => (
<GridRow columns={grid} key={gridRowIndex} />
))}
</Stack>
</Box>
);
@ -328,16 +250,6 @@ EditView.propTypes = {
canCreate: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired,
}).isRequired,
layout: PropTypes.shape({
components: PropTypes.object.isRequired,
contentType: PropTypes.shape({
uid: PropTypes.string.isRequired,
settings: PropTypes.object.isRequired,
metadatas: PropTypes.object.isRequired,
options: PropTypes.object.isRequired,
attributes: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
id: PropTypes.string,
isSingleType: PropTypes.bool,
goBack: PropTypes.func.isRequired,
@ -346,7 +258,4 @@ EditView.propTypes = {
userPermissions: PropTypes.array,
};
export { EditView };
export default memo(EditView);
// export default () => 'TODO Edit view';

View File

@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
import { createAttributesLayout } from './utils';
const selectCurrentLayout = (state) => state['content-manager_editViewLayoutManager'].currentLayout;
const selectAttributesLayout = createSelector(selectCurrentLayout, (layout) =>
createAttributesLayout(layout?.contentType ?? {})
);
export { selectCurrentLayout, selectAttributesLayout };

View File

@ -1,14 +1,19 @@
import { get, isEmpty } from 'lodash';
// TODO: refacto this file to avoid eslint issues
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-unused-vars */
const createAttributesLayout = (currentLayout, attributes) => {
const createAttributesLayout = (currentContentTypeLayoutData) => {
if (!currentContentTypeLayoutData.layouts) {
return [];
}
const currentLayout = currentContentTypeLayoutData.layouts.edit;
const attributes = currentContentTypeLayoutData.attributes;
const getType = (name) => get(attributes, [name, 'type'], '');
let currentRowIndex = 0;
const newLayout = [];
for (let row of currentLayout) {
currentLayout.forEach((row) => {
const hasDynamicZone = row.some(({ name }) => getType(name) === 'dynamiczone');
if (!newLayout[currentRowIndex]) {
@ -27,7 +32,7 @@ const createAttributesLayout = (currentLayout, attributes) => {
} else {
newLayout[currentRowIndex].push(row);
}
}
});
return newLayout.filter((arr) => arr.length > 0);
};

View File

@ -27,6 +27,12 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[{ name: 'postal_code', size: 6 }],
[{ name: 'geolocation', size: 12 }],
];
const currentLayoutData = {
layouts: {
edit: currentLayout,
},
attributes,
};
const expected = [
[
[
@ -38,7 +44,7 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
],
];
expect(createAttributesLayout(currentLayout, attributes)).toEqual(expected);
expect(createAttributesLayout(currentLayoutData)).toEqual(expected);
});
it('Should return an array of size 2 if there is a dynamic zone at the end of the layout', () => {
@ -68,6 +74,12 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[{ name: 'geolocation', size: 12 }],
[{ name: 'dynamicZone1', size: 12 }],
];
const currentLayoutData = {
layouts: {
edit: currentLayout,
},
attributes,
};
const expected = [
[
[
@ -80,7 +92,7 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[[{ name: 'dynamicZone1', size: 12 }]],
];
expect(createAttributesLayout(currentLayout, attributes)).toEqual(expected);
expect(createAttributesLayout(currentLayoutData)).toEqual(expected);
});
it('Should return an array of size 2 if there is a dynamic zone at the beginning of the layout', () => {
@ -114,6 +126,12 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[{ name: 'postal_code', size: 6 }],
[{ name: 'geolocation', size: 12 }],
];
const currentLayoutData = {
layouts: {
edit: currentLayout,
},
attributes,
};
const expected = [
[[{ name: 'dynamicZone1', size: 12 }]],
[
@ -126,7 +144,7 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
],
];
expect(createAttributesLayout(currentLayout, attributes)).toEqual(expected);
expect(createAttributesLayout(currentLayoutData)).toEqual(expected);
});
it('Should return an array of size 5 if there are 3 dynamic zones', () => {
@ -164,6 +182,12 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[{ name: 'geolocation', size: 12 }],
[{ name: 'dynamicZone3', size: 12 }],
];
const currentLayoutData = {
layouts: {
edit: currentLayout,
},
attributes,
};
const expected = [
[[{ name: 'dynamicZone1', size: 12 }]],
[
@ -177,6 +201,11 @@ describe('Content Manager | EditView | utils | createAttributesLayout', () => {
[[{ name: 'dynamicZone3', size: 12 }]],
];
expect(createAttributesLayout(currentLayout, attributes)).toEqual(expected);
expect(createAttributesLayout(currentLayoutData)).toEqual(expected);
});
it('Should return an empty array when no layouts are found', () => {
const currentLayoutData = {};
expect(createAttributesLayout(currentLayoutData)).toEqual([]);
});
});

View File

@ -30,7 +30,7 @@ const EditViewLayoutManager = ({ layout, ...rest }) => {
return <LoadingIndicatorPage />;
}
return <Permissions {...rest} layout={currentLayout} userPermissions={permissions} />;
return <Permissions {...rest} userPermissions={permissions} />;
};
EditViewLayoutManager.propTypes = {

View File

@ -19,6 +19,40 @@ const ALLOWED_TYPES = [
'uid',
];
const ALLOWED_ROOT_LEVEL_OPTIONS = [
'min',
'minLength',
'max',
'maxLength',
'required',
'regex',
'enum',
'unique',
'private',
'default',
];
const optionValidationsReducer = (acc, option) => {
if (option.items) {
return option.items.reduce(optionValidationsReducer, acc);
}
if (!option.name) {
acc.push({
isValidOptionPath: false,
errorMessage: "The 'name' property is required on an options object",
});
} else {
acc.push({
isValidOptionPath:
ALLOWED_ROOT_LEVEL_OPTIONS.includes(option.name) || option.name.startsWith('options'),
errorMessage: `'${option.name}' must be prefixed with 'options.'`,
});
}
return acc;
};
class CustomFields {
constructor() {
this.customFields = {};
@ -32,7 +66,8 @@ class CustomFields {
});
} else {
// Handle individual custom field
const { name, pluginId, type, intlLabel, intlDescription, components } = customFields;
const { name, pluginId, type, intlLabel, intlDescription, components, options } =
customFields;
// Ensure required attributes are provided
invariant(name, 'A name must be provided');
@ -55,6 +90,16 @@ class CustomFields {
`Custom field name: '${name}' is not a valid object key`
);
// Ensure options have valid name paths
const allFormOptions = [...(options?.base || []), ...(options?.advanced || [])];
if (allFormOptions.length) {
const optionPathValidations = allFormOptions.reduce(optionValidationsReducer, []);
optionPathValidations.forEach(({ isValidOptionPath, errorMessage }) => {
invariant(isValidOptionPath, errorMessage);
});
}
// When no plugin is specified, default to the global namespace
const uid = pluginId ? `plugin::${pluginId}.${name}` : `global::${name}`;

View File

@ -37,6 +37,26 @@ const SortSelect = ({ sortQuery, handleSelectChange }) => {
defaultMessage: 'Newest',
},
},
'githubStars:desc': {
selected: {
id: 'admin.pages.MarketPlacePage.sort.githubStars.selected',
defaultMessage: 'Sort by GitHub stars',
},
option: {
id: 'admin.pages.MarketPlacePage.sort.githubStars',
defaultMessage: 'Number of GitHub stars',
},
},
'npmDownloads:desc': {
selected: {
id: 'admin.pages.MarketPlacePage.sort.npmDownloads.selected',
defaultMessage: 'Sort by npm downloads',
},
option: {
id: 'admin.pages.MarketPlacePage.sort.npmDownloads',
defaultMessage: 'Number of downloads',
},
},
};
return (

View File

@ -192,6 +192,33 @@ describe('ADMIN | StrapiApp', () => {
});
});
it('should register a custom field with valid options', () => {
const app = StrapiApp({ middlewares, reducers, library });
const field = {
name: 'optionsCustomField',
pluginId: 'myplugin',
type: 'text',
icon: jest.fn(),
intlLabel: { id: 'foo', defaultMessage: 'foo' },
intlDescription: { id: 'foo', defaultMessage: 'foo' },
components: {
Input: jest.fn(),
},
options: {
base: [{ name: 'regex' }],
advanced: [
{ name: 'options.plop' },
{ name: 'required' },
{ sectionTitle: null, items: [{ name: 'options.deep' }] },
{ sectionTitle: null, items: [{ name: 'private' }] },
],
},
};
app.customFields.register(field);
expect(app.customFields.get('plugin::myplugin.optionsCustomField')).toEqual(field);
});
it('should register several custom fields at once', () => {
const app = StrapiApp({ middlewares, reducers, library });
const fields = [
@ -290,6 +317,56 @@ describe('ADMIN | StrapiApp', () => {
expect(() => app.customFields.register(field)).toThrowError(/(a|an) .* must be provided/i);
});
it('should validate option path names', () => {
const app = StrapiApp({ middlewares, reducers, library });
const field = {
name: 'test',
pluginId: 'myplugin',
type: 'text',
intlLabel: { id: 'foo', defaultMessage: 'foo' },
intlDescription: { id: 'foo', defaultMessage: 'foo' },
components: {
Input: jest.fn(),
},
options: {
base: [{ name: 'regex' }],
advanced: [{ name: 'plop' }],
},
};
// Test shallow value
expect(() => app.customFields.register(field)).toThrowError(
"'plop' must be prefixed with 'options.'"
);
// Test deep value
field.options.advanced = [{ sectionTitle: null, items: [{ name: 'deep.plop' }] }];
expect(() => app.customFields.register(field)).toThrowError(
"'deep.plop' must be prefixed with 'options.'"
);
});
it('requires options to have a name property', () => {
const app = StrapiApp({ middlewares, reducers, library });
const field = {
name: 'test',
pluginId: 'myplugin',
type: 'text',
intlLabel: { id: 'foo', defaultMessage: 'foo' },
intlDescription: { id: 'foo', defaultMessage: 'foo' },
components: {
Input: jest.fn(),
},
options: {
base: [{ name: 'regex' }],
advanced: [{ boom: 'kapow' }],
},
};
expect(() => app.customFields.register(field)).toThrowError(
"The 'name' property is required on an options object"
);
});
});
describe('Menu api', () => {

View File

@ -290,6 +290,10 @@
"admin.pages.MarketPlacePage.sort.newest": "Newest",
"admin.pages.MarketPlacePage.sort.alphabetical.selected": "Sort by alphabetical order",
"admin.pages.MarketPlacePage.sort.newest.selected": "Sort by newest",
"admin.pages.MarketPlacePage.sort.githubStars": "Number of GitHub stars",
"admin.pages.MarketPlacePage.sort.githubStars.selected": "Sort by GitHub stars",
"admin.pages.MarketPlacePage.sort.npmDownloads": "Number of downloads",
"admin.pages.MarketPlacePage.sort.npmDownloads.selected": "Sort by npm downloads",
"admin.pages.MarketPlacePage.filters.collections": "Collections",
"admin.pages.MarketPlacePage.filters.collectionsSelected": "{count, plural, =0 {No collections} one {# collection} other {# collections}} selected",
"admin.pages.MarketPlacePage.filters.categories": "Categories",

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,13 @@ import produce from 'immer';
import pluralize from 'pluralize';
import set from 'lodash/set';
import snakeCase from 'lodash/snakeCase';
import _ from 'lodash';
import getRelationType from '../../utils/getRelationType';
import nameToSlug from '../../utils/nameToSlug';
import { createComponentUid } from './utils/createUid';
import { shouldPluralizeName, shouldPluralizeTargetAttribute } from './utils/relations';
import * as actions from './constants';
import { customFieldDefaultOptionsReducer } from './utils/customFieldDefaultOptionsReducer';
const initialState = {
formErrors: {},
@ -301,6 +303,19 @@ const reducer = (state = initialState, action) =>
draftState.modifiedData = { ...options, type: customField.type };
const allOptions = [
...(customField?.options?.base || []),
...(customField?.options?.advanced || []),
];
const optionDefaults = allOptions.reduce(customFieldDefaultOptionsReducer, []);
if (optionDefaults.length) {
optionDefaults.forEach(({ name, defaultValue }) =>
_.set(draftState.modifiedData, name, defaultValue)
);
}
break;
}
case actions.SET_DYNAMIC_ZONE_DATA_SCHEMA: {

View File

@ -51,4 +51,58 @@ describe('CTB | components | FormModal | reducer | actions | SET_CUSTOM_FIELD_DA
expect(reducer(initialState, action)).toEqual(expected);
});
it("adds a custom field's default options", () => {
const mockCustomFieldWithOptionsPath = {
...mockCustomField,
options: {
advanced: [
{
name: 'regex',
type: 'text',
defaultValue: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
},
{
sectionTitle: {
id: 'global.settings',
defaultMessage: 'Settings',
},
items: [
{
name: 'required',
type: 'checkbox',
defaultValue: true,
},
{
name: 'options.format',
type: 'text',
defaultValue: 'hex',
},
],
},
],
},
};
const action = {
type: actions.SET_CUSTOM_FIELD_DATA_SCHEMA,
customField: mockCustomFieldWithOptionsPath,
isEditing: false,
modifiedDataToSetForEditing: { name: null },
};
const expected = {
...initialState,
modifiedData: {
type: mockCustomField.type,
required: true,
regex: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
options: {
format: 'hex',
},
},
};
expect(reducer(initialState, action)).toEqual(expected);
});
});

View File

@ -0,0 +1,14 @@
const customFieldDefaultOptionsReducer = (acc, option) => {
if (option.items) {
return option.items.reduce(customFieldDefaultOptionsReducer, acc);
}
if ('defaultValue' in option) {
const { name, defaultValue } = option;
acc.push({ name, defaultValue });
}
return acc;
};
module.exports = { customFieldDefaultOptionsReducer };

View File

@ -54,7 +54,7 @@ const createContentType = (uid, definition) => {
});
} else {
throw new Error(
`Incorrect Content Type UID "${uid}". The UID should start with api::, plugin:: or strapi::.`
`Incorrect Content Type UID "${uid}". The UID should start with api::, plugin:: or admin::.`
);
}

View File

@ -98,7 +98,7 @@
"chokidar": "3.5.2",
"ci-info": "3.5.0",
"cli-table3": "0.6.2",
"commander": "8.2.0",
"commander": "7.2.0",
"configstore": "5.0.1",
"debug": "4.3.2",
"delegates": "1.0.0",

View File

@ -33,6 +33,7 @@ export default {
},
name: 'regex',
type: 'text',
defaultValue: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
description: {
id: getTrad('color-picker.options.advanced.regex.description'),
defaultMessage: 'The text of the regular expression',

View File

@ -42,7 +42,7 @@
"react-router-dom": "5.2.0",
"redux": "^4.0.1",
"reselect": "^4.0.0",
"swagger-ui-dist": "4.12.0",
"swagger-ui-dist": "4.15.5",
"yaml": "1.10.2"
},
"peerDependencies": {

View File

@ -27,7 +27,7 @@
"test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
},
"dependencies": {
"@graphql-tools/schema": "8.1.2",
"@graphql-tools/schema": "8.5.1",
"@graphql-tools/utils": "^8.12.0",
"@strapi/utils": "4.5.2",
"apollo-server-core": "3.1.2",

View File

@ -1,6 +1,6 @@
'use strict';
const { getInitLocale } = require('..');
const { getInitLocale, isoLocales } = require('..');
describe('I18N default locale', () => {
describe('getInitLocale', () => {
@ -23,5 +23,10 @@ describe('I18N default locale', () => {
process.env.STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE = 'zzzzz';
expect(() => getInitLocale()).toThrow();
});
test('Checks if there are any duplicate locales present in the "iso-locales.json" file', () => {
const set = new Set(isoLocales.map((item) => item.code)).size !== isoLocales.length;
expect(set).toBe(false);
});
});
});

View File

@ -591,22 +591,6 @@
"code":"en-GG",
"name":"English (Guernsey) (en-GG)"
},
{
"code":"en-CA",
"name":"English (Canada) (en-CA)"
},
{
"code":"en-EG",
"name":"English (Egypt) (en-EG)"
},
{
"code":"en-EU",
"name":"English (Europe) (en-EU)"
},
{
"code":"en-GU",
"name":"English (Guam) (en-GU)"
},
{
"code":"en-GY",
"name":"English (Guyana) (en-GY)"
@ -667,10 +651,6 @@
"code":"en-MY",
"name":"English (Malaysia) (en-MY)"
},
{
"code":"en-JM",
"name":"English (Jamaica) (en-JM)"
},
{
"code":"en-MT",
"name":"English (Malta) (en-MT)"
@ -719,14 +699,6 @@
"code":"en-NF",
"name":"English (Norfolk Island) (en-NF)"
},
{
"code":"en-NA",
"name":"English (Namibia) (en-NA)"
},
{
"code":"en-NZ",
"name":"English (New Zealand) (en-NZ)"
},
{
"code":"en-MP",
"name":"English (Northern Mariana Islands) (en-MP)"
@ -795,10 +767,6 @@
"code":"en-SB",
"name":"English (Solomon Islands) (en-SB)"
},
{
"code":"en-ZA",
"name":"English (South Africa) (en-ZA)"
},
{
"code":"en-SS",
"name":"English (South Sudan) (en-SS)"
@ -859,26 +827,10 @@
"code":"en-TV",
"name":"English (Tuvalu) (en-TV)"
},
{
"code":"en-PH",
"name":"English (Philippines) (en-PH)"
},
{
"code":"en-SA",
"name":"English (Saudi Arabia) (en-SA)"
},
{
"code":"en-SG",
"name":"English (Singapore) (en-SG)"
},
{
"code":"en-ZA",
"name":"English (South Africa) (en-ZA)"
},
{
"code":"en-TT",
"name":"English (Trinidad and Tobago) (en-TT)"
},
{
"code":"en-AE",
"name":"English (U.A.E.) (en-AE)"

View File

@ -594,22 +594,6 @@ exports[`ISO locales getIsoLocales 1`] = `
"code": "en-GG",
"name": "English (Guernsey) (en-GG)",
},
{
"code": "en-CA",
"name": "English (Canada) (en-CA)",
},
{
"code": "en-EG",
"name": "English (Egypt) (en-EG)",
},
{
"code": "en-EU",
"name": "English (Europe) (en-EU)",
},
{
"code": "en-GU",
"name": "English (Guam) (en-GU)",
},
{
"code": "en-GY",
"name": "English (Guyana) (en-GY)",
@ -670,10 +654,6 @@ exports[`ISO locales getIsoLocales 1`] = `
"code": "en-MY",
"name": "English (Malaysia) (en-MY)",
},
{
"code": "en-JM",
"name": "English (Jamaica) (en-JM)",
},
{
"code": "en-MT",
"name": "English (Malta) (en-MT)",
@ -722,14 +702,6 @@ exports[`ISO locales getIsoLocales 1`] = `
"code": "en-NF",
"name": "English (Norfolk Island) (en-NF)",
},
{
"code": "en-NA",
"name": "English (Namibia) (en-NA)",
},
{
"code": "en-NZ",
"name": "English (New Zealand) (en-NZ)",
},
{
"code": "en-MP",
"name": "English (Northern Mariana Islands) (en-MP)",
@ -798,10 +770,6 @@ exports[`ISO locales getIsoLocales 1`] = `
"code": "en-SB",
"name": "English (Solomon Islands) (en-SB)",
},
{
"code": "en-ZA",
"name": "English (South Africa) (en-ZA)",
},
{
"code": "en-SS",
"name": "English (South Sudan) (en-SS)",
@ -862,26 +830,10 @@ exports[`ISO locales getIsoLocales 1`] = `
"code": "en-TV",
"name": "English (Tuvalu) (en-TV)",
},
{
"code": "en-PH",
"name": "English (Philippines) (en-PH)",
},
{
"code": "en-SA",
"name": "English (Saudi Arabia) (en-SA)",
},
{
"code": "en-SG",
"name": "English (Singapore) (en-SG)",
},
{
"code": "en-ZA",
"name": "English (South Africa) (en-ZA)",
},
{
"code": "en-TT",
"name": "English (Trinidad and Tobago) (en-TT)",
},
{
"code": "en-AE",
"name": "English (U.A.E.) (en-AE)",

View File

@ -37,7 +37,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
"aws-sdk": "2.1215.0",
"aws-sdk": "2.1260.0",
"lodash": "4.17.21"
},
"engines": {

View File

@ -2178,7 +2178,7 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@graphql-tools/merge@8.3.1", "@graphql-tools/merge@^8.0.2":
"@graphql-tools/merge@8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.1.tgz#06121942ad28982a14635dbc87b5d488a041d722"
integrity sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==
@ -2196,16 +2196,6 @@
fast-json-stable-stringify "^2.1.0"
tslib "^2.4.0"
"@graphql-tools/schema@8.1.2":
version "8.1.2"
resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.1.2.tgz#913879da1a7889a9488e9b7dc189e7c83eff74be"
integrity sha512-rX2pg42a0w7JLVYT+f/yeEKpnoZL5PpLq68TxC3iZ8slnNBNjfVfvzzOn8Q8Q6Xw3t17KP9QespmJEDfuQe4Rg==
dependencies:
"@graphql-tools/merge" "^8.0.2"
"@graphql-tools/utils" "^8.1.1"
tslib "~2.3.0"
value-or-promise "1.0.10"
"@graphql-tools/schema@8.5.1", "@graphql-tools/schema@^8.0.0":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.5.1.tgz#c2f2ff1448380919a330312399c9471db2580b58"
@ -2223,7 +2213,7 @@
dependencies:
tslib "^2.4.0"
"@graphql-tools/utils@^8.0.0", "@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.12.0":
"@graphql-tools/utils@^8.0.0", "@graphql-tools/utils@^8.12.0":
version "8.12.0"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.12.0.tgz#243bc4f5fc2edbc9e8fd1038189e57d837cbe31f"
integrity sha512-TeO+MJWGXjUTS52qfK4R8HiPoF/R7X+qmgtOYd8DTH0l6b+5Y/tlg5aGeUJefqImRq7nvi93Ms40k/Uz4D5CWw==
@ -7872,10 +7862,10 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
aws-sdk@2.1215.0:
version "2.1215.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1215.0.tgz#dafc339c2f9039a8f9de30d863a8665716df2ec0"
integrity sha512-btOexIY0O2F+HhjytToaYuub2HEdLqccZSM8rbT3nrbXo7U4k4Gqi6SbMGi2a+vEpj8lY8dAuMR2lvvVs4Ib9Q==
aws-sdk@2.1260.0:
version "2.1260.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1260.0.tgz#3dc18f49dd4aaa18e4e9787c62f1ad02e5aaac4c"
integrity sha512-iciXVukPbhmh44xcF+5/CO15jtESqRkXuEH54XaU8IpCzbYkAcPBaS29vLRN2SRuN1Dy2S3X7SaZZxFJWLAHrg==
dependencies:
buffer "4.9.2"
events "1.1.1"
@ -9398,20 +9388,10 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
commander@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc"
integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA==
commander@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
commander@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.2.0.tgz#37fe2bde301d87d47a53adeff8b5915db1381ca8"
integrity sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==
commander@7.2.0, commander@^7.0.0, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^2.19.0, commander@^2.20.0, commander@^2.20.3:
version "2.20.3"
@ -9428,22 +9408,12 @@ commander@^6.2.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^7.0.0, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.1.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c"
integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==
commander@^9.3.0:
commander@^9.1.0, commander@^9.3.0:
version "9.4.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd"
integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==
@ -21565,10 +21535,10 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
swagger-ui-dist@4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.12.0.tgz#986d90f05e81fb9db3ca40372278a5d8ce71db3a"
integrity sha512-B0Iy2ueXtbByE6OOyHTi3lFQkpPi/L7kFOKFeKTr44za7dJIELa9kzaca6GkndCgpK1QTjArnoXG+aUy0XQp1w==
swagger-ui-dist@4.15.5:
version "4.15.5"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz#cda226a79db2a9192579cc1f37ec839398a62638"
integrity sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==
swap-case@^1.1.0:
version "1.1.2"
@ -22654,11 +22624,6 @@ value-equal@^1.0.1:
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
value-or-promise@1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.10.tgz#5bf041f1e9a8e7043911875547636768a836e446"
integrity sha512-1OwTzvcfXkAfabk60UVr5NdjtjJ0Fg0T5+B1bhxtrOEwSH2fe8y4DnLgoksfCyd8yZCOQQHB0qLMQnwgCjbXLQ==
value-or-promise@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140"