Merge branch 'main' into feature/review-workflow

This commit is contained in:
Gustav Hansen 2023-04-21 11:07:12 +02:00
commit 092a80bc33
265 changed files with 689 additions and 528 deletions

View File

@ -5,7 +5,7 @@ on:
paths: paths:
- '**/admin/src/**.js' - '**/admin/src/**.js'
- '**/ee/admin/**.js' - '**/ee/admin/**.js'
- '**/helper-plugin/lib/src/**.js' - '**/helper-plugin/src/**.js'
- '**/translations/**.json' - '**/translations/**.json'
# Might be too broad, but it runs the action even if a # Might be too broad, but it runs the action even if a

View File

@ -2,24 +2,36 @@
class LocalStorageMock { class LocalStorageMock {
constructor() { constructor() {
this.store = {}; this.store = new Map();
} }
clear() { clear() {
this.store = {}; this.store.clear();
} }
getItem(key) { getItem(key) {
return this.store[key] || null; /**
* We return null to avoid returning `undefined`
* because `undefined` is not a valid JSON value.
*/
return this.store.get(key) ?? null;
} }
setItem(key, value) { setItem(key, value) {
this.store[key] = String(value); this.store.set(key, String(value));
} }
removeItem(key) { removeItem(key) {
delete this.store[key]; this.store.delete(key);
}
get length() {
return this.store.size;
} }
} }
global.localStorage = new LocalStorageMock(); // eslint-disable-next-line no-undef
Object.defineProperty(window, 'localStorage', {
writable: true,
value: new LocalStorageMock(),
});

View File

@ -1,30 +1,39 @@
import { useEffect, useReducer } from 'react'; import { useEffect, useReducer } from 'react';
import { request } from '@strapi/helper-plugin'; import { useFetchClient } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
/**
* TODO: refactor this to use react-query and move it to the `Roles` SettingsPage
*/
const useFetchPermissionsLayout = (id) => { const useFetchPermissionsLayout = (id) => {
const [{ data, error, isLoading }, dispatch] = useReducer(reducer, initialState); const [{ data, error, isLoading }, dispatch] = useReducer(reducer, initialState);
const { get } = useFetchClient();
useEffect(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
dispatch({ try {
type: 'GET_DATA', dispatch({
}); type: 'GET_DATA',
});
const { data } = await request('/admin/permissions', { const {
method: 'GET', data: { data },
params: { role: id }, } = await get('/admin/permissions', {
}); params: { role: id },
});
dispatch({ dispatch({
type: 'GET_DATA_SUCCEEDED', type: 'GET_DATA_SUCCEEDED',
data, data,
}); });
} catch (err) {
// silence is golden
}
}; };
getData(); getData();
}, [id]); }, [id, get]);
return { data, error, isLoading }; return { data, error, isLoading };
}; };

View File

@ -1,13 +1,48 @@
import { useCallback, useReducer, useEffect } from 'react'; import { useCallback, useReducer, useEffect } from 'react';
import { request, useNotification } from '@strapi/helper-plugin'; import { useFetchClient, useNotification } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
const useFetchRole = (id) => { const useFetchRole = (id) => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const { get } = useFetchClient();
useEffect(() => { useEffect(() => {
if (id) { if (id) {
const fetchRole = async (roleId) => {
try {
const [
{
data: { data: role },
},
{
data: { data: permissions },
},
] = await Promise.all(
[`roles/${roleId}`, `roles/${roleId}/permissions`].map((endPoint) =>
get(`/admin/${endPoint}`)
)
);
dispatch({
type: 'GET_DATA_SUCCEEDED',
role,
permissions,
});
} catch (err) {
console.error(err);
dispatch({
type: 'GET_DATA_ERROR',
});
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
};
fetchRole(id); fetchRole(id);
} else { } else {
dispatch({ dispatch({
@ -16,35 +51,7 @@ const useFetchRole = (id) => {
permissions: [], permissions: [],
}); });
} }
}, [get, id, toggleNotification]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const fetchRole = async (roleId) => {
try {
const [{ data: role }, { data: permissions }] = await Promise.all(
[`roles/${roleId}`, `roles/${roleId}/permissions`].map((endPoint) =>
request(`/admin/${endPoint}`, { method: 'GET' })
)
);
dispatch({
type: 'GET_DATA_SUCCEEDED',
role,
permissions,
});
} catch (err) {
console.error(err);
dispatch({
type: 'GET_DATA_ERROR',
});
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
};
const handleSubmitSucceeded = useCallback((data) => { const handleSubmitSucceeded = useCallback((data) => {
dispatch({ dispatch({

View File

@ -1,26 +1,32 @@
import { useReducer, useEffect } from 'react'; import { useReducer, useEffect, useCallback } from 'react';
import { request, useNotification } from '@strapi/helper-plugin'; import { useFetchClient, useNotification } from '@strapi/helper-plugin';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
/**
* TODO: refactor this to not use the `useReducer` hook,
* it's not really necessary. Also use `useQuery`?
*/
const useModels = () => { const useModels = () => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => { const { get } = useFetchClient();
fetchModels();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchModels = async () => { const fetchModels = useCallback(async () => {
dispatch({ dispatch({
type: 'GET_MODELS', type: 'GET_MODELS',
}); });
try { try {
const [{ data: components }, { data: contentTypes }] = await Promise.all( const [
['components', 'content-types'].map((endPoint) => {
request(`/content-manager/${endPoint}`, { method: 'GET' }) data: { data: components },
) },
{
data: { data: contentTypes },
},
] = await Promise.all(
['components', 'content-types'].map((endPoint) => get(`/content-manager/${endPoint}`))
); );
dispatch({ dispatch({
@ -37,7 +43,11 @@ const useModels = () => {
message: { id: 'notification.error' }, message: { id: 'notification.error' },
}); });
} }
}; }, [toggleNotification, get]);
useEffect(() => {
fetchModels();
}, [fetchModels]);
return { return {
...state, ...state,

View File

@ -1,10 +1,15 @@
import { useEffect, useReducer } from 'react'; import { useEffect, useReducer } from 'react';
import { request, useNotification, useOverlayBlocker } from '@strapi/helper-plugin'; import { useFetchClient, useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { checkFormValidity, formatAPIErrors } from '../../utils'; import { checkFormValidity, formatAPIErrors } from '../../utils';
import { initialState, reducer } from './reducer'; import { initialState, reducer } from './reducer';
import init from './init'; import init from './init';
/**
* TODO: refactor this, it's confusing and hard to read.
* It's also only used in `Settings/pages/SingleSignOn` so it can
* probably be deleted and everything written there...
*/
const useSettingsForm = (endPoint, schema, cbSuccess, fieldsToPick) => { const useSettingsForm = (endPoint, schema, cbSuccess, fieldsToPick) => {
const [ const [
{ formErrors, initialData, isLoading, modifiedData, showHeaderButtonLoader, showHeaderLoader }, { formErrors, initialData, isLoading, modifiedData, showHeaderButtonLoader, showHeaderLoader },
@ -13,10 +18,14 @@ const useSettingsForm = (endPoint, schema, cbSuccess, fieldsToPick) => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { lockApp, unlockApp } = useOverlayBlocker(); const { lockApp, unlockApp } = useOverlayBlocker();
const { get, put } = useFetchClient();
useEffect(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
try { try {
const { data } = await request(endPoint, { method: 'GET' }); const {
data: { data },
} = await get(endPoint);
dispatch({ dispatch({
type: 'GET_DATA_SUCCEEDED', type: 'GET_DATA_SUCCEEDED',
@ -85,10 +94,9 @@ const useSettingsForm = (endPoint, schema, cbSuccess, fieldsToPick) => {
cleanedData.roles = cleanedData.roles.map((role) => role.id); cleanedData.roles = cleanedData.roles.map((role) => role.id);
} }
const { data } = await request(endPoint, { const {
method: 'PUT', data: { data },
body: cleanedData, } = await put(endPoint, cleanedData);
});
cbSuccess(data); cbSuccess(data);

View File

@ -6,17 +6,16 @@
import React, { useEffect, useState, useMemo, lazy, Suspense } from 'react'; import React, { useEffect, useState, useMemo, lazy, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import axios from 'axios';
import { import {
LoadingIndicatorPage, LoadingIndicatorPage,
auth, auth,
request,
useNotification, useNotification,
TrackingProvider, TrackingProvider,
prefixFileUrlWithBackendUrl, prefixFileUrlWithBackendUrl,
useAppInfos, useAppInfos,
useFetchClient, useFetchClient,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import axios from 'axios';
import { SkipToContent } from '@strapi/design-system'; import { SkipToContent } from '@strapi/design-system';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import PrivateRoute from '../../components/PrivateRoute'; import PrivateRoute from '../../components/PrivateRoute';
@ -41,7 +40,7 @@ function App() {
hasAdmin: false, hasAdmin: false,
}); });
const appInfo = useAppInfos(); const appInfo = useAppInfos();
const { get } = useFetchClient(); const { get, post } = useFetchClient();
const authRoutes = useMemo(() => { const authRoutes = useMemo(() => {
return makeUniqueRoutes( return makeUniqueRoutes(
@ -57,11 +56,10 @@ function App() {
const renewToken = async () => { const renewToken = async () => {
try { try {
const { const {
data: { token }, data: {
} = await request('/admin/renew-token', { data: { token },
method: 'POST', },
body: { token: currentToken }, } = await post('/admin/renew-token', { token: currentToken });
});
auth.updateToken(token); auth.updateToken(token);
} catch (err) { } catch (err) {
// Refresh app // Refresh app
@ -73,7 +71,7 @@ function App() {
if (currentToken) { if (currentToken) {
renewToken(); renewToken();
} }
}, []); }, [post]);
useEffect(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
@ -82,7 +80,7 @@ function App() {
data: { data: {
data: { hasAdmin, uuid, menuLogo, authLogo }, data: { hasAdmin, uuid, menuLogo, authLogo },
}, },
} = await axios.get(`${strapi.backendURL}/admin/init`); } = await get(`/admin/init`);
updateProjectSettings({ updateProjectSettings({
menuLogo: prefixFileUrlWithBackendUrl(menuLogo), menuLogo: prefixFileUrlWithBackendUrl(menuLogo),
@ -102,20 +100,17 @@ function App() {
setTelemetryProperties(properties); setTelemetryProperties(properties);
try { try {
await fetch('https://analytics.strapi.io/api/v2/track', { /**
method: 'POST', * TODO: remove this call to `axios`
body: JSON.stringify({ */
// This event is anonymous await axios.post('https://analytics.strapi.io/api/v2/track', {
event: 'didInitializeAdministration', // This event is anonymous
userId: '', event: 'didInitializeAdministration',
deviceId, userId: '',
eventPropeties: {}, deviceId,
userProperties: { environment: appInfo.currentEnvironment }, eventPropeties: {},
groupProperties: { ...properties, projectId: uuid }, userProperties: { environment: appInfo.currentEnvironment },
}), groupProperties: { ...properties, projectId: uuid },
headers: {
'Content-Type': 'application/json',
},
}); });
} catch (e) { } catch (e) {
// Silent. // Silent.

View File

@ -43,9 +43,7 @@ const PendingLogoDialog = ({ onClose, asset, prev, next, goTo, setLocalImage, on
})} })}
</Button> </Button>
</Flex> </Flex>
<Box maxWidth={pxToRem(180)}> <Box maxWidth={pxToRem(180)}>{asset.url ? <ImageCardAsset asset={asset} /> : null}</Box>
<ImageCardAsset asset={asset} />
</Box>
</Box> </Box>
<ModalFooter <ModalFooter
startActions={ startActions={

View File

@ -5,7 +5,7 @@ import {
Form, Form,
LoadingIndicatorPage, LoadingIndicatorPage,
SettingsPageTitle, SettingsPageTitle,
request, useFetchClient,
useNotification, useNotification,
useOverlayBlocker, useOverlayBlocker,
useTracking, useTracking,
@ -59,6 +59,8 @@ const CreatePage = () => {
const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout(); const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout();
const { permissions: rolePermissions, isLoading: isRoleLoading } = useFetchRole(id); const { permissions: rolePermissions, isLoading: isRoleLoading } = useFetchRole(id);
const { post, put } = useFetchClient();
const handleCreateRoleSubmit = (data) => { const handleCreateRoleSubmit = (data) => {
lockApp(); lockApp();
setIsSubmiting(true); setIsSubmiting(true);
@ -69,13 +71,8 @@ const CreatePage = () => {
trackUsage('willCreateNewRole'); trackUsage('willCreateNewRole');
} }
Promise.resolve( Promise.resolve(post('/admin/roles', data))
request('/admin/roles', { .then(async ({ data: res }) => {
method: 'POST',
body: data,
})
)
.then(async (res) => {
const { permissionsToSend } = permissionsRef.current.getPermissions(); const { permissionsToSend } = permissionsRef.current.getPermissions();
if (id) { if (id) {
@ -85,10 +82,7 @@ const CreatePage = () => {
} }
if (res.data.id && !isEmpty(permissionsToSend)) { if (res.data.id && !isEmpty(permissionsToSend)) {
await request(`/admin/roles/${res.data.id}/permissions`, { await put(`/admin/roles/${res.data.id}/permissions`, { permissions: permissionsToSend });
method: 'PUT',
body: { permissions: permissionsToSend },
});
} }
return res; return res;

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { import {
request, useFetchClient,
useNotification, useNotification,
useOverlayBlocker, useOverlayBlocker,
useTracking, useTracking,
@ -37,6 +37,8 @@ const EditPage = () => {
onSubmitSucceeded, onSubmitSucceeded,
} = useFetchRole(id); } = useFetchRole(id);
const { put } = useFetchClient();
const handleEditRoleSubmit = async (data) => { const handleEditRoleSubmit = async (data) => {
try { try {
lockApp(); lockApp();
@ -44,17 +46,11 @@ const EditPage = () => {
const { permissionsToSend, didUpdateConditions } = permissionsRef.current.getPermissions(); const { permissionsToSend, didUpdateConditions } = permissionsRef.current.getPermissions();
await request(`/admin/roles/${id}`, { await put(`/admin/roles/${id}`, data);
method: 'PUT',
body: data,
});
if (role.code !== 'strapi-super-admin') { if (role.code !== 'strapi-super-admin') {
await request(`/admin/roles/${id}/permissions`, { await put(`/admin/roles/${id}/permissions`, {
method: 'PUT', permissions: permissionsToSend,
body: {
permissions: permissionsToSend,
},
}); });
if (didUpdateConditions) { if (didUpdateConditions) {

View File

@ -3,12 +3,10 @@
* EditView * EditView
* *
*/ */
import React, { useCallback, useMemo } from 'react'; import * as React from 'react';
import { import {
LoadingIndicatorPage, LoadingIndicatorPage,
request,
SettingsPageTitle, SettingsPageTitle,
to,
useNotification, useNotification,
useOverlayBlocker, useOverlayBlocker,
useFetchClient, useFetchClient,
@ -30,19 +28,20 @@ const EditView = () => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { isLoading: isLoadingForModels, collectionTypes } = useModels(); const { isLoading: isLoadingForModels, collectionTypes } = useModels();
const { post } = useFetchClient(); const { put, get, post } = useFetchClient();
const isCreating = id === 'create'; const isCreating = id === 'create';
const fetchWebhook = useCallback( const { isLoading, data } = useQuery(
async (id) => { ['get-webhook', id],
const [err, { data }] = await to( async () => {
request(`/admin/webhooks/${id}`, { try {
method: 'GET', const {
}) data: { data },
); } = await get(`/admin/webhooks/${id}`);
if (err) { return data;
} catch (err) {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
message: { id: 'notification.error' }, message: { id: 'notification.error' },
@ -50,16 +49,12 @@ const EditView = () => {
return null; return null;
} }
return data;
}, },
[toggleNotification] {
enabled: !isCreating,
}
); );
const { isLoading, data } = useQuery(['get-webhook', id], () => fetchWebhook(id), {
enabled: !isCreating,
});
const { const {
isLoading: isTriggering, isLoading: isTriggering,
data: triggerResponse, data: triggerResponse,
@ -77,25 +72,15 @@ const EditView = () => {
}, },
}); });
const createWebhookMutation = useMutation((body) => const createWebhookMutation = useMutation((body) => post('/admin/webhooks', body));
request('/admin/webhooks', {
method: 'POST',
body,
})
);
const updateWebhookMutation = useMutation(({ id, body }) => const updateWebhookMutation = useMutation(({ id, body }) => put(`/admin/webhooks/${id}`, body));
request(`/admin/webhooks/${id}`, {
method: 'PUT',
body,
})
);
const handleSubmit = async (data) => { const handleSubmit = async (data) => {
if (isCreating) { if (isCreating) {
lockApp(); lockApp();
createWebhookMutation.mutate(cleanData(data), { createWebhookMutation.mutate(cleanData(data), {
onSuccess(result) { onSuccess({ data: result }) {
toggleNotification({ toggleNotification({
type: 'success', type: 'success',
message: { id: 'Settings.webhooks.created' }, message: { id: 'Settings.webhooks.created' },
@ -138,7 +123,7 @@ const EditView = () => {
} }
}; };
const isDraftAndPublishEvents = useMemo( const isDraftAndPublishEvents = React.useMemo(
() => collectionTypes.some((ct) => ct.options.draftAndPublish === true), () => collectionTypes.some((ct) => ct.options.draftAndPublish === true),
[collectionTypes] [collectionTypes]
); );

View File

@ -9,7 +9,7 @@ import React, { useEffect, useReducer, useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { import {
request, useFetchClient,
useRBAC, useRBAC,
LoadingIndicatorPage, LoadingIndicatorPage,
useNotification, useNotification,
@ -63,6 +63,8 @@ const ListView = () => {
); );
const { notifyStatus } = useNotifyAT(); const { notifyStatus } = useNotifyAT();
const { get, del, post, put } = useFetchClient();
useFocusWhenNavigate(); useFocusWhenNavigate();
const { push } = useHistory(); const { push } = useHistory();
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -78,42 +80,45 @@ const ListView = () => {
}; };
}, []); }, []);
/**
* TODO: refactor this, but actually refactor
* the whole component. Needs some love.
*/
useEffect(() => { useEffect(() => {
const fetchWebHooks = async () => {
try {
const {
data: { data },
} = await get('/admin/webhooks');
if (isMounted.current) {
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
notifyStatus('webhooks have been loaded');
}
} catch (err) {
console.log(err);
if (isMounted.current) {
if (err.code !== 20) {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
dispatch({
type: 'TOGGLE_LOADING',
});
}
}
};
if (canRead) { if (canRead) {
fetchWebHooks(); fetchWebHooks();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [canRead, get, notifyStatus, toggleNotification]);
}, [canRead]);
const fetchWebHooks = async () => {
try {
const { data } = await request('/admin/webhooks', {
method: 'GET',
});
if (isMounted.current) {
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
notifyStatus('webhooks have been loaded');
}
} catch (err) {
console.log(err);
if (isMounted.current) {
if (err.code !== 20) {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
dispatch({
type: 'TOGGLE_LOADING',
});
}
}
};
const handleToggleModal = () => { const handleToggleModal = () => {
setShowModal((prev) => !prev); setShowModal((prev) => !prev);
@ -129,15 +134,16 @@ const ListView = () => {
const handleConfirmDeleteOne = async () => { const handleConfirmDeleteOne = async () => {
try { try {
await request(`/admin/webhooks/${webhookToDelete}`, { await del(`/admin/webhooks/${webhookToDelete}`);
method: 'DELETE',
});
dispatch({ dispatch({
type: 'WEBHOOK_DELETED', type: 'WEBHOOK_DELETED',
index: getWebhookIndex(webhookToDelete), index: getWebhookIndex(webhookToDelete),
}); });
} catch (err) { } catch (err) {
/**
* TODO: especially this.
*/
if (err.code !== 20) { if (err.code !== 20) {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
@ -154,10 +160,7 @@ const ListView = () => {
}; };
try { try {
await request('/admin/webhooks/batch-delete', { await post('/admin/webhooks/batch-delete', body);
method: 'POST',
body,
});
if (isMounted.current) { if (isMounted.current) {
dispatch({ dispatch({
@ -207,10 +210,7 @@ const ListView = () => {
value, value,
}); });
await request(`/admin/webhooks/${id}`, { await put(`/admin/webhooks/${id}`, body);
method: 'PUT',
body,
});
} catch (err) { } catch (err) {
if (isMounted.current) { if (isMounted.current) {
dispatch({ dispatch({

View File

@ -3,6 +3,7 @@
"Auth.components.Oops.text": "Votre compte a été suspendu.", "Auth.components.Oops.text": "Votre compte a été suspendu.",
"Auth.components.Oops.text.admin": "Si c'est une erreur, veuillez contacter votre administrateur.", "Auth.components.Oops.text.admin": "Si c'est une erreur, veuillez contacter votre administrateur.",
"Auth.components.Oops.title": "Oups !", "Auth.components.Oops.title": "Oups !",
"Auth.form.active.label": "Actif",
"Auth.form.button.forgot-password": "Envoyer à nouveau", "Auth.form.button.forgot-password": "Envoyer à nouveau",
"Auth.form.button.go-home": "Retour à l'accueil", "Auth.form.button.go-home": "Retour à l'accueil",
"Auth.form.button.login": "Se connecter", "Auth.form.button.login": "Se connecter",
@ -54,6 +55,7 @@
"Auth.login.sso.subtitle": "Vous connecter via SSO", "Auth.login.sso.subtitle": "Vous connecter via SSO",
"Auth.privacy-policy-agreement.policy": "la politique de confidentialité", "Auth.privacy-policy-agreement.policy": "la politique de confidentialité",
"Auth.privacy-policy-agreement.terms": "termes", "Auth.privacy-policy-agreement.terms": "termes",
"Auth.reset-password.title": "Réinitialiser le mot de passe",
"Content Manager": "Content Manager", "Content Manager": "Content Manager",
"Content Type Builder": "Content Types Builder", "Content Type Builder": "Content Types Builder",
"Documentation": "Documentation", "Documentation": "Documentation",
@ -74,22 +76,95 @@
"Roles.ListPage.notification.delete-all-not-allowed": "Certains rôles n'ont pas pu être supprimés car ils sont associés à des utilisateurs.", "Roles.ListPage.notification.delete-all-not-allowed": "Certains rôles n'ont pas pu être supprimés car ils sont associés à des utilisateurs.",
"Roles.ListPage.notification.delete-not-allowed": "Un rôle ne peu pas être supprimé s'il est associé à des utilisateurs.", "Roles.ListPage.notification.delete-not-allowed": "Un rôle ne peu pas être supprimé s'il est associé à des utilisateurs.",
"Roles.RoleRow.select-all": "Sélectionner {name} pour action groupée", "Roles.RoleRow.select-all": "Sélectionner {name} pour action groupée",
"Roles.RoleRow.user-count": "{number, plural, =0 {# utilisateur} one {# utilisateur} other {# utilisateurs}}",
"Roles.components.List.empty.withSearch": "Il n'y a pas de rôles correspondant à la recherche ({search})...", "Roles.components.List.empty.withSearch": "Il n'y a pas de rôles correspondant à la recherche ({search})...",
"Settings.PageTitle": "Réglages - {name}", "Settings.PageTitle": "Réglages - {name}",
"Settings.apiTokens.ListView.headers.createdAt": "Créé le",
"Settings.apiTokens.ListView.headers.description": "Description",
"Settings.apiTokens.ListView.headers.lastUsedAt": "Dernière utilisation le",
"Settings.apiTokens.ListView.headers.name": "Nom",
"Settings.apiTokens.ListView.headers.type": "Type de jeton",
"Settings.apiTokens.regenerate": "Régénérer",
"Settings.apiTokens.createPage.title": "Créer un jeton d'API",
"Settings.transferTokens.createPage.title": "Créer un jeton de transfert",
"Settings.tokens.RegenerateDialog.title": "Régénérer le jeton",
"Settings.apiTokens.addFirstToken": "Ajouter votre premier jeton d'API", "Settings.apiTokens.addFirstToken": "Ajouter votre premier jeton d'API",
"Settings.apiTokens.addNewToken": "Ajouter un nouveau jeton d'API", "Settings.apiTokens.addNewToken": "Ajouter un nouveau jeton d'API",
"Settings.tokens.copy.editMessage": "Pour des raisons de sécurité, vous ne pouvoir voir votre jeton qu'une seule fois", "Settings.tokens.copy.editMessage": "Pour des raisons de sécurité, vous ne pouvoir voir votre jeton qu'une seule fois",
"Settings.tokens.copy.editTitle": "Ce jeton n'est désormais plus accessible", "Settings.tokens.copy.editTitle": "Ce jeton n'est désormais plus accessible",
"Settings.tokens.copy.lastWarning": "Assurez-vous de copier ce jeton, vous ne pourrez plus le revoir par la suite !", "Settings.tokens.copy.lastWarning": "Assurez-vous de copier ce jeton, vous ne pourrez plus le revoir par la suite !",
"Settings.apiTokens.create": "Ajouter une entrée", "Settings.apiTokens.create": "Ajouter une entrée",
"Settings.apiTokens.createPage.permissions.description": "Seules les actions rattachées à une route sont listées ci-dessous.",
"Settings.apiTokens.createPage.permissions.title": "Permissions",
"Settings.apiTokens.description": "Liste des jetons générés pour consommer l'API", "Settings.apiTokens.description": "Liste des jetons générés pour consommer l'API",
"Settings.apiTokens.createPage.BoundRoute.title": "Route rattachée à",
"Settings.apiTokens.createPage.permissions.header.title": "Paramètres avancés",
"Settings.apiTokens.createPage.permissions.header.hint": "Sélectionner les actions de l'application ou du plugin et sur l'icône de la roue crantée pour afficher la route rattachée",
"Settings.tokens.duration.30-days": "30 jours",
"Settings.tokens.duration.7-days": "7 jours",
"Settings.tokens.duration.90-days": "90 jours",
"Settings.tokens.duration.expiration-date": "Date d'expiration",
"Settings.tokens.duration.unlimited": "Illimité",
"Settings.apiTokens.emptyStateLayout": "Vous n'avez pas encore de contenu...", "Settings.apiTokens.emptyStateLayout": "Vous n'avez pas encore de contenu...",
"Settings.tokens.form.duration": "Durée de vie du jeton",
"Settings.tokens.form.type": "Type de jeton",
"Settings.tokens.form.name": "Nom",
"Settings.tokens.form.description": "Description",
"Settings.tokens.notification.copied": "Jeton copié dans le press-papiers.", "Settings.tokens.notification.copied": "Jeton copié dans le press-papiers.",
"Settings.apiTokens.title": "Jetons d'API", "Settings.tokens.popUpWarning.message": "Êtes-vous sûr(e) de vouloir régénérer ce jeton ?",
"Settings.tokens.Button.cancel": "Annuler",
"Settings.tokens.Button.regenerate": "Régénérer",
"Settings.tokens.types.full-access": "Accès total", "Settings.tokens.types.full-access": "Accès total",
"Settings.tokens.types.read-only": "Lecture seule", "Settings.tokens.types.read-only": "Lecture seule",
"Settings.tokens.types.custom": "Custom",
"Settings.tokens.regenerate": "Régénérer",
"Settings.transferTokens.title": "Jetons de transfert",
"Settings.transferTokens.description": "Liste des jetons de transfert générés",
"Settings.transferTokens.create": "Créer un nouveau jeton de transfert",
"Settings.transferTokens.addFirstToken": "Ajouter votre premier jeton de transfert",
"Settings.transferTokens.addNewToken": "Ajouter un nouveau jeton de transfert",
"Settings.transferTokens.emptyStateLayout": "Vous n'avez aucun contenu pour le moment...",
"Settings.tokens.ListView.headers.name": "Nom",
"Settings.tokens.ListView.headers.description": "Description",
"Settings.transferTokens.ListView.headers.type": "Type de jeton",
"Settings.tokens.ListView.headers.createdAt": "Créé le",
"Settings.tokens.ListView.headers.lastUsedAt": "Dernière utilisation",
"Settings.application.ee.admin-seats.count": "<text>{enforcementUserCount}</text>/{permittedSeats}",
"Settings.application.ee.admin-seats.at-limit-tooltip": "Limite atteinte : ajouter des places pour inviter d'autres utilisateurs",
"Settings.application.ee.admin-seats.add-seats": "{isHostedOnStrapiCloud, select, true {AJouter des places} other {Contacter le service clients}}",
"Settings.application.customization": "Customisation",
"Settings.application.customization.auth-logo.carousel-hint": "Remplacer le logo dans la page de connexion",
"Settings.application.customization.carousel-hint": "Changer le logo dans l'interface d'administration (dimensions maximales: {dimension}x{dimension}, poids maximal du fichier : {size}KB)",
"Settings.application.customization.carousel-slide.label": "Logo slide",
"Settings.application.customization.carousel.auth-logo.title": "Logo de connexion",
"Settings.application.customization.carousel.change-action": "Changer le logo",
"Settings.application.customization.carousel.menu-logo.title": "Logo du menu",
"Settings.application.customization.carousel.reset-action": "Réinitialiser le logo",
"Settings.application.customization.carousel.title": "Logo",
"Settings.application.customization.menu-logo.carousel-hint": "Remplacer le logo dans la navigation principale",
"Settings.application.customization.modal.cancel": "Annuler",
"Settings.application.customization.modal.pending": "Téléchargement du logo",
"Settings.application.customization.modal.pending.card-badge": "image",
"Settings.application.customization.modal.pending.choose-another": "Choisir un autre logo",
"Settings.application.customization.modal.pending.subtitle": "Gérer le logo choisi avant de le télécharger",
"Settings.application.customization.modal.pending.title": "Logo prêt pour le téléchargement",
"Settings.application.customization.modal.pending.upload": "Téléchargement du logo",
"Settings.application.customization.modal.tab.label": "Comment voulez-vous télécharger vos medias ?",
"Settings.application.customization.modal.upload": "Télécharger le logo",
"Settings.application.customization.modal.upload.cta.browse": "Explorer les fichiers",
"Settings.application.customization.modal.upload.drag-drop": "Glisser-déposer ici ou",
"Settings.application.customization.modal.upload.error-format": "Mauvais format chargé (formats acceptés : jpeg, jpg, png, svg).",
"Settings.application.customization.modal.upload.error-network": "Erreur réseau",
"Settings.application.customization.modal.upload.error-size": "Le fichier téléchargé est trop grand (dimensions max : {dimension}x{dimension}, poids max: {size}KB)",
"Settings.application.customization.modal.upload.file-validation": "Dimensions maximales : {dimension}x{dimension}, poids maximal : {size}KB",
"Settings.application.customization.modal.upload.from-computer": "Depuis l'ordinateur",
"Settings.application.customization.modal.upload.from-url": "Depuis une URL",
"Settings.application.customization.modal.upload.from-url.input-label": "URL",
"Settings.application.customization.modal.upload.next": "Suivant",
"Settings.application.customization.size-details": "Dimensions maximales : {dimension}×{dimension}, poids maximal : {size}KB",
"Settings.application.description": "Informations globales du panneau d'administration", "Settings.application.description": "Informations globales du panneau d'administration",
"Settings.application.edition-title": "plan actuel", "Settings.application.edition-title": "plan actuel",
"Settings.application.ee-or-ce": "{communityEdition, select, true {Édition Communauté} other {Édition Entreprise}}",
"Settings.application.get-help": "Obtenir de l'aide", "Settings.application.get-help": "Obtenir de l'aide",
"Settings.application.link-pricing": "Voir tous les tarifs", "Settings.application.link-pricing": "Voir tous les tarifs",
"Settings.application.link-upgrade": "Mettez à niveau votre panneau d'administration", "Settings.application.link-upgrade": "Mettez à niveau votre panneau d'administration",
@ -566,6 +641,14 @@
"content-manager.plugin.description.long": "Visualisez, modifiez et supprimez les données de votre base de données.", "content-manager.plugin.description.long": "Visualisez, modifiez et supprimez les données de votre base de données.",
"content-manager.plugin.description.short": "Visualisez, modifiez et supprimez les données de votre base de données.", "content-manager.plugin.description.short": "Visualisez, modifiez et supprimez les données de votre base de données.",
"content-manager.popover.display-relations.label": "Afficher les relations", "content-manager.popover.display-relations.label": "Afficher les relations",
"content-manager.relation.add": "Ajouter une relation",
"content-manager.relation.disconnect": "Supprimer",
"content-manager.relation.isLoading": "Chargement des relations en cours",
"content-manager.relation.loadMore": "Charger davantage",
"content-manager.relation.notAvailable": "Aucune relation disponible",
"content-manager.relation.publicationState.draft": "Brouillon",
"content-manager.relation.publicationState.published": "Publiée",
"content-manager.select.currently.selected": "{count} actuellement sélectionnées",
"content-manager.success.record.delete": "Supprimé", "content-manager.success.record.delete": "Supprimé",
"content-manager.success.record.publish": "Publié", "content-manager.success.record.publish": "Publié",
"content-manager.success.record.save": "Sauvegardé", "content-manager.success.record.save": "Sauvegardé",
@ -575,9 +658,11 @@
"content-manager.popUpWarning.warning.publish-question": "Êtes-vous sûr de vouloir le publier ?", "content-manager.popUpWarning.warning.publish-question": "Êtes-vous sûr de vouloir le publier ?",
"content-manager.popUpwarning.warning.has-draft-relations.button-confirm": "Oui, publier", "content-manager.popUpwarning.warning.has-draft-relations.button-confirm": "Oui, publier",
"content-manager.popUpwarning.warning.has-draft-relations.message": "<b>{count, plural, =0 { des relations de votre contenu n'est} one { des relations de votre contenu n'est} other { des relations de votre contenu ne sont}}</b> pas publié actuellement.<br></br>Cela peut engendrer des liens cassés ou des erreurs dans votre projet.", "content-manager.popUpwarning.warning.has-draft-relations.message": "<b>{count, plural, =0 { des relations de votre contenu n'est} one { des relations de votre contenu n'est} other { des relations de votre contenu ne sont}}</b> pas publié actuellement.<br></br>Cela peut engendrer des liens cassés ou des erreurs dans votre projet.",
"dark": "Sombre",
"form.button.continue": "Continuer", "form.button.continue": "Continuer",
"global.search": "Rechercher", "global.search": "Rechercher",
"global.actions": "Actions", "global.actions": "Actions",
"global.auditLogs": "Journaux d'audit",
"global.back": "Retour", "global.back": "Retour",
"global.cancel": "Annuler", "global.cancel": "Annuler",
"global.change-password": "Modifier le mot de passe", "global.change-password": "Modifier le mot de passe",
@ -606,6 +691,7 @@
"global.settings": "Paramètres", "global.settings": "Paramètres",
"global.type": "Type", "global.type": "Type",
"global.users": "Utilisateurs", "global.users": "Utilisateurs",
"light": "Clair",
"form.button.done": "Terminer", "form.button.done": "Terminer",
"global.prompt.unsaved": "Êtes-vous sûr de vouloir quitter cette page? Toutes vos modifications seront perdues", "global.prompt.unsaved": "Êtes-vous sûr de vouloir quitter cette page? Toutes vos modifications seront perdues",
"notification.contentType.relations.conflict": "Le Type de Contenu à des relations qui rentrent en conflit", "notification.contentType.relations.conflict": "Le Type de Contenu à des relations qui rentrent en conflit",
@ -621,8 +707,10 @@
"notification.success.title": "Succès :", "notification.success.title": "Succès :",
"notification.version.update.message": "Une nouvelle version de Strapi est disponible !", "notification.version.update.message": "Une nouvelle version de Strapi est disponible !",
"notification.warning.title": "Attention :", "notification.warning.title": "Attention :",
"notification.warning.404": "404 - Introuvable",
"or": "OU", "or": "OU",
"request.error.model.unknown": "Le model n'existe pas", "request.error.model.unknown": "Le model n'existe pas",
"selectButtonTitle": "Sélectionner",
"skipToContent": "Aller au contenu", "skipToContent": "Aller au contenu",
"submit": "Soumettre" "submit": "Soumettre"
} }

View File

@ -1,5 +1,5 @@
import { useReducer, useEffect } from 'react'; import { useReducer, useEffect } from 'react';
import { request, useNotification } from '@strapi/helper-plugin'; import { useFetchClient, useNotification } from '@strapi/helper-plugin';
import { getRequestUrl } from '../../../../admin/src/utils'; import { getRequestUrl } from '../../../../admin/src/utils';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
@ -7,43 +7,42 @@ import reducer, { initialState } from './reducer';
const useAuthProviders = ({ ssoEnabled }) => { const useAuthProviders = ({ ssoEnabled }) => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { get } = useFetchClient();
useEffect(() => { useEffect(() => {
fetchAuthProviders(); const fetchAuthProviders = async () => {
// eslint-disable-next-line react-hooks/exhaustive-deps try {
}, []); if (!ssoEnabled) {
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: [],
});
return;
}
const { data } = await get(getRequestUrl('providers'));
const fetchAuthProviders = async () => {
try {
if (!ssoEnabled) {
dispatch({ dispatch({
type: 'GET_DATA_SUCCEEDED', type: 'GET_DATA_SUCCEEDED',
data: [], data,
});
} catch (err) {
console.error(err);
dispatch({
type: 'GET_DATA_ERROR',
}); });
return; toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
} }
};
const requestUrl = getRequestUrl('providers'); fetchAuthProviders();
const data = await request(requestUrl, { method: 'GET' }); }, [get, ssoEnabled, toggleNotification]);
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
} catch (err) {
console.error(err);
dispatch({
type: 'GET_DATA_ERROR',
});
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
};
return state; return state;
}; };

View File

@ -2,7 +2,7 @@ import React, { useEffect, useRef, useCallback } from 'react';
import { useHistory, useRouteMatch } from 'react-router-dom'; import { useHistory, useRouteMatch } from 'react-router-dom';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { auth, LoadingIndicatorPage, request } from '@strapi/helper-plugin'; import { auth, LoadingIndicatorPage, useFetchClient } from '@strapi/helper-plugin';
import { getRequestUrl } from '../../../../admin/src/utils'; import { getRequestUrl } from '../../../../admin/src/utils';
const AuthResponse = () => { const AuthResponse = () => {
@ -24,6 +24,8 @@ const AuthResponse = () => {
); );
}, [push]); }, [push]);
const { get } = useFetchClient();
const fetchUserInfo = useCallback(async () => { const fetchUserInfo = useCallback(async () => {
try { try {
const jwtToken = Cookies.get('jwtToken'); const jwtToken = Cookies.get('jwtToken');
@ -33,7 +35,9 @@ const AuthResponse = () => {
if (jwtToken) { if (jwtToken) {
auth.setToken(jwtToken, true); auth.setToken(jwtToken, true);
const requestUrl = getRequestUrl('users/me'); const requestUrl = getRequestUrl('users/me');
const { data } = await request(requestUrl, { method: 'GET' }); const {
data: { data },
} = await get(requestUrl);
auth.setUserInfo(data, true); auth.setUserInfo(data, true);
@ -44,7 +48,7 @@ const AuthResponse = () => {
} catch (e) { } catch (e) {
redirectToOops(); redirectToOops();
} }
}, [push, redirectToOops]); }, [get, push, redirectToOops]);
useEffect(() => { useEffect(() => {
if (authResponse === 'error') { if (authResponse === 'error') {

View File

@ -154,6 +154,13 @@ describe('API Token Auth Strategy', () => {
})), })),
}; };
const strapiMock = {
container,
telemetry: {
send: jest.fn(),
},
};
// mock ability.can (since normally it only gets added to credentials in authenticate) // mock ability.can (since normally it only gets added to credentials in authenticate)
const ability = { const ability = {
can: jest.fn((ability) => { can: jest.fn((ability) => {
@ -163,9 +170,7 @@ describe('API Token Auth Strategy', () => {
}; };
test('Verify read-only access', () => { test('Verify read-only access', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect( expect(
apiTokenStrategy.verify( apiTokenStrategy.verify(
@ -176,9 +181,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Verify full-access access', () => { test('Verify full-access access', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect( expect(
apiTokenStrategy.verify( apiTokenStrategy.verify(
@ -189,9 +192,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Verify custom access', async () => { test('Verify custom access', async () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect( expect(
apiTokenStrategy.verify( apiTokenStrategy.verify(
@ -202,9 +203,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Verify with expiration in future', () => { test('Verify with expiration in future', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect( expect(
apiTokenStrategy.verify( apiTokenStrategy.verify(
@ -220,9 +219,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Throws with expired token', () => { test('Throws with expired token', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect(() => { expect(() => {
apiTokenStrategy.verify( apiTokenStrategy.verify(
@ -238,9 +235,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Throws an error if trying to access a `full-access` action with a read only access key', () => { test('Throws an error if trying to access a `full-access` action with a read only access key', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect.assertions(1); expect.assertions(1);
@ -255,9 +250,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Throws an error if trying to access an action with a custom access key without the permission', () => { test('Throws an error if trying to access an action with a custom access key without the permission', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect.assertions(1); expect.assertions(1);
@ -272,9 +265,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Throws an error if the credentials are not passed in the auth object', () => { test('Throws an error if the credentials are not passed in the auth object', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect.assertions(1); expect.assertions(1);
@ -286,17 +277,13 @@ describe('API Token Auth Strategy', () => {
}); });
test('A `full-access` token is needed when no scope is passed', () => { test('A `full-access` token is needed when no scope is passed', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect(apiTokenStrategy.verify({ credentials: fullAccessApiToken }, {})).toBeUndefined(); expect(apiTokenStrategy.verify({ credentials: fullAccessApiToken }, {})).toBeUndefined();
}); });
test('Throws an error if no scope is passed with a `read-only` token', () => { test('Throws an error if no scope is passed with a `read-only` token', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect.assertions(1); expect.assertions(1);
@ -308,9 +295,7 @@ describe('API Token Auth Strategy', () => {
}); });
test('Throws an error if no scope is passed with a `custom` token', () => { test('Throws an error if no scope is passed with a `custom` token', () => {
global.strapi = { global.strapi = strapiMock;
container,
};
expect.assertions(1); expect.assertions(1);

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const { castArray, isNil } = require('lodash/fp'); const { castArray, isNil, isEmpty } = require('lodash/fp');
const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors; const { UnauthorizedError, ForbiddenError } = require('@strapi/utils').errors;
const constants = require('../services/constants'); const constants = require('../services/constants');
const { getService } = require('../utils'); const { getService } = require('../utils');
@ -78,6 +78,13 @@ const authenticate = async (ctx) => {
const verify = (auth, config) => { const verify = (auth, config) => {
const { credentials: apiToken, ability } = auth; const { credentials: apiToken, ability } = auth;
strapi.telemetry.send('didReceiveAPIRequest', {
eventProperties: {
authenticationMethod: auth?.strategy?.name || 'api-token',
isAuthenticated: !isEmpty(apiToken),
},
});
if (!apiToken) { if (!apiToken) {
throw new UnauthorizedError('Token not found'); throw new UnauthorizedError('Token not found');
} }

View File

@ -415,7 +415,7 @@ const baseForm = {
...nameField, ...nameField,
placeholder: { placeholder: {
id: getTrad('modalForm.attribute.form.base.name.placeholder'), id: getTrad('modalForm.attribute.form.base.name.placeholder'),
defaultMessage: 'e.g. Slug, SEO URL, Canonical URL', defaultMessage: 'e.g. slug, seoUrl, canonicalUrl',
}, },
}, },
{ {

View File

@ -134,7 +134,7 @@
"menu.section.models.name": "Sammlungen", "menu.section.models.name": "Sammlungen",
"menu.section.single-types.name": "Einzel-Einträge", "menu.section.single-types.name": "Einzel-Einträge",
"modalForm.attribute.form.base.name.description": "Leerzeichen sind im Name eines Attributs nicht erlaubt", "modalForm.attribute.form.base.name.description": "Leerzeichen sind im Name eines Attributs nicht erlaubt",
"modalForm.attribute.form.base.name.placeholder": "z.B. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "z.B. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Verknüpftes Feld", "modalForm.attribute.target-field": "Verknüpftes Feld",
"modalForm.attributes.select-component": "Wähle eine Komponente", "modalForm.attributes.select-component": "Wähle eine Komponente",
"modalForm.attributes.select-components": "Wähle die Komponenten", "modalForm.attributes.select-components": "Wähle die Komponenten",

View File

@ -125,7 +125,7 @@
"from": "fra", "from": "fra",
"listView.headerLayout.description": "Byg datastrukturen for dit indhold", "listView.headerLayout.description": "Byg datastrukturen for dit indhold",
"modalForm.attribute.form.base.name.description": "Mellemrum er ikke tilladt i navnet", "modalForm.attribute.form.base.name.description": "Mellemrum er ikke tilladt i navnet",
"modalForm.attribute.form.base.name.placeholder": "f.eks. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "f.eks. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Vedhæftet felt", "modalForm.attribute.target-field": "Vedhæftet felt",
"modalForm.attributes.select-component": "Vælg et komponent", "modalForm.attributes.select-component": "Vælg et komponent",
"modalForm.attributes.select-components": "Vælg komponenterne", "modalForm.attributes.select-components": "Vælg komponenterne",

View File

@ -137,7 +137,7 @@
"menu.section.models.name": "Collection Types", "menu.section.models.name": "Collection Types",
"menu.section.single-types.name": "Single Types", "menu.section.single-types.name": "Single Types",
"modalForm.attribute.form.base.name.description": "No space is allowed for the name of the attribute", "modalForm.attribute.form.base.name.description": "No space is allowed for the name of the attribute",
"modalForm.attribute.form.base.name.placeholder": "e.g. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "e.g. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Attached field", "modalForm.attribute.target-field": "Attached field",
"modalForm.attributes.select-component": "Select a component", "modalForm.attributes.select-component": "Select a component",
"modalForm.attributes.select-components": "Select the components", "modalForm.attributes.select-components": "Select the components",

View File

@ -125,7 +125,7 @@
"from": "de", "from": "de",
"listView.headerLayout.description": "Construya la arquitectura de datos de su contenido", "listView.headerLayout.description": "Construya la arquitectura de datos de su contenido",
"modalForm.attribute.form.base.name.description": "No se permiten espacios para el nombre del atributo", "modalForm.attribute.form.base.name.description": "No se permiten espacios para el nombre del atributo",
"modalForm.attribute.form.base.name.placeholder": "p. ej. Slug, URL SEO, URL canónica", "modalForm.attribute.form.base.name.placeholder": "p. ej. slug, urlSeo, urlCanónica",
"modalForm.attribute.target-field": "Campo adjunto", "modalForm.attribute.target-field": "Campo adjunto",
"modalForm.attributes.select-component": "Seleccione un componente", "modalForm.attributes.select-component": "Seleccione un componente",
"modalForm.attributes.select-components": "Seleccionar los componentes", "modalForm.attributes.select-components": "Seleccionar los componentes",

View File

@ -43,7 +43,7 @@
"form.button.cancel": "Annuler", "form.button.cancel": "Annuler",
"form.button.configure-view": "Configurer la vue", "form.button.configure-view": "Configurer la vue",
"from": "de", "from": "de",
"modalForm.attribute.form.base.name.placeholder": "ex : Slug, URL SEO, URL Canonique", "modalForm.attribute.form.base.name.placeholder": "ex : slug, urlSeo, urlCanonique",
"modalForm.attribute.target-field": "Champ associé", "modalForm.attribute.target-field": "Champ associé",
"modalForm.singleType.header-create": "Créer un single type", "modalForm.singleType.header-create": "Créer un single type",
"modalForm.sub-header.chooseAttribute.collectionType": "Selectionnez un champ pour votre collection", "modalForm.sub-header.chooseAttribute.collectionType": "Selectionnez un champ pour votre collection",

View File

@ -114,7 +114,7 @@
"form.button.single-type.description": "Terbaik untuk satu contoh seperti tentang kami, beranda, dll.", "form.button.single-type.description": "Terbaik untuk satu contoh seperti tentang kami, beranda, dll.",
"from": "dari", "from": "dari",
"modalForm.attribute.form.base.name.description": "Tidak ada spasi yang diperbolehkan untuk nama atribut", "modalForm.attribute.form.base.name.description": "Tidak ada spasi yang diperbolehkan untuk nama atribut",
"modalForm.attribute.form.base.name.placeholder": "Misalnya Siput, URL SEO, URL Kanonis", "modalForm.attribute.form.base.name.placeholder": "Misalnya Siput, urlSeo, urlKanonis",
"modalForm.attribute.target-field": "Bidang terlampir", "modalForm.attribute.target-field": "Bidang terlampir",
"modalForm.attributes.select-component": "Pilih komponen", "modalForm.attributes.select-component": "Pilih komponen",
"modalForm.attributes.select-components": "Pilih komponen", "modalForm.attributes.select-components": "Pilih komponen",

View File

@ -115,7 +115,7 @@
"form.button.single-type.description": "Indicato per entità uniche come home page, chi siamo, ecc...", "form.button.single-type.description": "Indicato per entità uniche come home page, chi siamo, ecc...",
"from": "da", "from": "da",
"modalForm.attribute.form.base.name.description": "Spazi non ammessi per il nome dell'attributo", "modalForm.attribute.form.base.name.description": "Spazi non ammessi per il nome dell'attributo",
"modalForm.attribute.form.base.name.placeholder": "Es: Slug, URL SEO, URL Canonico", "modalForm.attribute.form.base.name.placeholder": "Es: slug, urlSeo, urlCanonico",
"modalForm.attribute.target-field": "Campo collegato", "modalForm.attribute.target-field": "Campo collegato",
"modalForm.attributes.select-component": "Seleziona un componente", "modalForm.attributes.select-component": "Seleziona un componente",
"modalForm.attributes.select-components": "Seleziona i componenti", "modalForm.attributes.select-components": "Seleziona i componenti",

View File

@ -125,7 +125,7 @@
"from": "from", "from": "from",
"listView.headerLayout.description": "콘텐츠 구조를 만듭니다.", "listView.headerLayout.description": "콘텐츠 구조를 만듭니다.",
"modalForm.attribute.form.base.name.description": "속성 이름에는 공백을 사용할 수 없습니다.", "modalForm.attribute.form.base.name.description": "속성 이름에는 공백을 사용할 수 없습니다.",
"modalForm.attribute.form.base.name.placeholder": "예: Slug, SEO URL, 표준 URL", "modalForm.attribute.form.base.name.placeholder": "예: slug, seoUrl, 표준Url",
"modalForm.attribute.target-field": "Attached field", "modalForm.attribute.target-field": "Attached field",
"modalForm.attributes.select-component": "컴포넌트 선택", "modalForm.attributes.select-component": "컴포넌트 선택",
"modalForm.attributes.select-components": "컴포넌트 여러개 선택", "modalForm.attributes.select-components": "컴포넌트 여러개 선택",

View File

@ -112,7 +112,7 @@
"form.button.single-type.description": "Sesuai untuk data tunggal seperti mengenai kami, laman utama dan lain-lain", "form.button.single-type.description": "Sesuai untuk data tunggal seperti mengenai kami, laman utama dan lain-lain",
"from": "dari", "from": "dari",
"modalForm.attribute.form.base.name.description": "Tidak boleh ada jarak dalam nama", "modalForm.attribute.form.base.name.description": "Tidak boleh ada jarak dalam nama",
"modalForm.attribute.form.base.name.placeholder": "cth. Slug, URL SEO, URL Canonical", "modalForm.attribute.form.base.name.placeholder": "cth. slug, urlSeo, urlCanonical",
"modalForm.attribute.target-field": "Ruang terpasang", "modalForm.attribute.target-field": "Ruang terpasang",
"modalForm.attributes.select-component": "Pilih komponen", "modalForm.attributes.select-component": "Pilih komponen",
"modalForm.attributes.select-components": "Pilih komponen", "modalForm.attributes.select-components": "Pilih komponen",

View File

@ -105,7 +105,7 @@
"form.button.single-type.description": "Het beste voor een enkele instantie zoals over ons, homepage, enz.", "form.button.single-type.description": "Het beste voor een enkele instantie zoals over ons, homepage, enz.",
"from": "van", "from": "van",
"modalForm.attribute.form.base.name.description": "Er is geen ruimte toegestaan voor de naam van het attribuut", "modalForm.attribute.form.base.name.description": "Er is geen ruimte toegestaan voor de naam van het attribuut",
"modalForm.attribute.form.base.name.placeholder": "Bijv.: Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "Bijv.: slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Gekoppeld veld", "modalForm.attribute.target-field": "Gekoppeld veld",
"modalForm.attributes.select-component": "Selecteer een component", "modalForm.attributes.select-component": "Selecteer een component",
"modalForm.attributes.select-components": "Selecteer de componenten", "modalForm.attributes.select-components": "Selecteer de componenten",

View File

@ -134,7 +134,7 @@
"menu.section.models.name": "Kolekcje", "menu.section.models.name": "Kolekcje",
"menu.section.single-types.name": "Pojedyncze typy", "menu.section.single-types.name": "Pojedyncze typy",
"modalForm.attribute.form.base.name.description": "Spacja nie jest dozwolona dla nazwy", "modalForm.attribute.form.base.name.description": "Spacja nie jest dozwolona dla nazwy",
"modalForm.attribute.form.base.name.placeholder": "np. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "np. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Dołączone pole", "modalForm.attribute.target-field": "Dołączone pole",
"modalForm.attributes.select-component": "Wybierz komponent", "modalForm.attributes.select-component": "Wybierz komponent",
"modalForm.attributes.select-components": "Wybierz komponenty", "modalForm.attributes.select-components": "Wybierz komponenty",

View File

@ -134,7 +134,7 @@
"menu.section.models.name": "Tipos de Coleção", "menu.section.models.name": "Tipos de Coleção",
"menu.section.single-types.name": "Tipos Únicos", "menu.section.single-types.name": "Tipos Únicos",
"modalForm.attribute.form.base.name.description": "Nenhum espaço é permitido para o nome do atributo", "modalForm.attribute.form.base.name.description": "Nenhum espaço é permitido para o nome do atributo",
"modalForm.attribute.form.base.name.placeholder": "por exemplo. Slug, URL de SEO, URL canônica", "modalForm.attribute.form.base.name.placeholder": "por exemplo. slug, urlDeSeo, urlCanônica",
"modalForm.attribute.target-field": "Campo anexado", "modalForm.attribute.target-field": "Campo anexado",
"modalForm.attributes.select-component": "Selecione um componente", "modalForm.attributes.select-component": "Selecione um componente",
"modalForm.attributes.select-components": "Selecione os componentes", "modalForm.attributes.select-components": "Selecione os componentes",

View File

@ -116,7 +116,7 @@
"form.button.single-type.description": "Лучше всего подходит для одного экземпляра, например, о нас, домашняя страница и т.д.", "form.button.single-type.description": "Лучше всего подходит для одного экземпляра, например, о нас, домашняя страница и т.д.",
"from": "из", "from": "из",
"modalForm.attribute.form.base.name.description": "Пробелы в имени атрибута недопустимы", "modalForm.attribute.form.base.name.description": "Пробелы в имени атрибута недопустимы",
"modalForm.attribute.form.base.name.placeholder": "e.g. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "e.g. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Добавленные поля", "modalForm.attribute.target-field": "Добавленные поля",
"modalForm.attributes.select-component": "Выбор компонента", "modalForm.attributes.select-component": "Выбор компонента",
"modalForm.attributes.select-components": "Выбор компонентов", "modalForm.attributes.select-components": "Выбор компонентов",

View File

@ -115,7 +115,7 @@
"form.button.single-type.description": "Ideálne pre jednorazové inštancie ako sú napríklad domovská stránka, atď.", "form.button.single-type.description": "Ideálne pre jednorazové inštancie ako sú napríklad domovská stránka, atď.",
"from": "od", "from": "od",
"modalForm.attribute.form.base.name.description": "Medzery nie sú povolené v názve políčka", "modalForm.attribute.form.base.name.description": "Medzery nie sú povolené v názve políčka",
"modalForm.attribute.form.base.name.placeholder": "napr. slug, SEO URL, kanonická URL", "modalForm.attribute.form.base.name.placeholder": "napr. slug, seoUrl, kanonickáUrl",
"modalForm.attribute.target-field": "Priložené políčko", "modalForm.attribute.target-field": "Priložené políčko",
"modalForm.attributes.select-component": "Vyberte komponent", "modalForm.attributes.select-component": "Vyberte komponent",
"modalForm.attributes.select-components": "Vyberte komponenty", "modalForm.attributes.select-components": "Vyberte komponenty",

View File

@ -135,7 +135,7 @@
"menu.section.models.name": "Samlingstyper", "menu.section.models.name": "Samlingstyper",
"menu.section.single-types.name": "Engångstyper", "menu.section.single-types.name": "Engångstyper",
"modalForm.attribute.form.base.name.description": "Mellanslag tillåts inte i namnet på attributet", "modalForm.attribute.form.base.name.description": "Mellanslag tillåts inte i namnet på attributet",
"modalForm.attribute.form.base.name.placeholder": "t.ex Titel, Slug, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "t.ex titel, slug, canonicalUrl",
"modalForm.attribute.target-field": "Kopplat fält", "modalForm.attribute.target-field": "Kopplat fält",
"modalForm.attributes.select-component": "Välj komponent", "modalForm.attributes.select-component": "Välj komponent",
"modalForm.attributes.select-components": "Välj komponenter", "modalForm.attributes.select-components": "Välj komponenter",

View File

@ -113,7 +113,7 @@
"form.button.single-type.description": "ที่ดีที่สุดสำหรับอินสแตนซ์เดี่ยวเช่น เกี่ยวกับเรา, โฮมเพจ, เป็นต้น", "form.button.single-type.description": "ที่ดีที่สุดสำหรับอินสแตนซ์เดี่ยวเช่น เกี่ยวกับเรา, โฮมเพจ, เป็นต้น",
"from": "จาก", "from": "จาก",
"modalForm.attribute.form.base.name.description": "ไม่มีพื้นที่ว่างที่อนุญาตให้ใช้สำหรับชื่อของแอ็ตทริบิวต์", "modalForm.attribute.form.base.name.description": "ไม่มีพื้นที่ว่างที่อนุญาตให้ใช้สำหรับชื่อของแอ็ตทริบิวต์",
"modalForm.attribute.form.base.name.placeholder": "เช่น Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "เช่น slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "ฟิลด์ที่แนบ", "modalForm.attribute.target-field": "ฟิลด์ที่แนบ",
"modalForm.attributes.select-component": "เลือกคอมโพเนนต์", "modalForm.attributes.select-component": "เลือกคอมโพเนนต์",
"modalForm.attributes.select-components": "เลือกคอมโพเนนต์", "modalForm.attributes.select-components": "เลือกคอมโพเนนต์",

View File

@ -113,7 +113,7 @@
"menu.section.models.name": "Koleksiyon Tipleri", "menu.section.models.name": "Koleksiyon Tipleri",
"menu.section.single-types.name": "Tekil Tipler", "menu.section.single-types.name": "Tekil Tipler",
"modalForm.attribute.form.base.name.description": "Nitelik adında boşluk olamaz", "modalForm.attribute.form.base.name.description": "Nitelik adında boşluk olamaz",
"modalForm.attribute.form.base.name.placeholder": "ör. Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "ör. slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "İliştirilmiş alan", "modalForm.attribute.target-field": "İliştirilmiş alan",
"modalForm.attributes.select-component": "Bir bileşen seç", "modalForm.attributes.select-component": "Bir bileşen seç",
"modalForm.attributes.select-components": "Bileşenleri seç", "modalForm.attributes.select-components": "Bileşenleri seç",

View File

@ -113,7 +113,7 @@
"form.button.single-type.description": "Підходить для поодиноких об'єктів, як-то домашня сторінка, про нас тощо", "form.button.single-type.description": "Підходить для поодиноких об'єктів, як-то домашня сторінка, про нас тощо",
"from": "з", "from": "з",
"modalForm.attribute.form.base.name.description": "Для назви атрибута не допускається пробілів", "modalForm.attribute.form.base.name.description": "Для назви атрибута не допускається пробілів",
"modalForm.attribute.form.base.name.placeholder": "наприклад, Slug, SEO URL, Canonical URL", "modalForm.attribute.form.base.name.placeholder": "наприклад, slug, seoUrl, canonicalUrl",
"modalForm.attribute.target-field": "Пов'язане поле", "modalForm.attribute.target-field": "Пов'язане поле",
"modalForm.attributes.select-component": "Виберіть компонент", "modalForm.attributes.select-component": "Виберіть компонент",
"modalForm.attributes.select-components": "Виберіть компоненти", "modalForm.attributes.select-components": "Виберіть компоненти",

View File

@ -135,7 +135,7 @@
"menu.section.models.name": "集合型別", "menu.section.models.name": "集合型別",
"menu.section.single-types.name": "單一型別", "menu.section.single-types.name": "單一型別",
"modalForm.attribute.form.base.name.description": "欄位名稱不允許空白", "modalForm.attribute.form.base.name.description": "欄位名稱不允許空白",
"modalForm.attribute.form.base.name.placeholder": "例如:Slug、SEO 網址、Canonical 網址", "modalForm.attribute.form.base.name.placeholder": "例如:slug、seo網址、canonical網址",
"modalForm.attribute.target-field": "關聯目標欄位", "modalForm.attribute.target-field": "關聯目標欄位",
"modalForm.attributes.select-component": "選擇一個元件", "modalForm.attributes.select-component": "選擇一個元件",
"modalForm.attributes.select-components": "選擇多個元件", "modalForm.attributes.select-components": "選擇多個元件",

View File

@ -18,7 +18,11 @@ class MysqlDialect extends Dialect {
configure() { configure() {
this.db.config.connection.connection.supportBigNumbers = true; this.db.config.connection.connection.supportBigNumbers = true;
this.db.config.connection.connection.bigNumberStrings = true; // Only allow bigNumberStrings option set to be true if no connection option passed
// Otherwise bigNumberStrings option should be allowed to used from DB config
if (this.db.config.connection.connection.bigNumberStrings === undefined) {
this.db.config.connection.connection.bigNumberStrings = true;
}
this.db.config.connection.connection.typeCast = (field, next) => { this.db.config.connection.connection.typeCast = (field, next) => {
if (field.type === 'DECIMAL' || field.type === 'NEWDECIMAL') { if (field.type === 'DECIMAL' || field.type === 'NEWDECIMAL') {
const value = field.string(); const value = field.string();

View File

@ -1,4 +1,5 @@
node_modules/ node_modules/
.eslintrc.js .eslintrc.js
build/ build/
lib/src/utils/** # TODO: we should fix this.
src/utils/**

View File

@ -1,9 +1,5 @@
module.exports = { module.exports = {
stories: [ stories: ['../*.stories.mdx', '../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
'../*.stories.mdx',
'../lib/src/**/*.stories.mdx',
'../lib/src/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'], addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
core: { core: {
builder: 'webpack5', builder: 'webpack5',

View File

@ -1,4 +1,4 @@
module.exports = { module.exports = {
preset: '../../../jest-preset.front.js', preset: '../../../jest-preset.front.js',
collectCoverageFrom: ['<rootDir>/packages/core/helper-plugin/lib/src/**/*.js'], collectCoverageFrom: ['<rootDir>/packages/core/helper-plugin/src/**/*.js'],
}; };

View File

@ -51,7 +51,8 @@ const Inputs = ({ label, onChange, options, type, value }) => {
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })} clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
ariaLabel={label} ariaLabel={label}
name="datetimepicker" name="datetimepicker"
onChange={(date) => onChange(date.toISOString())} // check if date is not null or undefined
onChange={(date) => onChange(date ? date.toISOString() : null)}
onClear={() => onChange(null)} onClear={() => onChange(null)}
value={value ? new Date(value) : null} value={value ? new Date(value) : null}
selectedDateLabel={(formattedDate) => `Date picker, current is ${formattedDate}`} selectedDateLabel={(formattedDate) => `Date picker, current is ${formattedDate}`}

View File

@ -226,7 +226,8 @@ const GenericInput = ({
hint={hint} hint={hint}
name={name} name={name}
onChange={(date) => { onChange={(date) => {
const formattedDate = date.toISOString(); // check if date is not null or undefined
const formattedDate = date ? date.toISOString() : null;
onChange({ target: { name, value: formattedDate, type } }); onChange({ target: { name, value: formattedDate, type } });
}} }}

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { render, fireEvent } from '@testing-library/react'; import { render, fireEvent, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import GenericInput from '../index'; import GenericInput from '../index';
@ -43,6 +44,33 @@ function setupNumber(props) {
}; };
} }
function setupDatetimePicker(props) {
const DATETIMEPICKER_FIXTURE_PROPS = {
type: 'datetime',
name: 'datetime-picker',
intlLabel: {
id: 'label.test',
defaultMessage: 'datetime picker',
},
value: null,
onChange: jest.fn(),
onClear: jest.fn(),
...props,
};
const rendered = render(<ComponentFixture {...DATETIMEPICKER_FIXTURE_PROPS} />);
return {
...rendered,
};
}
/**
* We extend the timeout of these tests because the DS
* DateTimePicker has a slow rendering issue at the moment.
* It passes locally, but fails in the CI.
*/
jest.setTimeout(50000);
describe('GenericInput', () => { describe('GenericInput', () => {
describe('number', () => { describe('number', () => {
test('renders and matches the snapshot', () => { test('renders and matches the snapshot', () => {
@ -128,4 +156,39 @@ describe('GenericInput', () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
}); });
describe('datetime', () => {
test('renders the datetime picker with the correct value for date and time', async () => {
const user = userEvent.setup();
const { getByRole } = setupDatetimePicker();
const btnDate = getByRole('textbox', { name: 'datetime picker' });
await user.click(btnDate);
const numberDayBtn = await getByRole('button', { name: /15/ });
await act(async () => {
await user.click(numberDayBtn);
});
const today = new Date();
const month = today.getMonth() + 1;
const year = today.getFullYear();
expect(getByRole('textbox', { name: 'datetime picker' })).toHaveValue(`${month}/15/${year}`);
expect(getByRole('combobox', { name: /datetime picker/i })).toHaveValue('00:00');
});
test('simulate clicking on the Clear button in the date and check if the date and time are empty', async () => {
const user = userEvent.setup();
const { getByRole } = setupDatetimePicker();
const btnDate = getByRole('textbox', { name: /datetime picker/i });
await user.click(btnDate);
await act(async () => {
await user.click(getByRole('button', { name: /15/ }));
});
await user.click(getByRole('button', { name: /clear date/i }));
expect(getByRole('textbox', { name: 'datetime picker' })).toHaveValue('');
expect(getByRole('combobox', { name: /datetime picker/i })).toHaveValue('');
});
});
}); });

Some files were not shown because too many files have changed in this diff Show More