@akemona-org/strapi-plugin-i18n 3.7.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 +19 -0
- package/admin/src/assets/images/logo.svg +1 -0
- package/admin/src/components/CMEditViewCopyLocale/index.js +183 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/cleanData.js +36 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/generateOptions.js +22 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/index.js +2 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/removePasswordAndRelationsFieldFromData.js +54 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/cleanData.test.js +83 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/data.js +219 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/generateOptions.test.js +79 -0
- package/admin/src/components/CMEditViewCopyLocale/utils/tests/removePasswordAndRelationsFieldFromData.test.js +40 -0
- package/admin/src/components/CMEditViewInjectedComponents/index.js +58 -0
- package/admin/src/components/CMEditViewLocalePicker/Option.js +66 -0
- package/admin/src/components/CMEditViewLocalePicker/Wrapper.js +8 -0
- package/admin/src/components/CMEditViewLocalePicker/index.js +160 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/addStatusColorToLocale.js +24 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/createLocalesOption.js +20 -0
- package/admin/src/components/CMEditViewLocalePicker/utils/index.js +2 -0
- package/admin/src/components/CheckboxConfirmation/Wrapper.js +12 -0
- package/admin/src/components/CheckboxConfirmation/index.js +70 -0
- package/admin/src/components/DeleteModalAdditionalInfos/index.js +25 -0
- package/admin/src/components/LocaleList/index.js +101 -0
- package/admin/src/components/LocaleListCell/LocaleListCell.js +90 -0
- package/admin/src/components/LocaleListCell/tests/LocaleListCell.test.js +128 -0
- package/admin/src/components/LocalePicker/index.js +126 -0
- package/admin/src/components/LocaleRow/index.js +77 -0
- package/admin/src/components/ModalCreate/AdvancedForm.js +45 -0
- package/admin/src/components/ModalCreate/BaseForm.js +103 -0
- package/admin/src/components/ModalCreate/index.js +136 -0
- package/admin/src/components/ModalDelete/index.js +49 -0
- package/admin/src/components/ModalEdit/AdvancedForm.js +51 -0
- package/admin/src/components/ModalEdit/BaseForm.js +91 -0
- package/admin/src/components/ModalEdit/index.js +122 -0
- package/admin/src/components/SettingsModal.js +66 -0
- package/admin/src/components/index.js +2 -0
- package/admin/src/containers/Initializer.js +31 -0
- package/admin/src/containers/SettingsPage/LocaleSettingsPage.js +69 -0
- package/admin/src/containers/SettingsPage/index.js +33 -0
- package/admin/src/containers/SettingsPage/tests/SettingsPage.test.js +744 -0
- package/admin/src/containers/SettingsPage/tests/__snapshots__/SettingsPage.test.js.snap +241 -0
- package/admin/src/hooks/constants.js +6 -0
- package/admin/src/hooks/reducers.js +63 -0
- package/admin/src/hooks/tests/reducers.test.js +203 -0
- package/admin/src/hooks/useAddLocale/index.js +60 -0
- package/admin/src/hooks/useContentTypePermissions/index.js +16 -0
- package/admin/src/hooks/useDefaultLocales/index.js +27 -0
- package/admin/src/hooks/useDeleteLocale/index.js +45 -0
- package/admin/src/hooks/useEditLocale/index.js +46 -0
- package/admin/src/hooks/useHasI18n/index.js +13 -0
- package/admin/src/hooks/useLocales/index.js +35 -0
- package/admin/src/index.js +169 -0
- package/admin/src/middlewares/addCommonFieldsToInitialDataMiddleware.js +83 -0
- package/admin/src/middlewares/addLocaleColumnToListViewMiddleware.js +32 -0
- package/admin/src/middlewares/addLocaleToCollectionTypesMiddleware.js +25 -0
- package/admin/src/middlewares/addLocaleToSingleTypesMiddleware.js +25 -0
- package/admin/src/middlewares/extendCMEditViewLayoutMiddleware.js +159 -0
- package/admin/src/middlewares/extendCTBAttributeInitialDataMiddleware.js +58 -0
- package/admin/src/middlewares/extendCTBInitialDataMiddleware.js +33 -0
- package/admin/src/middlewares/index.js +21 -0
- package/admin/src/middlewares/localePermissionMiddleware.js +39 -0
- package/admin/src/middlewares/tests/addCommonFieldsToInitialDataMiddleware.test.js +97 -0
- package/admin/src/middlewares/tests/addLocaleColumnToListViewMiddleware.test.js +68 -0
- package/admin/src/middlewares/tests/addLocaleToCollectionTypesMiddleware.test.js +200 -0
- package/admin/src/middlewares/tests/addLocaleToSingleTypesMiddleware.test.js +193 -0
- package/admin/src/middlewares/tests/extendCMEditViewLayoutMiddleware.test.js +556 -0
- package/admin/src/middlewares/tests/extendCTBAttrributeInitialDataMiddleware.test.js +124 -0
- package/admin/src/middlewares/tests/extendCTBInitialDataMiddleware.test.js +92 -0
- package/admin/src/middlewares/tests/localePermissionMiddleware.test.js +150 -0
- package/admin/src/middlewares/utils/addLocaleToLinksSearch.js +56 -0
- package/admin/src/middlewares/utils/tests/addLocaleToLinksSearch.test.js +137 -0
- package/admin/src/permissions.js +9 -0
- package/admin/src/pluginId.js +5 -0
- package/admin/src/schemas.js +7 -0
- package/admin/src/selectors/selectCollectionTypesRelatedPermissions.js +4 -0
- package/admin/src/selectors/selectI18nLocales.js +3 -0
- package/admin/src/translations/en.json +60 -0
- package/admin/src/translations/fr.json +9 -0
- package/admin/src/translations/index.js +11 -0
- package/admin/src/translations/zh-Hans.json +60 -0
- package/admin/src/utils/getDefaultLocale.js +60 -0
- package/admin/src/utils/getInitialLocale.js +14 -0
- package/admin/src/utils/getLocaleFromQuery.js +7 -0
- package/admin/src/utils/getTrad.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/localizedFields.js +23 -0
- package/admin/src/utils/mutateCTBContentTypeSchema.js +66 -0
- package/admin/src/utils/tests/getDefaultLocale.test.js +337 -0
- package/admin/src/utils/tests/getInitialLocale.test.js +106 -0
- package/admin/src/utils/tests/mutateCTBContentTypeSchema.test.js +205 -0
- package/config/functions/bootstrap.js +57 -0
- package/config/functions/migrations/__tests__/content-type.test.js +255 -0
- package/config/functions/migrations/__tests__/field.test.js +150 -0
- package/config/functions/migrations/content-type/disable/index.js +34 -0
- package/config/functions/migrations/content-type/disable/migrate-for-bookshelf.js +58 -0
- package/config/functions/migrations/content-type/disable/migrate-for-mongoose.js +39 -0
- package/config/functions/migrations/content-type/enable/index.js +40 -0
- package/config/functions/migrations/content-type/utils/index.js +27 -0
- package/config/functions/migrations/field/__tests__/utils.test.js +53 -0
- package/config/functions/migrations/field/index.js +37 -0
- package/config/functions/migrations/field/migrate-for-bookshelf.js +72 -0
- package/config/functions/migrations/field/migrate-for-mongoose.js +24 -0
- package/config/functions/migrations/field/migrate.js +55 -0
- package/config/functions/migrations/field/utils.js +58 -0
- package/config/functions/register.js +46 -0
- package/config/policies/validateLocaleCreation.js +68 -0
- package/config/routes.json +64 -0
- package/constants/__tests__/index.test.js +27 -0
- package/constants/index.js +36 -0
- package/constants/iso-locales.json +2002 -0
- package/controllers/__tests__/content-types.test.js +113 -0
- package/controllers/__tests__/iso-locales.test.js +26 -0
- package/controllers/__tests__/locales.test.js +308 -0
- package/controllers/content-types.js +64 -0
- package/controllers/iso-locales.js +11 -0
- package/controllers/locales.js +104 -0
- package/domain/locale.js +10 -0
- package/middlewares/i18n/defaults.json +5 -0
- package/middlewares/i18n/index.js +28 -0
- package/models/Locale.settings.json +31 -0
- package/oas.yml +195 -0
- package/package.json +31 -0
- package/services/__tests__/__snapshots__/iso-locales.test.js.snap +2006 -0
- package/services/__tests__/content-types.test.js +545 -0
- package/services/__tests__/core-api.test.js +106 -0
- package/services/__tests__/entity-service-decorator.test.js +280 -0
- package/services/__tests__/iso-locales.test.js +11 -0
- package/services/__tests__/locales.test.js +237 -0
- package/services/__tests__/localizations.test.js +187 -0
- package/services/__tests__/metrics.test.js +90 -0
- package/services/content-types.js +200 -0
- package/services/core-api.js +296 -0
- package/services/entity-service-decorator.js +155 -0
- package/services/iso-locales.js +9 -0
- package/services/locales.js +97 -0
- package/services/localizations.js +65 -0
- package/services/metrics.js +24 -0
- package/services/permissions/actions.js +124 -0
- package/services/permissions/engine.js +63 -0
- package/services/permissions/sections-builder.js +48 -0
- package/services/permissions.js +11 -0
- package/tests/content-manager/list-relation.test.e2e.js +122 -0
- package/tests/graphql.test.e2e.js +120 -0
- package/tests/locales.test.e2e.js +414 -0
- package/utils/index.js +20 -0
- package/validation/content-types.js +30 -0
- package/validation/locales.js +39 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sendDidUpdateI18nLocalesEvent, sendDidInitializeEvent } = require('../metrics');
|
|
4
|
+
const { isLocalizedContentType } = require('../content-types');
|
|
5
|
+
|
|
6
|
+
describe('Metrics', () => {
|
|
7
|
+
test('sendDidInitializeEvent', async () => {
|
|
8
|
+
global.strapi = {
|
|
9
|
+
contentTypes: {
|
|
10
|
+
withI18n: {
|
|
11
|
+
pluginOptions: {
|
|
12
|
+
i18n: {
|
|
13
|
+
localized: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
withoutI18n: {
|
|
18
|
+
pluginOptions: {
|
|
19
|
+
i18n: {
|
|
20
|
+
localized: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
withNoOption: {
|
|
25
|
+
pluginOptions: {},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
plugins: {
|
|
29
|
+
i18n: {
|
|
30
|
+
services: {
|
|
31
|
+
['content-types']: {
|
|
32
|
+
isLocalizedContentType,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
telemetry: {
|
|
38
|
+
send: jest.fn(),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
await sendDidInitializeEvent();
|
|
43
|
+
|
|
44
|
+
/* expect(strapi.telemetry.send).toHaveBeenCalledWith('didInitializeI18n', {
|
|
45
|
+
numberOfContentTypes: 1,
|
|
46
|
+
}); */
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('sendDidUpdateI18nLocalesEvent', async () => {
|
|
50
|
+
global.strapi = {
|
|
51
|
+
contentTypes: {
|
|
52
|
+
withI18n: {
|
|
53
|
+
pluginOptions: {
|
|
54
|
+
i18n: {
|
|
55
|
+
localized: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
withoutI18n: {
|
|
60
|
+
pluginOptions: {
|
|
61
|
+
i18n: {
|
|
62
|
+
localized: false,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
withNoOption: {
|
|
67
|
+
pluginOptions: {},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
plugins: {
|
|
71
|
+
i18n: {
|
|
72
|
+
services: {
|
|
73
|
+
locales: {
|
|
74
|
+
count: jest.fn(() => 3),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
telemetry: {
|
|
80
|
+
send: jest.fn(),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await sendDidUpdateI18nLocalesEvent();
|
|
85
|
+
|
|
86
|
+
/* expect(strapi.telemetry.send).toHaveBeenCalledWith('didUpdateI18nLocales', {
|
|
87
|
+
numberOfLocales: 3,
|
|
88
|
+
}); */
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { pick, pipe, has, prop, isNil, cloneDeep, isArray } = require('lodash/fp');
|
|
5
|
+
const { isRelationalAttribute, getVisibleAttributes, isMediaAttribute, isTypedAttribute } =
|
|
6
|
+
require('@akemona-org/strapi-utils').contentTypes;
|
|
7
|
+
const { getService } = require('../utils');
|
|
8
|
+
|
|
9
|
+
const hasLocalizedOption = (modelOrAttribute) => {
|
|
10
|
+
return prop('pluginOptions.i18n.localized', modelOrAttribute) === true;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getValidLocale = async (locale) => {
|
|
14
|
+
const localesService = getService('locales');
|
|
15
|
+
|
|
16
|
+
if (isNil(locale)) {
|
|
17
|
+
return localesService.getDefaultLocale();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const foundLocale = await localesService.findByCode(locale);
|
|
21
|
+
if (!foundLocale) {
|
|
22
|
+
throw new Error('Locale not found');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return locale;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the related entity used for entity creation
|
|
30
|
+
* @param {Object} relatedEntity related entity
|
|
31
|
+
* @returns {id[]} related entity
|
|
32
|
+
*/
|
|
33
|
+
const getNewLocalizationsFrom = async (relatedEntity) => {
|
|
34
|
+
if (relatedEntity) {
|
|
35
|
+
return [relatedEntity.id, ...relatedEntity.localizations.map(prop('id'))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the related entity used for entity creation
|
|
43
|
+
* @param {id} relatedEntityId related entity id
|
|
44
|
+
* @param {string} model corresponding model
|
|
45
|
+
* @param {string} locale locale of the entity to create
|
|
46
|
+
* @returns {Object} related entity
|
|
47
|
+
*/
|
|
48
|
+
const getAndValidateRelatedEntity = async (relatedEntityId, model, locale) => {
|
|
49
|
+
const { kind } = strapi.getModel(model);
|
|
50
|
+
let relatedEntity;
|
|
51
|
+
|
|
52
|
+
if (kind === 'singleType') {
|
|
53
|
+
relatedEntity = await strapi.query(model).findOne({});
|
|
54
|
+
} else if (relatedEntityId) {
|
|
55
|
+
relatedEntity = await strapi.query(model).findOne({ id: relatedEntityId });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (relatedEntityId && !relatedEntity) {
|
|
59
|
+
throw new Error("The related entity doesn't exist");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
relatedEntity &&
|
|
64
|
+
(relatedEntity.locale === locale ||
|
|
65
|
+
relatedEntity.localizations.map(prop('locale')).includes(locale))
|
|
66
|
+
) {
|
|
67
|
+
throw new Error('The entity already exists in this locale');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return relatedEntity;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns whether an attribute is localized or not
|
|
75
|
+
* @param {*} attribute
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
const isLocalizedAttribute = (model, attributeName) => {
|
|
79
|
+
const attribute = model.attributes[attributeName];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
hasLocalizedOption(attribute) ||
|
|
83
|
+
(isRelationalAttribute(attribute) && !isMediaAttribute(attribute)) ||
|
|
84
|
+
isTypedAttribute(attribute, 'uid')
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns whether a model is localized or not
|
|
90
|
+
* @param {*} model
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
const isLocalizedContentType = (model) => {
|
|
94
|
+
return hasLocalizedOption(model);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns the list of attribute names that are not localized
|
|
99
|
+
* @param {object} model
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
const getNonLocalizedAttributes = (model) => {
|
|
103
|
+
return getVisibleAttributes(model).filter(
|
|
104
|
+
(attributeName) => !isLocalizedAttribute(model, attributeName)
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const removeId = (value) => {
|
|
109
|
+
if (typeof value === 'object' && (has('id', value) || has('_id', value))) {
|
|
110
|
+
delete value.id;
|
|
111
|
+
delete value._id;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const removeIds = (model) => (entry) => removeIdsMut(model, cloneDeep(entry));
|
|
116
|
+
|
|
117
|
+
const removeIdsMut = (model, entry) => {
|
|
118
|
+
if (isNil(entry)) {
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
removeId(entry);
|
|
123
|
+
|
|
124
|
+
_.forEach(model.attributes, (attr, attrName) => {
|
|
125
|
+
const value = entry[attrName];
|
|
126
|
+
if (attr.type === 'dynamiczone' && isArray(value)) {
|
|
127
|
+
value.forEach((compo) => {
|
|
128
|
+
if (has('__component', compo)) {
|
|
129
|
+
const model = strapi.components[compo.__component];
|
|
130
|
+
removeIdsMut(model, compo);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} else if (attr.type === 'component') {
|
|
134
|
+
const [model] = strapi.db.getModelsByAttribute(attr);
|
|
135
|
+
if (isArray(value)) {
|
|
136
|
+
value.forEach((compo) => removeIdsMut(model, compo));
|
|
137
|
+
} else {
|
|
138
|
+
removeIdsMut(model, value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return entry;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns a copy of an entry picking only its non localized attributes
|
|
148
|
+
* @param {object} model
|
|
149
|
+
* @param {object} entry
|
|
150
|
+
* @returns {object}
|
|
151
|
+
*/
|
|
152
|
+
const copyNonLocalizedAttributes = (model, entry) => {
|
|
153
|
+
const nonLocalizedAttributes = getNonLocalizedAttributes(model);
|
|
154
|
+
|
|
155
|
+
return pipe(pick(nonLocalizedAttributes), removeIds(model))(entry);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the list of attribute names that are localized
|
|
160
|
+
* @param {object} model
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
const getLocalizedAttributes = (model) => {
|
|
164
|
+
return getVisibleAttributes(model).filter((attributeName) =>
|
|
165
|
+
isLocalizedAttribute(model, attributeName)
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Fill non localized fields of an entry if there are nil
|
|
171
|
+
* @param {Object} entry entry to fill
|
|
172
|
+
* @param {Object} relatedEntry values used to fill
|
|
173
|
+
* @param {Object} options
|
|
174
|
+
* @param {Object} options.model corresponding model
|
|
175
|
+
*/
|
|
176
|
+
const fillNonLocalizedAttributes = (entry, relatedEntry, { model }) => {
|
|
177
|
+
if (isNil(relatedEntry)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const modelDef = strapi.getModel(model);
|
|
182
|
+
const relatedEntryCopy = copyNonLocalizedAttributes(modelDef, relatedEntry);
|
|
183
|
+
|
|
184
|
+
_.forEach(relatedEntryCopy, (value, field) => {
|
|
185
|
+
if (isNil(entry[field])) {
|
|
186
|
+
entry[field] = value;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
isLocalizedContentType,
|
|
193
|
+
getValidLocale,
|
|
194
|
+
getNewLocalizationsFrom,
|
|
195
|
+
getLocalizedAttributes,
|
|
196
|
+
getNonLocalizedAttributes,
|
|
197
|
+
copyNonLocalizedAttributes,
|
|
198
|
+
getAndValidateRelatedEntity,
|
|
199
|
+
fillNonLocalizedAttributes,
|
|
200
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { has, prop, pick, reduce, map, keys, toPath } = require('lodash/fp');
|
|
5
|
+
const { contentTypes, parseMultipartData, sanitizeEntity } = require('@akemona-org/strapi-utils');
|
|
6
|
+
|
|
7
|
+
const { getService } = require('../utils');
|
|
8
|
+
|
|
9
|
+
const { getContentTypeRoutePrefix, isSingleType, getWritableAttributes } = contentTypes;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a parsed request body. It handles whether the body is multipart or not
|
|
13
|
+
* @param {object} ctx - Koa request context
|
|
14
|
+
* @returns {{ data: { [key: string]: any }, files: { [key: string]: any } }}
|
|
15
|
+
*/
|
|
16
|
+
const parseRequest = (ctx) => {
|
|
17
|
+
if (ctx.is('multipart')) {
|
|
18
|
+
return parseMultipartData(ctx);
|
|
19
|
+
} else {
|
|
20
|
+
return { data: ctx.request.body, files: {} };
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns all locales for an entry
|
|
26
|
+
* @param {object} entry
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
const getAllLocales = (entry) => {
|
|
30
|
+
return [entry.locale, ...map(prop('locale'), entry.localizations)];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns all localizations ids for an entry
|
|
35
|
+
* @param {object} entry
|
|
36
|
+
* @returns {any[]}
|
|
37
|
+
*/
|
|
38
|
+
const getAllLocalizationsIds = (entry) => {
|
|
39
|
+
return [entry.id, ...map(prop('id'), entry.localizations)];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns a sanitizer object with a data & a file sanitizer for a content type
|
|
44
|
+
* @param {object} contentType
|
|
45
|
+
* @returns {{
|
|
46
|
+
* sanitizeInput(data: object): object,
|
|
47
|
+
* sanitizeInputFiles(files: object): object
|
|
48
|
+
* }}
|
|
49
|
+
*/
|
|
50
|
+
const createSanitizer = (contentType) => {
|
|
51
|
+
/**
|
|
52
|
+
* Returns the writable attributes of a content type in the localization routes
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
const getAllowedAttributes = () => {
|
|
56
|
+
return getWritableAttributes(contentType).filter(
|
|
57
|
+
(attributeName) => !['locale', 'localizations'].includes(attributeName)
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sanitizes uploaded files to keep only writable ones
|
|
63
|
+
* @param {object} files - input files to sanitize
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
const sanitizeInputFiles = (files) => {
|
|
67
|
+
const allowedFields = getAllowedAttributes();
|
|
68
|
+
return reduce(
|
|
69
|
+
(acc, keyPath) => {
|
|
70
|
+
const [rootKey] = toPath(keyPath);
|
|
71
|
+
if (allowedFields.includes(rootKey)) {
|
|
72
|
+
acc[keyPath] = files[keyPath];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return acc;
|
|
76
|
+
},
|
|
77
|
+
{},
|
|
78
|
+
keys(files)
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sanitizes input data to keep only writable attributes
|
|
84
|
+
* @param {object} data - input data to sanitize
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
const sanitizeInput = (data) => {
|
|
88
|
+
return pick(getAllowedAttributes(), data);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return { sanitizeInput, sanitizeInputFiles };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a handler to handle localizations creation in the core api
|
|
96
|
+
* @param {object} contentType
|
|
97
|
+
* @returns {(object) => void}
|
|
98
|
+
*/
|
|
99
|
+
const createLocalizationHandler = (contentType) => {
|
|
100
|
+
const { copyNonLocalizedAttributes } = getService('content-types');
|
|
101
|
+
|
|
102
|
+
const { sanitizeInput, sanitizeInputFiles } = createSanitizer(contentType);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create localized entry from another one
|
|
106
|
+
*/
|
|
107
|
+
const createFromBaseEntry = async (ctx, entry) => {
|
|
108
|
+
const { data, files } = parseRequest(ctx);
|
|
109
|
+
|
|
110
|
+
const { findByCode } = getService('locales');
|
|
111
|
+
|
|
112
|
+
if (!has('locale', data)) {
|
|
113
|
+
throw strapi.errors.badRequest('locale.missing');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const matchingLocale = await findByCode(data.locale);
|
|
117
|
+
if (!matchingLocale) {
|
|
118
|
+
throw strapi.errors.badRequest('locale.invalid');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const usedLocales = getAllLocales(entry);
|
|
122
|
+
if (usedLocales.includes(data.locale)) {
|
|
123
|
+
throw strapi.errors.badRequest('locale.already.used');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sanitizedData = {
|
|
127
|
+
...copyNonLocalizedAttributes(contentType, entry),
|
|
128
|
+
...sanitizeInput(data),
|
|
129
|
+
locale: data.locale,
|
|
130
|
+
localizations: getAllLocalizationsIds(entry),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const sanitizedFiles = sanitizeInputFiles(files);
|
|
134
|
+
|
|
135
|
+
const newEntry = await strapi.entityService.create(
|
|
136
|
+
{ data: sanitizedData, files: sanitizedFiles },
|
|
137
|
+
{ model: contentType.uid }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
ctx.body = sanitizeEntity(newEntry, { model: strapi.getModel(contentType.uid) });
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (isSingleType(contentType)) {
|
|
144
|
+
return async function (ctx) {
|
|
145
|
+
const entry = await strapi.query(contentType.uid).findOne();
|
|
146
|
+
|
|
147
|
+
if (!entry) {
|
|
148
|
+
throw strapi.errors.notFound('baseEntryId.invalid');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await createFromBaseEntry(ctx, entry);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return async function (ctx) {
|
|
156
|
+
const { id: baseEntryId } = ctx.params;
|
|
157
|
+
|
|
158
|
+
const entry = await strapi.query(contentType.uid).findOne({ id: baseEntryId });
|
|
159
|
+
|
|
160
|
+
if (!entry) {
|
|
161
|
+
throw strapi.errors.notFound('baseEntryId.invalid');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await createFromBaseEntry(ctx, entry);
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Returns a route config to handle localizations creation in the core api
|
|
170
|
+
* @param {object} contentType
|
|
171
|
+
* @returns {{ method: string, path: string, handler: string, config: { policies: string[] }}}
|
|
172
|
+
*/
|
|
173
|
+
const createLocalizationRoute = (contentType) => {
|
|
174
|
+
const { modelName } = contentType;
|
|
175
|
+
|
|
176
|
+
const routePrefix = getContentTypeRoutePrefix(contentType);
|
|
177
|
+
const routePath = isSingleType(contentType)
|
|
178
|
+
? `/${routePrefix}/localizations`
|
|
179
|
+
: `/${routePrefix}/:id/localizations`;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
path: routePath,
|
|
184
|
+
handler: `${modelName}.createLocalization`,
|
|
185
|
+
config: {
|
|
186
|
+
policies: [],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Adds a route & an action to the core api controller of a content type to allow creating new localizations
|
|
193
|
+
* @param {object} contentType
|
|
194
|
+
*/
|
|
195
|
+
const addCreateLocalizationAction = (contentType) => {
|
|
196
|
+
const { modelName, apiName } = contentType;
|
|
197
|
+
|
|
198
|
+
const localizationRoute = createLocalizationRoute(contentType);
|
|
199
|
+
|
|
200
|
+
const coreApiControllerPath = `api.${apiName}.controllers.${modelName}.createLocalization`;
|
|
201
|
+
const handler = createLocalizationHandler(contentType);
|
|
202
|
+
|
|
203
|
+
strapi.config.routes.push(localizationRoute);
|
|
204
|
+
|
|
205
|
+
_.set(strapi, coreApiControllerPath, handler);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const mergeCustomizer = (dest, src) => {
|
|
209
|
+
if (typeof dest === 'string') {
|
|
210
|
+
return `${dest}\n${src}`;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Add a graphql schema to the plugin's global graphl schema to be processed
|
|
216
|
+
* @param {object} schema
|
|
217
|
+
*/
|
|
218
|
+
const addGraphqlSchema = (schema) => {
|
|
219
|
+
_.mergeWith(strapi.plugins.i18n.config.schema.graphql, schema, mergeCustomizer);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Add localization mutation & filters to use with the graphql plugin
|
|
224
|
+
* @param {object} contentType
|
|
225
|
+
*/
|
|
226
|
+
const addGraphqlLocalizationAction = (contentType) => {
|
|
227
|
+
const { globalId, modelName } = contentType;
|
|
228
|
+
|
|
229
|
+
if (!strapi.plugins.graphql) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { toSingular, toPlural } = strapi.plugins.graphql.services.naming;
|
|
234
|
+
|
|
235
|
+
// We use a string instead of an enum as the locales can be changed in the admin
|
|
236
|
+
// NOTE: We could use a custom scalar so the validation becomes dynamic
|
|
237
|
+
const localeArgs = {
|
|
238
|
+
args: {
|
|
239
|
+
locale: 'String',
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// add locale arguments in the existing queries
|
|
244
|
+
if (isSingleType(contentType)) {
|
|
245
|
+
const queryName = toSingular(modelName);
|
|
246
|
+
const mutationSuffix = _.upperFirst(queryName);
|
|
247
|
+
|
|
248
|
+
addGraphqlSchema({
|
|
249
|
+
resolver: {
|
|
250
|
+
Query: {
|
|
251
|
+
[queryName]: localeArgs,
|
|
252
|
+
},
|
|
253
|
+
Mutation: {
|
|
254
|
+
[`update${mutationSuffix}`]: localeArgs,
|
|
255
|
+
[`delete${mutationSuffix}`]: localeArgs,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
const queryName = toPlural(modelName);
|
|
261
|
+
|
|
262
|
+
addGraphqlSchema({
|
|
263
|
+
resolver: {
|
|
264
|
+
Query: {
|
|
265
|
+
[queryName]: localeArgs,
|
|
266
|
+
[`${queryName}Connection`]: localeArgs,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// add new mutation to create a localization
|
|
273
|
+
const typeName = globalId;
|
|
274
|
+
|
|
275
|
+
const capitalizedName = _.upperFirst(toSingular(modelName));
|
|
276
|
+
const mutationName = `create${capitalizedName}Localization`;
|
|
277
|
+
const mutationDef = `${mutationName}(input: update${capitalizedName}Input!): ${typeName}!`;
|
|
278
|
+
const actionName = `${contentType.uid}.createLocalization`;
|
|
279
|
+
|
|
280
|
+
addGraphqlSchema({
|
|
281
|
+
mutation: mutationDef,
|
|
282
|
+
resolver: {
|
|
283
|
+
Mutation: {
|
|
284
|
+
[mutationName]: {
|
|
285
|
+
resolver: actionName,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
addCreateLocalizationAction,
|
|
294
|
+
addGraphqlLocalizationAction,
|
|
295
|
+
createSanitizer,
|
|
296
|
+
};
|