mirror of
https://github.com/strapi/strapi.git
synced 2025-11-25 14:41:15 +00:00
Merge branch 'v5/main' into v5/disable-dp
This commit is contained in:
commit
d3ccb6d8c1
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -189,7 +189,7 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: ce-playwright-trace
|
||||
path: test-apps/e2e/**/test-results/**/trace.zip
|
||||
path: test-apps/e2e/test-results/**/trace.zip
|
||||
retention-days: 1
|
||||
|
||||
e2e_ee:
|
||||
@ -231,7 +231,7 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: ee-playwright-trace
|
||||
path: test-apps/e2e/**/test-results/**/trace.zip
|
||||
path: test-apps/e2e/test-results/**/trace.zip
|
||||
retention-days: 1
|
||||
|
||||
cli:
|
||||
|
||||
@ -29,6 +29,46 @@ This will spawn by default a Strapi instance per testing domain (e.g. content-ma
|
||||
|
||||
If you need to clean the test-apps folder because they are not working as expected, you can run `yarn test:e2e clean` which will clean said directory.
|
||||
|
||||
### Running specific tests
|
||||
|
||||
To run only one domain, meaning a top-level directory in e2e/tests such as "admin" or "content-manager", use the `--domains` option.
|
||||
|
||||
```shell
|
||||
yarn test:e2e --domains admin
|
||||
yarn test:e2e --domain admin
|
||||
```
|
||||
|
||||
To run a specific file, you can pass arguments and options to playwright using `--` between the test:e2e options and the playwright options, such as:
|
||||
|
||||
```shell
|
||||
# to run just the login.spec.ts file in the admin domain
|
||||
yarn test:e2e --domains admin -- login.spec.ts
|
||||
```
|
||||
|
||||
### Concurrency / parallellization
|
||||
|
||||
By default, every domain is run with its own test app in parallel with the other domains. The tests within a domain are run in series, one at a time.
|
||||
|
||||
If you need an easier way to view the output, or have problems running multiple apps at once on your system, you can use the `-c` option
|
||||
|
||||
```shell
|
||||
# only run one domain at a time
|
||||
yarn test:e2e -c 1
|
||||
```
|
||||
|
||||
### Env Variables to Control Test Config
|
||||
|
||||
Some helpers have been added to allow you to modify the playwright configuration on your own system without touching the playwright config file used by the test runner.
|
||||
|
||||
| env var | Description | Default |
|
||||
| ---------------------------- | -------------------------------------------- | ------------------ |
|
||||
| PLAYWRIGHT_WEBSERVER_TIMEOUT | timeout for starting the Strapi server | 16000 (160s) |
|
||||
| PLAYWRIGHT_ACTION_TIMEOUT | playwright action timeout (ie, click()) | 15000 (15s) |
|
||||
| PLAYWRIGHT_EXPECT_TIMEOUT | playwright expect waitFor timeout | 10000 (10s) |
|
||||
| PLAYWRIGHT_TIMEOUT | playwright timeout, for each individual test | 30000 (30s) |
|
||||
| PLAYWRIGHT_OUTPUT_DIR | playwright output dir, such as trace files | '../test-results/' |
|
||||
| PLAYWRIGHT_VIDEO | set 'true' to save videos on failed tests | false |
|
||||
|
||||
## Strapi Templates
|
||||
|
||||
The test-app you create uses a [template](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/templates.html) found at `e2e/app-template` in this folder we can store our premade content schemas & any customisations we may need such as other plugins / custom fields / endpoints etc.
|
||||
|
||||
3
e2e/README.md
Normal file
3
e2e/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
## End-to-end Playwright Tests
|
||||
|
||||
See contributor docs in docs/docs/guides/e2e for more info
|
||||
@ -1,3 +1,5 @@
|
||||
const { createTestTransferToken } = require('../../../create-transfer-token');
|
||||
|
||||
module.exports = {
|
||||
rateLimitEnable(ctx) {
|
||||
const { value } = ctx.request.body;
|
||||
@ -13,6 +15,11 @@ module.exports = {
|
||||
|
||||
await permissionService.cleanPermissionsInDatabase();
|
||||
|
||||
ctx.send(200);
|
||||
},
|
||||
async resetTransferToken(ctx) {
|
||||
await createTestTransferToken(strapi);
|
||||
|
||||
ctx.send(200);
|
||||
},
|
||||
};
|
||||
|
||||
@ -16,5 +16,13 @@ module.exports = {
|
||||
auth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/config/resettransfertoken',
|
||||
handler: 'config.resetTransferToken',
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
27
e2e/app-template/template/src/create-transfer-token.js
Normal file
27
e2e/app-template/template/src/create-transfer-token.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants');
|
||||
|
||||
/**
|
||||
* Make sure the test transfer token exists in the database
|
||||
* @param {Strapi.Strapi} strapi
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const createTestTransferToken = async (strapi) => {
|
||||
const { token: transferTokenService } = strapi.admin.services.transfer;
|
||||
|
||||
const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY);
|
||||
const exists = await transferTokenService.exists({ accessKey: accessKeyHash });
|
||||
|
||||
if (!exists) {
|
||||
await transferTokenService.create({
|
||||
name: 'TestToken',
|
||||
description: 'Transfer token used to seed the e2e database',
|
||||
lifespan: null,
|
||||
permissions: ['push'],
|
||||
accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createTestTransferToken,
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants');
|
||||
const { createTestTransferToken } = require('./create-transfer-token');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@ -23,25 +23,3 @@ module.exports = {
|
||||
await createTestTransferToken(strapi);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Make sure the test transfer token exists in the database
|
||||
* @param {Strapi.Strapi} strapi
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const createTestTransferToken = async (strapi) => {
|
||||
const { token: transferTokenService } = strapi.admin.services.transfer;
|
||||
|
||||
const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY);
|
||||
const exists = await transferTokenService.exists({ accessKey: accessKeyHash });
|
||||
|
||||
if (!exists) {
|
||||
await transferTokenService.create({
|
||||
name: 'TestToken',
|
||||
description: 'Transfer token used to seed the e2e database',
|
||||
lifespan: null,
|
||||
permissions: ['push'],
|
||||
accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -5,6 +5,8 @@ const ALLOWED_CONTENT_TYPES = [
|
||||
'admin::user',
|
||||
'admin::role',
|
||||
'admin::permission',
|
||||
'admin::api-token',
|
||||
'admin::transfer-token',
|
||||
'api::article.article',
|
||||
'api::author.author',
|
||||
'api::homepage.homepage',
|
||||
|
||||
@ -53,6 +53,19 @@ export const resetDatabaseAndImportDataFromPath = async (
|
||||
|
||||
engine.diagnostics.onDiagnostic(console.log);
|
||||
|
||||
try {
|
||||
// reset the transfer token to allow the transfer if it's been wiped (that is, not included in previous import data)
|
||||
const res = await fetch(
|
||||
`http://127.0.0.1:${process.env.PORT ?? 1337}/api/config/resettransfertoken`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Token reset failed.' + JSON.stringify(err, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await engine.transfer();
|
||||
} catch {
|
||||
|
||||
@ -60,7 +60,7 @@ test.describe('Sign Up', () => {
|
||||
await expect(page.getByText('The value must be a lowercase string')).toBeVisible();
|
||||
|
||||
await fillEmailAndSubmit('notanemail');
|
||||
await expect(page.getByText('This is an invalid email')).toBeVisible();
|
||||
await expect(page.getByText('This is not a valid email')).toBeVisible();
|
||||
});
|
||||
|
||||
test("a user cannot submit the form if a password isn't provided or doesn't meet the password validation requirements", async ({
|
||||
|
||||
65
e2e/tests/admin/transfer/tokens.spec.ts
Normal file
65
e2e/tests/admin/transfer/tokens.spec.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../../utils/login';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../../scripts/dts-import';
|
||||
import { navToHeader, delay } from '../../../utils/shared';
|
||||
|
||||
const createTransferToken = async (page, tokenName, duration, type) => {
|
||||
await navToHeader(
|
||||
page,
|
||||
['Settings', 'Transfer Tokens', 'Create new Transfer Token'],
|
||||
'Create Transfer Token'
|
||||
);
|
||||
|
||||
await page.getByLabel('Name*').click();
|
||||
await page.getByLabel('Name*').fill(tokenName);
|
||||
|
||||
await page.getByLabel('Token duration').click();
|
||||
await page.getByRole('option', { name: duration }).click();
|
||||
|
||||
await page.getByLabel('Token type').click();
|
||||
await page.getByRole('option', { name: type }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.getByText(/copy this token/)).toBeVisible();
|
||||
await expect(page.getByText('Expiration date:')).toBeVisible();
|
||||
};
|
||||
|
||||
test.describe('Transfer Tokens', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar');
|
||||
await page.goto('/admin');
|
||||
await login({ page });
|
||||
});
|
||||
|
||||
// Test token creation
|
||||
const testCases = [
|
||||
['30-day push token', '30 days', 'Push'],
|
||||
['30-day pull token', '30 days', 'Pull'],
|
||||
['30-day full-access token', '30 days', 'Full access'],
|
||||
// if push+pull work generally that's good enough for e2e
|
||||
['7-day token', '7 days', 'Full access'],
|
||||
['90-day token', '90 days', 'Full access'],
|
||||
['unlimited token', 'Unlimited', 'Full access'],
|
||||
];
|
||||
for (const [name, duration, type] of testCases) {
|
||||
test(`A user should be able to create a ${name}`, async ({ page }) => {
|
||||
await createTransferToken(page, name, duration, type);
|
||||
});
|
||||
}
|
||||
|
||||
test('Created tokens list page should be correct', async ({ page }) => {
|
||||
await createTransferToken(page, 'my test token', 'unlimited', 'Full access');
|
||||
|
||||
// if we don't wait until createdAt is at least 1s, we see "NaN" for the timestamp
|
||||
// TODO: fix the bug and remove this
|
||||
await page.waitForTimeout(1100);
|
||||
|
||||
await navToHeader(page, ['Settings', 'Transfer Tokens'], 'Transfer Tokens');
|
||||
|
||||
const row = page.getByRole('gridcell', { name: 'my test token', exact: true });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(page.getByText(/\d+ (second|minute)s? ago/)).toBeVisible();
|
||||
// TODO: expand on this test, it could check edit and delete icons
|
||||
});
|
||||
});
|
||||
@ -70,7 +70,17 @@ describeOnCondition(edition === 'EE')('Releases page', () => {
|
||||
name: 'Date',
|
||||
})
|
||||
.click();
|
||||
await page.getByRole('gridcell', { name: 'Sunday, March 3, 2024' }).click();
|
||||
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 1);
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
await page.getByRole('gridcell', { name: formattedDate }).click();
|
||||
|
||||
await page
|
||||
.getByRole('combobox', {
|
||||
|
||||
@ -13,7 +13,7 @@ export const login = async ({ page, rememberMe = false }: { page: Page; remember
|
||||
.fill(ADMIN_PASSWORD);
|
||||
|
||||
if (rememberMe) {
|
||||
await page.getByLabel('rememberMe').click();
|
||||
await page.getByLabel('Remember me').click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
@ -1,7 +1,25 @@
|
||||
import { test } from '@playwright/test';
|
||||
import { test, Page, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Execute a test suite only if the condition is true
|
||||
*/
|
||||
export const describeOnCondition = (shouldDescribe: boolean) =>
|
||||
shouldDescribe ? test.describe : test.describe.skip;
|
||||
|
||||
/**
|
||||
* Navigate to a page and confirm the header, awaiting each step
|
||||
*/
|
||||
export const navToHeader = async (page: Page, navItems: string[], headerText: string) => {
|
||||
for (const navItem of navItems) {
|
||||
// This does not use getByRole because sometimes "Settings" is "Settings 1" if there's a badge notification
|
||||
// BUT if we don't match exact it conflicts with "Advanceed Settings"
|
||||
// As a workaround, we implement our own startsWith with page.locator
|
||||
const item = page.locator(`role=link[name^="${navItem}"]`);
|
||||
await expect(item).toBeVisible();
|
||||
await item.click();
|
||||
}
|
||||
|
||||
const header = page.getByRole('heading', { name: headerText, exact: true });
|
||||
await expect(header).toBeVisible();
|
||||
return header;
|
||||
};
|
||||
|
||||
@ -17,8 +17,10 @@ import isEqual from 'lodash/isEqual';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
|
||||
import { createContext } from '../../components/Context';
|
||||
import { getIn, setIn } from '../utils/object';
|
||||
import { useComposedRefs } from '../utils/refs';
|
||||
|
||||
import { createContext } from './Context';
|
||||
|
||||
import type { InputProps as InputPropsImpl, EnumerationProps } from './FormInputs/types';
|
||||
import type * as Yup from 'yup';
|
||||
@ -118,6 +120,7 @@ interface FormProps<TFormValues extends FormValues = FormValues>
|
||||
*/
|
||||
const Form = React.forwardRef<HTMLFormElement, FormProps>(
|
||||
({ disabled = false, method, onSubmit, ...props }, ref) => {
|
||||
const formRef = React.useRef<HTMLFormElement>(null!);
|
||||
const initialValues = React.useRef(props.initialValues ?? {});
|
||||
const [state, dispatch] = React.useReducer(reducer, {
|
||||
errors: {},
|
||||
@ -146,6 +149,31 @@ const Form = React.forwardRef<HTMLFormElement, FormProps>(
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (Object.keys(state.errors).length === 0) return;
|
||||
|
||||
/**
|
||||
* Small timeout to ensure the form has been
|
||||
* rendered before we try to focus on the first
|
||||
*/
|
||||
const ref = setTimeout(() => {
|
||||
const [firstError] = formRef.current.querySelectorAll('[data-strapi-field-error]');
|
||||
|
||||
if (firstError) {
|
||||
const errorId = firstError.getAttribute('id');
|
||||
const formElementInError = formRef.current.querySelector(
|
||||
`[aria-describedby="${errorId}"]`
|
||||
);
|
||||
|
||||
if (formElementInError && formElementInError instanceof HTMLElement) {
|
||||
formElementInError.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => clearTimeout(ref);
|
||||
}, [state.errors]);
|
||||
|
||||
/**
|
||||
* Uses the provided validation schema
|
||||
*/
|
||||
@ -344,8 +372,10 @@ const Form = React.forwardRef<HTMLFormElement, FormProps>(
|
||||
dispatch({ type: 'SET_ISSUBMITTING', payload: isSubmitting });
|
||||
}, []);
|
||||
|
||||
const composedRefs = useComposedRefs(formRef, ref);
|
||||
|
||||
return (
|
||||
<form ref={ref} method={method} noValidate onSubmit={handleSubmit}>
|
||||
<form ref={composedRefs} method={method} noValidate onSubmit={handleSubmit}>
|
||||
<FormProvider
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
@ -23,6 +23,7 @@ const BooleanInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
checked={field.value === null ? null : field.value || false}
|
||||
disabled={disabled}
|
||||
hint={hint}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
error={field.error}
|
||||
/**
|
||||
@ -0,0 +1,34 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Checkbox } from '@strapi/design-system';
|
||||
import { useFocusInputField } from '@strapi/helper-plugin';
|
||||
|
||||
import { useComposedRefs } from '../../utils/refs';
|
||||
import { useField } from '../Form';
|
||||
|
||||
import { InputProps } from './types';
|
||||
|
||||
const CheckboxInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ disabled, label, hint, name, required }, ref) => {
|
||||
const field = useField<boolean>(name);
|
||||
const fieldRef = useFocusInputField(name);
|
||||
|
||||
const composedRefs = useComposedRefs<HTMLInputElement | null>(ref, fieldRef);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
hint={hint}
|
||||
name={name}
|
||||
onValueChange={(checked) => field.onChange(name, checked)}
|
||||
ref={composedRefs}
|
||||
required={required}
|
||||
value={field.value}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { CheckboxInput };
|
||||
@ -23,6 +23,7 @@ const DateInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -23,6 +23,7 @@ const DateTimeInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -21,6 +21,7 @@ export const EmailInput = forwardRef<any, InputProps>(
|
||||
autoComplete="email"
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -20,6 +20,7 @@ export const EnumerationInput = forwardRef<any, EnumerationProps>(
|
||||
ref={composedRefs}
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
hint={hint}
|
||||
name={name}
|
||||
@ -21,6 +21,7 @@ export const JsonInput = forwardRef<any, InputProps>(
|
||||
return (
|
||||
<JSONInputImpl
|
||||
ref={composedRefs}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
@ -21,6 +21,7 @@ const NumberInputImpl = forwardRef<HTMLInputElement, InputProps>(
|
||||
defaultValue={field.initialValue}
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -48,6 +48,7 @@ export const PasswordInput = forwardRef<any, InputProps>(
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -7,6 +7,7 @@ import { useComposedRefs } from '../../utils/refs';
|
||||
import { useField } from '../Form';
|
||||
|
||||
import { BooleanInput } from './Boolean';
|
||||
import { CheckboxInput } from './Checkbox';
|
||||
import { DateInput } from './Date';
|
||||
import { DateTimeInput } from './DateTime';
|
||||
import { EmailInput } from './Email';
|
||||
@ -38,6 +39,8 @@ const InputRenderer = memo(
|
||||
return <StringInput ref={forwardRef} {...props} />;
|
||||
case 'boolean':
|
||||
return <BooleanInput ref={forwardRef} {...props} />;
|
||||
case 'checkbox':
|
||||
return <CheckboxInput ref={forwardRef} {...props} />;
|
||||
case 'datetime':
|
||||
return <DateTimeInput ref={forwardRef} {...props} />;
|
||||
case 'date':
|
||||
@ -76,6 +79,7 @@ const NotSupportedField = forwardRef<any, InputProps>((props, ref) => {
|
||||
ref={composedRefs}
|
||||
disabled
|
||||
error={error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={props.label}
|
||||
id={props.name}
|
||||
hint={props.hint}
|
||||
@ -23,6 +23,7 @@ export const StringInput = forwardRef<any, InputProps>(
|
||||
ref={composedRefs}
|
||||
disabled={disabled}
|
||||
hint={hint}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
name={name}
|
||||
error={field.error}
|
||||
@ -21,6 +21,7 @@ export const TextareaInput = forwardRef<any, InputProps>(
|
||||
disabled={disabled}
|
||||
defaultValue={field.initialValue}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -23,6 +23,7 @@ const TimeInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
|
||||
disabled={disabled}
|
||||
error={field.error}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
id={name}
|
||||
hint={hint}
|
||||
@ -15,22 +15,24 @@ interface EnumerationProps extends Omit<InputProps, 'options' | 'type'> {
|
||||
interface InputProps {
|
||||
disabled?: boolean;
|
||||
hint?: ReactNode;
|
||||
label: string;
|
||||
label: ReactNode;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
options?: never;
|
||||
type: Exclude<
|
||||
Attribute.Kind,
|
||||
| 'enumeration'
|
||||
| 'media'
|
||||
| 'blocks'
|
||||
| 'richtext'
|
||||
| 'uid'
|
||||
| 'dynamiczone'
|
||||
| 'component'
|
||||
| 'relation'
|
||||
>;
|
||||
type:
|
||||
| Exclude<
|
||||
Attribute.Kind,
|
||||
| 'enumeration'
|
||||
| 'media'
|
||||
| 'blocks'
|
||||
| 'richtext'
|
||||
| 'uid'
|
||||
| 'dynamiczone'
|
||||
| 'component'
|
||||
| 'relation'
|
||||
>
|
||||
| 'checkbox';
|
||||
}
|
||||
|
||||
export { EnumerationProps, InputProps };
|
||||
@ -15,13 +15,13 @@ import { useNotification } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Form, InputProps, useField } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { capitalise } from '../../../utils/strings';
|
||||
import { ATTRIBUTE_TYPES_THAT_CANNOT_BE_MAIN_FIELD } from '../../constants/attributes';
|
||||
import { useGetInitialDataQuery } from '../../services/init';
|
||||
import { getTranslation } from '../../utils/translations';
|
||||
import { FieldTypeIcon } from '../FieldTypeIcon';
|
||||
import { Form, InputProps, useField } from '../Form';
|
||||
import { InputRenderer } from '../FormInputs/Renderer';
|
||||
|
||||
import { TEMP_FIELD_NAME } from './Fields';
|
||||
|
||||
|
||||
@ -9,12 +9,12 @@ import { useIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useField, useForm } from '../../../components/Form';
|
||||
import { useComposedRefs } from '../../../utils/refs';
|
||||
import { ItemTypes } from '../../constants/dragAndDrop';
|
||||
import { type UseDragAndDropOptions, useDragAndDrop } from '../../hooks/useDragAndDrop';
|
||||
import { useComposedRefs } from '../../utils/refs';
|
||||
import { getTranslation } from '../../utils/translations';
|
||||
import { ComponentIcon } from '../ComponentIcon';
|
||||
import { useField, useForm } from '../Form';
|
||||
|
||||
import { EditFieldForm, EditFieldFormProps } from './EditFieldForm';
|
||||
|
||||
@ -226,7 +226,7 @@ const Fields = ({ attributes, fieldSizes, components, metadatas = {} }: FieldsPr
|
||||
</Menu.Trigger>
|
||||
<Menu.Content>
|
||||
{remainingFields.map((field) => (
|
||||
<Menu.Item key={field.label} onSelect={handleAddField(field)}>
|
||||
<Menu.Item key={field.name} onSelect={handleAddField(field)}>
|
||||
{field.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
|
||||
@ -19,11 +19,11 @@ import pipe from 'lodash/fp/pipe';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Form, FormProps, useForm } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { capitalise } from '../../../utils/strings';
|
||||
import { ATTRIBUTE_TYPES_THAT_CANNOT_BE_MAIN_FIELD } from '../../constants/attributes';
|
||||
import { getTranslation } from '../../utils/translations';
|
||||
import { Form, FormProps, useForm } from '../Form';
|
||||
import { InputRenderer } from '../FormInputs/Renderer';
|
||||
|
||||
import { Fields, FieldsProps, TEMP_FIELD_NAME } from './Fields';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { fireEvent, render as renderRTL, screen } from '@tests/utils';
|
||||
|
||||
import { Form } from '../../Form';
|
||||
import { Form } from '../../../../components/Form';
|
||||
import { EditFieldForm, EditFieldFormProps } from '../EditFieldForm';
|
||||
import { ConfigurationFormData } from '../Form';
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import { useIntl } from 'react-intl';
|
||||
import { FixedSizeList, FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { composeRefs } from '../../../utils/refs';
|
||||
import { ItemTypes } from '../../constants/dragAndDrop';
|
||||
import {
|
||||
UseDragAndDropOptions,
|
||||
@ -30,7 +31,6 @@ import {
|
||||
DROP_SENSITIVITY,
|
||||
} from '../../hooks/useDragAndDrop';
|
||||
import { usePrev } from '../../hooks/usePrev';
|
||||
import { composeRefs } from '../../utils/refs';
|
||||
import { getTranslation } from '../../utils/translations';
|
||||
|
||||
import type { NormalizedRelation } from './utils/normalizeRelations';
|
||||
|
||||
@ -14,5 +14,3 @@ export type {
|
||||
ListLayout,
|
||||
} from './hooks/useDocumentLayout';
|
||||
export * from './features/DocumentRBAC';
|
||||
export * from './components/Form';
|
||||
export * from './components/FormInputs/Renderer';
|
||||
|
||||
@ -45,7 +45,13 @@ const useContentManagerInitData = (): ContentManagerAppState => {
|
||||
|
||||
const state = useTypedSelector((state) => state['content-manager_app']);
|
||||
|
||||
const initialDataQuery = useGetInitialDataQuery();
|
||||
const initialDataQuery = useGetInitialDataQuery(undefined, {
|
||||
/**
|
||||
* TODO: remove this when the CTB has been refactored to use redux-toolkit-query
|
||||
* and it can invalidate the cache on mutation
|
||||
*/
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialDataQuery.data) {
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
useDocument,
|
||||
} from './useDocument';
|
||||
|
||||
import type { InputProps } from '../components/FormInputs/types';
|
||||
import type { InputProps } from '../../components/FormInputs/types';
|
||||
import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared';
|
||||
import type { Attribute } from '@strapi/types';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
@ -66,8 +66,9 @@ interface ListLayout {
|
||||
options: LayoutOptions;
|
||||
settings: LayoutSettings;
|
||||
}
|
||||
interface EditFieldSharedProps extends Omit<InputProps, 'hint' | 'type'> {
|
||||
interface EditFieldSharedProps extends Omit<InputProps, 'hint' | 'label' | 'type'> {
|
||||
hint?: string;
|
||||
label: string;
|
||||
mainField?: string;
|
||||
size: number;
|
||||
unique?: boolean;
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useTypedSelector } from '../../core/store/hooks';
|
||||
import { setIn } from '../../utils/object';
|
||||
import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';
|
||||
import { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';
|
||||
import { ComponentsDictionary, extractContentTypeComponents } from '../hooks/useDocument';
|
||||
@ -23,7 +24,6 @@ import {
|
||||
useUpdateComponentConfigurationMutation,
|
||||
} from '../services/components';
|
||||
import { useGetInitialDataQuery } from '../services/init';
|
||||
import { setIn } from '../utils/object';
|
||||
|
||||
import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared';
|
||||
|
||||
|
||||
@ -10,13 +10,13 @@ import {
|
||||
} from '@strapi/helper-plugin';
|
||||
|
||||
import { useTypedSelector } from '../../core/store/hooks';
|
||||
import { setIn } from '../../utils/object';
|
||||
import { TEMP_FIELD_NAME } from '../components/ConfigurationForm/Fields';
|
||||
import { ConfigurationForm, ConfigurationFormProps } from '../components/ConfigurationForm/Form';
|
||||
import { useDoc } from '../hooks/useDocument';
|
||||
import { useDocLayout } from '../hooks/useDocumentLayout';
|
||||
import { useUpdateContentTypeConfigurationMutation } from '../services/contentTypes';
|
||||
import { useGetInitialDataQuery } from '../services/init';
|
||||
import { setIn } from '../utils/object';
|
||||
|
||||
import type { Contracts } from '@strapi/plugin-content-manager/_internal/shared';
|
||||
|
||||
|
||||
@ -23,8 +23,8 @@ import { useIntl } from 'react-intl';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Blocker, Form } from '../../../components/Form';
|
||||
import { useOnce } from '../../../hooks/useOnce';
|
||||
import { Blocker, Form } from '../../components/Form';
|
||||
import { SINGLE_TYPES } from '../../constants/collections';
|
||||
import { DocumentRBAC, useDocumentRBAC } from '../../features/DocumentRBAC';
|
||||
import { type UseDocument, useDoc } from '../../hooks/useDocument';
|
||||
|
||||
@ -23,9 +23,9 @@ import { useIntl } from 'react-intl';
|
||||
import { useMatch, useNavigate } from 'react-router-dom';
|
||||
import styled, { DefaultTheme } from 'styled-components';
|
||||
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { DocumentActionComponent } from '../../../../core/apis/content-manager';
|
||||
import { isBaseQueryError } from '../../../../utils/baseQuery';
|
||||
import { useForm } from '../../../components/Form';
|
||||
import { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes';
|
||||
import { SINGLE_TYPES } from '../../../constants/collections';
|
||||
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
|
||||
|
||||
@ -14,7 +14,7 @@ import { Editor, Path, Range, Transforms } from 'slate';
|
||||
import { type RenderElementProps, ReactEditor } from 'slate-react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { composeRefs } from '../../../../../../utils/refs';
|
||||
import { composeRefs } from '../../../../../../../utils/refs';
|
||||
import { type BlocksStore, useBlocksEditorContext } from '../BlocksEditor';
|
||||
import { editLink, removeLink } from '../utils/links';
|
||||
import { isLinkNode, type Block } from '../utils/types';
|
||||
|
||||
@ -7,9 +7,9 @@ import { Editor, Range, Transforms } from 'slate';
|
||||
import { ReactEditor, type RenderElementProps, type RenderLeafProps, Editable } from 'slate-react';
|
||||
import styled, { CSSProperties, css } from 'styled-components';
|
||||
|
||||
import { composeRefs } from '../../../../../../utils/refs';
|
||||
import { ItemTypes } from '../../../../../constants/dragAndDrop';
|
||||
import { useDragAndDrop, DIRECTIONS } from '../../../../../hooks/useDragAndDrop';
|
||||
import { composeRefs } from '../../../../../utils/refs';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
|
||||
import { type BlocksStore, useBlocksEditorContext } from './BlocksEditor';
|
||||
|
||||
@ -11,7 +11,7 @@ import { withHistory } from 'slate-history';
|
||||
import { type RenderElementProps, Slate, withReact, ReactEditor, useSlate } from 'slate-react';
|
||||
import styled, { type CSSProperties } from 'styled-components';
|
||||
|
||||
import { FieldValue } from '../../../../../components/Form';
|
||||
import { FieldValue } from '../../../../../../components/Form';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
|
||||
import { codeBlocks } from './Blocks/Code';
|
||||
|
||||
@ -2,8 +2,8 @@ import * as React from 'react';
|
||||
|
||||
import { Field, FieldError, FieldHint, FieldLabel, Flex } from '@strapi/design-system';
|
||||
|
||||
import { useField } from '../../../../../components/Form';
|
||||
import { InputProps } from '../../../../../components/FormInputs/types';
|
||||
import { useField } from '../../../../../../components/Form';
|
||||
import { InputProps } from '../../../../../../components/FormInputs/types';
|
||||
|
||||
import { BlocksEditor } from './BlocksEditor';
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
|
||||
import { render, screen, waitFor } from '@tests/utils';
|
||||
|
||||
import { Form } from '../../../../../../components/Form';
|
||||
import { Form } from '../../../../../../../components/Form';
|
||||
import { BlocksInput } from '../BlocksInput';
|
||||
|
||||
import { blocksData } from './mock-schema';
|
||||
|
||||
@ -6,7 +6,7 @@ import { PlusCircle } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useField } from '../../../../../components/Form';
|
||||
import { useField } from '../../../../../../components/Form';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
|
||||
interface InitializerProps {
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box, Flex, IconButton, Typography } from '@strapi/design-system';
|
||||
import { Trash } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { InputProps, useField } from '../../../../../components/Form';
|
||||
import { InputProps, useField } from '../../../../../../components/Form';
|
||||
import { useDoc } from '../../../../../hooks/useDocument';
|
||||
import { EditFieldLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
|
||||
@ -20,13 +20,13 @@ import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useField, useForm } from '../../../../../components/Form';
|
||||
import { useField, useForm } from '../../../../../../components/Form';
|
||||
import { getIn } from '../../../../../../utils/object';
|
||||
import { useComposedRefs } from '../../../../../../utils/refs';
|
||||
import { ItemTypes } from '../../../../../constants/dragAndDrop';
|
||||
import { useDoc } from '../../../../../hooks/useDocument';
|
||||
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { useDragAndDrop, type UseDragAndDropOptions } from '../../../../../hooks/useDragAndDrop';
|
||||
import { getIn } from '../../../../../utils/object';
|
||||
import { useComposedRefs } from '../../../../../utils/refs';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
import { transformDocument } from '../../../utils/data';
|
||||
import { createDefaultForm } from '../../../utils/forms';
|
||||
|
||||
@ -17,13 +17,13 @@ import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useForm } from '../../../../../../components/Form';
|
||||
import { getIn } from '../../../../../../utils/object';
|
||||
import { useComposedRefs } from '../../../../../../utils/refs';
|
||||
import { ComponentIcon } from '../../../../../components/ComponentIcon';
|
||||
import { useForm } from '../../../../../components/Form';
|
||||
import { ItemTypes } from '../../../../../constants/dragAndDrop';
|
||||
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { type UseDragAndDropOptions, useDragAndDrop } from '../../../../../hooks/useDragAndDrop';
|
||||
import { getIn } from '../../../../../utils/object';
|
||||
import { useComposedRefs } from '../../../../../utils/refs';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
import { InputRenderer } from '../../InputRenderer';
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { Box, Flex, Typography } from '@strapi/design-system';
|
||||
import { pxToRem } from '@strapi/helper-plugin';
|
||||
|
||||
interface DynamicZoneLabelProps {
|
||||
label?: string;
|
||||
label?: React.ReactNode;
|
||||
labelAction?: React.ReactNode;
|
||||
name: string;
|
||||
numberOfComponents?: number;
|
||||
|
||||
@ -6,7 +6,7 @@ import pipe from 'lodash/fp/pipe';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { createContext } from '../../../../../../components/Context';
|
||||
import { InputProps, useField, useForm } from '../../../../../components/Form';
|
||||
import { InputProps, useField, useForm } from '../../../../../../components/Form';
|
||||
import { useDoc } from '../../../../../hooks/useDocument';
|
||||
import { type EditFieldLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { screen, fireEvent, render as renderRTL } from '@tests/utils';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Form } from '../../../../../../components/Form';
|
||||
import { Form } from '../../../../../../../components/Form';
|
||||
import { DynamicComponent, DynamicComponentProps } from '../DynamicComponent';
|
||||
|
||||
import { dynamicComponentsByCategory } from './fixtures';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { act, render as renderRTL, screen } from '@tests/utils';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Form } from '../../../../../../components/Form';
|
||||
import { Form } from '../../../../../../../components/Form';
|
||||
import { DynamicZone, DynamicZoneProps } from '../Field';
|
||||
|
||||
const TEST_NAME = 'DynamicZoneComponent';
|
||||
|
||||
@ -12,8 +12,9 @@ import { Contracts } from '@strapi/plugin-content-manager/_internal/shared';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
import { type InputProps, useField, useForm } from '../../../../../components/Form';
|
||||
import { useDebounce } from '../../../../../hooks/useDebounce';
|
||||
import { type InputProps, useField, useForm } from '../../../../components/Form';
|
||||
import { useComposedRefs } from '../../../../../utils/refs';
|
||||
import { useDoc } from '../../../../hooks/useDocument';
|
||||
import {
|
||||
useGenerateUIDMutation,
|
||||
@ -21,7 +22,6 @@ import {
|
||||
useGetDefaultUIDQuery,
|
||||
} from '../../../../services/uid';
|
||||
import { buildValidParams } from '../../../../utils/api';
|
||||
import { useComposedRefs } from '../../../../utils/refs';
|
||||
|
||||
import type { Attribute } from '@strapi/types';
|
||||
|
||||
@ -246,6 +246,7 @@ const UIDInput = React.forwardRef<any, UIDInputProps>(
|
||||
</Flex>
|
||||
}
|
||||
hint={hint}
|
||||
// @ts-expect-error – label _could_ be a ReactNode since it's a child, this should be fixed in the DS.
|
||||
label={label}
|
||||
name={name}
|
||||
onChange={field.onChange}
|
||||
|
||||
@ -6,8 +6,8 @@ import styled from 'styled-components';
|
||||
import { PreviewWysiwyg } from './PreviewWysiwyg';
|
||||
import { newlineAndIndentContinueMarkdownList } from './utils/continueList';
|
||||
|
||||
import type { FieldValue } from '../../../../../components/Form';
|
||||
import type { InputProps } from '../../../../../components/FormInputs/types';
|
||||
import type { FieldValue } from '../../../../../../components/Form';
|
||||
import type { InputProps } from '../../../../../../components/FormInputs/types';
|
||||
|
||||
import 'codemirror5/addon/display/placeholder';
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { Field, FieldError, FieldHint, FieldLabel, Flex } from '@strapi/design-s
|
||||
import { prefixFileUrlWithBackendUrl, useLibrary } from '@strapi/helper-plugin';
|
||||
import { EditorFromTextArea } from 'codemirror5';
|
||||
|
||||
import { useField } from '../../../../../components/Form';
|
||||
import { useField } from '../../../../../../components/Form';
|
||||
|
||||
import { Editor, EditorApi } from './Editor';
|
||||
import { EditorLayout } from './EditorLayout';
|
||||
@ -18,7 +18,7 @@ import {
|
||||
import { WysiwygFooter } from './WysiwygFooter';
|
||||
import { WysiwygNav } from './WysiwygNav';
|
||||
|
||||
import type { InputProps } from '../../../../../components/FormInputs/types';
|
||||
import type { InputProps } from '../../../../../../components/FormInputs/types';
|
||||
import type { Attribute } from '@strapi/types';
|
||||
|
||||
interface WysiwygProps extends Omit<InputProps, 'type'> {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { render as renderRTL } from '@tests/utils';
|
||||
|
||||
import { Form } from '../../../../../../components/Form';
|
||||
import { Form } from '../../../../../../../components/Form';
|
||||
import { Wysiwyg, WysiwygProps } from '../Field';
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { render as renderRTL, waitFor, act, screen } from '@tests/utils';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Form } from '../../../../../components/Form';
|
||||
import { Form } from '../../../../../../components/Form';
|
||||
import { UIDInput, UIDInputProps } from '../UID';
|
||||
|
||||
const render = ({
|
||||
|
||||
@ -16,8 +16,8 @@ import { useMatch, useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DescriptionComponentRenderer } from '../../../../components/DescriptionComponentRenderer';
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { capitalise } from '../../../../utils/strings';
|
||||
import { useForm } from '../../../components/Form';
|
||||
import {
|
||||
CREATED_AT_ATTRIBUTE_NAME,
|
||||
CREATED_BY_ATTRIBUTE_NAME,
|
||||
|
||||
@ -3,8 +3,8 @@ import { ReactNode } from 'react';
|
||||
import { NotAllowedInput, useLibrary } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useForm } from '../../../components/Form';
|
||||
import { InputRenderer as FormInputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { InputRenderer as FormInputRenderer } from '../../../../components/FormInputs/Renderer';
|
||||
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
|
||||
import { useDoc } from '../../../hooks/useDocument';
|
||||
import { useLazyComponents } from '../../../hooks/useLazyComponents';
|
||||
|
||||
@ -11,13 +11,13 @@ import {
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { Form, FormProps } from '../../../components/Form';
|
||||
import { useTypedSelector } from '../../../core/store/hooks';
|
||||
import { Form, FormProps } from '../../components/Form';
|
||||
import { setIn } from '../../../utils/object';
|
||||
import { SINGLE_TYPES } from '../../constants/collections';
|
||||
import { useDoc } from '../../hooks/useDocument';
|
||||
import { ListFieldLayout, ListLayout, useDocLayout } from '../../hooks/useDocumentLayout';
|
||||
import { useUpdateContentTypeConfigurationMutation } from '../../services/contentTypes';
|
||||
import { setIn } from '../../utils/object';
|
||||
|
||||
import { Header } from './components/Header';
|
||||
import { Settings } from './components/Settings';
|
||||
|
||||
@ -6,10 +6,10 @@ import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useComposedRefs } from '../../../../utils/refs';
|
||||
import { CardDragPreview } from '../../../components/DragPreviews/CardDragPreview';
|
||||
import { ItemTypes } from '../../../constants/dragAndDrop';
|
||||
import { useDragAndDrop } from '../../../hooks/useDragAndDrop';
|
||||
import { useComposedRefs } from '../../../utils/refs';
|
||||
import { getTranslation } from '../../../utils/translations';
|
||||
|
||||
import { EditFieldForm } from './EditFieldForm';
|
||||
|
||||
@ -16,10 +16,10 @@ import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Form, useField } from '../../../../components/Form';
|
||||
import { InputRenderer } from '../../../../components/FormInputs/Renderer';
|
||||
import { capitalise } from '../../../../utils/strings';
|
||||
import { FieldTypeIcon } from '../../../components/FieldTypeIcon';
|
||||
import { Form, useField } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { getTranslation } from '../../../utils/translations';
|
||||
|
||||
import type { ListFieldLayout } from '../../../hooks/useDocumentLayout';
|
||||
|
||||
@ -5,8 +5,8 @@ import { ArrowLeft } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { capitalise } from '../../../../utils/strings';
|
||||
import { useForm } from '../../../components/Form';
|
||||
import { getTranslation } from '../../../utils/translations';
|
||||
|
||||
interface HeaderProps {
|
||||
|
||||
@ -4,9 +4,9 @@ import { Flex, Grid, GridItem, Typography } from '@strapi/design-system';
|
||||
import { useCollator } from '@strapi/helper-plugin';
|
||||
import { MessageDescriptor, useIntl } from 'react-intl';
|
||||
|
||||
import { useForm, type InputProps } from '../../../../components/Form';
|
||||
import { InputRenderer } from '../../../../components/FormInputs/Renderer';
|
||||
import { useEnterprise } from '../../../../hooks/useEnterprise';
|
||||
import { useForm, type InputProps } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { useDoc } from '../../../hooks/useDocument';
|
||||
import { type EditFieldLayout } from '../../../hooks/useDocumentLayout';
|
||||
import { getTranslation } from '../../../utils/translations';
|
||||
|
||||
@ -5,7 +5,7 @@ import { Menu } from '@strapi/design-system/v2';
|
||||
import { Plus } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useForm } from '../../../components/Form';
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { useDoc } from '../../../hooks/useDocument';
|
||||
import { useGetContentTypeConfigurationQuery } from '../../../services/contentTypes';
|
||||
import { checkIfAttributeIsDisplayable } from '../../../utils/attributes';
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
*/
|
||||
export * from './render';
|
||||
|
||||
/**
|
||||
* components
|
||||
*/
|
||||
export * from './components/Form';
|
||||
export * from './components/FormInputs/Renderer';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
*/
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import { FieldAction } from '@strapi/design-system';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FieldActionWrapper = styled(FieldAction)`
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.neutral600};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export { FieldActionWrapper };
|
||||
@ -1,11 +1,12 @@
|
||||
import { Box, Button, Flex, Main, TextInput, Typography } from '@strapi/design-system';
|
||||
import { Box, Button, Flex, Main, Typography } from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import { Form, translatedErrors, useAPIErrorHandler } from '@strapi/helper-plugin';
|
||||
import { Formik } from 'formik';
|
||||
import { translatedErrors, useAPIErrorHandler } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Form } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { Logo } from '../../../components/UnauthenticatedLogo';
|
||||
import {
|
||||
Column,
|
||||
@ -49,8 +50,8 @@ const ForgotPassword = () => {
|
||||
</Typography>
|
||||
) : null}
|
||||
</Column>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
<Form
|
||||
method="POST"
|
||||
initialValues={{
|
||||
email: '',
|
||||
}}
|
||||
@ -62,42 +63,41 @@ const ForgotPassword = () => {
|
||||
}
|
||||
}}
|
||||
validationSchema={yup.object().shape({
|
||||
email: yup.string().email(translatedErrors.email).required(translatedErrors.required),
|
||||
email: yup
|
||||
.string()
|
||||
.email({
|
||||
id: translatedErrors.email,
|
||||
defaultMessage: 'This is not a valid email.',
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'This field is required.',
|
||||
}),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
>
|
||||
{({ values, errors, handleChange }) => (
|
||||
<Form>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
<TextInput
|
||||
error={
|
||||
errors.email
|
||||
? formatMessage({
|
||||
id: errors.email,
|
||||
defaultMessage: 'This email is invalid.',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
value={values.email}
|
||||
onChange={handleChange}
|
||||
label={formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' })}
|
||||
placeholder={formatMessage({
|
||||
id: 'Auth.form.email.placeholder',
|
||||
defaultMessage: 'kai@doe.com',
|
||||
})}
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" fullWidth>
|
||||
{formatMessage({
|
||||
id: 'Auth.form.button.forgot-password',
|
||||
defaultMessage: 'Send Email',
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
{[
|
||||
{
|
||||
label: formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' }),
|
||||
name: 'email',
|
||||
placeholder: formatMessage({
|
||||
id: 'Auth.form.email.placeholder',
|
||||
defaultMessage: 'kai@doe.com',
|
||||
}),
|
||||
required: true,
|
||||
type: 'string' as const,
|
||||
},
|
||||
].map((field) => (
|
||||
<InputRenderer key={field.name} {...field} />
|
||||
))}
|
||||
<Button type="submit" fullWidth>
|
||||
{formatMessage({
|
||||
id: 'Auth.form.button.forgot-password',
|
||||
defaultMessage: 'Send Email',
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
</LayoutContent>
|
||||
<Flex justifyContent="center">
|
||||
<Box paddingTop={4}>
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Checkbox, Flex, Main, TextInput, Typography } from '@strapi/design-system';
|
||||
import { Box, Button, Flex, Main, Typography } from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import { Form, translatedErrors, useQuery } from '@strapi/helper-plugin';
|
||||
import { Eye, EyeStriked } from '@strapi/icons';
|
||||
import { Formik } from 'formik';
|
||||
import { translatedErrors, useQuery } from '@strapi/helper-plugin';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { Form } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { Logo } from '../../../components/UnauthenticatedLogo';
|
||||
import { useAuth } from '../../../features/Auth';
|
||||
import {
|
||||
@ -19,8 +18,6 @@ import {
|
||||
LayoutContent,
|
||||
} from '../../../layouts/UnauthenticatedLayout';
|
||||
|
||||
import { FieldActionWrapper } from './FieldActionWrapper';
|
||||
|
||||
import type { Login } from '../../../../../shared/contracts/authentication';
|
||||
|
||||
interface LoginProps {
|
||||
@ -28,14 +25,25 @@ interface LoginProps {
|
||||
}
|
||||
|
||||
const LOGIN_SCHEMA = yup.object().shape({
|
||||
email: yup.string().email(translatedErrors.email).required(translatedErrors.required),
|
||||
password: yup.string().required(translatedErrors.required),
|
||||
email: yup
|
||||
.string()
|
||||
.email({
|
||||
id: translatedErrors.email,
|
||||
defaultMessage: 'Not a valid email',
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'This value is required.',
|
||||
}),
|
||||
password: yup.string().required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'This value is required.',
|
||||
}),
|
||||
rememberMe: yup.bool().nullable(),
|
||||
});
|
||||
|
||||
const Login = ({ children }: LoginProps) => {
|
||||
const [apiError, setApiError] = React.useState<string>();
|
||||
const [passwordShown, setPasswordShown] = React.useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const query = useQuery();
|
||||
const navigate = useNavigate();
|
||||
@ -92,8 +100,8 @@ const Login = ({ children }: LoginProps) => {
|
||||
</Typography>
|
||||
) : null}
|
||||
</Column>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
<Form
|
||||
method="PUT"
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
@ -103,90 +111,44 @@ const Login = ({ children }: LoginProps) => {
|
||||
handleLogin(values);
|
||||
}}
|
||||
validationSchema={LOGIN_SCHEMA}
|
||||
validateOnChange={false}
|
||||
>
|
||||
{({ values, errors, handleChange }) => (
|
||||
<Form>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
<TextInput
|
||||
error={
|
||||
errors.email
|
||||
? formatMessage({
|
||||
id: errors.email,
|
||||
defaultMessage: 'This value is required.',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
value={values.email}
|
||||
onChange={handleChange}
|
||||
label={formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' })}
|
||||
placeholder={formatMessage({
|
||||
id: 'Auth.form.email.placeholder',
|
||||
defaultMessage: 'kai@doe.com',
|
||||
})}
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
<PasswordInput
|
||||
error={
|
||||
errors.password
|
||||
? formatMessage({
|
||||
id: errors.password,
|
||||
defaultMessage: 'This value is required.',
|
||||
})
|
||||
: ''
|
||||
}
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
label={formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
name="password"
|
||||
type={passwordShown ? 'text' : 'password'}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPasswordShown((prev) => !prev);
|
||||
}}
|
||||
label={formatMessage(
|
||||
passwordShown
|
||||
? {
|
||||
id: 'Auth.form.password.show-password',
|
||||
defaultMessage: 'Show password',
|
||||
}
|
||||
: {
|
||||
id: 'Auth.form.password.hide-password',
|
||||
defaultMessage: 'Hide password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{passwordShown ? <Eye /> : <EyeStriked />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
required
|
||||
/>
|
||||
<Checkbox
|
||||
onValueChange={(checked) => {
|
||||
handleChange({ target: { value: checked, name: 'rememberMe' } });
|
||||
}}
|
||||
value={values.rememberMe}
|
||||
aria-label="rememberMe"
|
||||
name="rememberMe"
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'Auth.form.rememberMe.label',
|
||||
defaultMessage: 'Remember me',
|
||||
})}
|
||||
</Checkbox>
|
||||
<Button fullWidth type="submit">
|
||||
{formatMessage({ id: 'Auth.form.button.login', defaultMessage: 'Login' })}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
{[
|
||||
{
|
||||
label: formatMessage({ id: 'Auth.form.email.label', defaultMessage: 'Email' }),
|
||||
name: 'email',
|
||||
placeholder: formatMessage({
|
||||
id: 'Auth.form.email.placeholder',
|
||||
defaultMessage: 'kai@doe.com',
|
||||
}),
|
||||
required: true,
|
||||
type: 'string' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
}),
|
||||
name: 'password',
|
||||
required: true,
|
||||
type: 'password' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.rememberMe.label',
|
||||
defaultMessage: 'Remember me',
|
||||
}),
|
||||
name: 'rememberMe',
|
||||
type: 'checkbox' as const,
|
||||
},
|
||||
].map((field) => (
|
||||
<InputRenderer key={field.name} {...field} />
|
||||
))}
|
||||
<Button fullWidth type="submit">
|
||||
{formatMessage({ id: 'Auth.form.button.login', defaultMessage: 'Login' })}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
{children}
|
||||
</LayoutContent>
|
||||
<Flex justifyContent="center">
|
||||
@ -205,11 +167,5 @@ const Login = ({ children }: LoginProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PasswordInput = styled(TextInput)`
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export { Login };
|
||||
export type { LoginProps };
|
||||
|
||||
@ -1,21 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
GridItem,
|
||||
Main,
|
||||
TextInput,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { Box, Button, Flex, Grid, GridItem, Main, Typography } from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import {
|
||||
Form,
|
||||
auth,
|
||||
getYupInnerErrors,
|
||||
translatedErrors,
|
||||
useAPIErrorHandler,
|
||||
useGuidedTour,
|
||||
@ -23,10 +11,8 @@ import {
|
||||
useQuery,
|
||||
useTracking,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Eye, EyeStriked } from '@strapi/icons';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import omit from 'lodash/omit';
|
||||
import { MessageDescriptor, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, Navigate, useNavigate, useMatch } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import * as yup from 'yup';
|
||||
@ -36,6 +22,8 @@ import {
|
||||
Register as RegisterUser,
|
||||
RegisterAdmin,
|
||||
} from '../../../../../shared/contracts/authentication';
|
||||
import { Form, FormHelpers } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { useNpsSurveySettings } from '../../../components/NpsSurvey';
|
||||
import { Logo } from '../../../components/UnauthenticatedLogo';
|
||||
import { useAuth } from '../../../features/Auth';
|
||||
@ -47,45 +35,117 @@ import {
|
||||
} from '../../../services/auth';
|
||||
import { isBaseQueryError } from '../../../utils/baseQuery';
|
||||
|
||||
import { FieldActionWrapper } from './FieldActionWrapper';
|
||||
|
||||
const REGISTER_USER_SCHEMA = yup.object().shape({
|
||||
firstname: yup.string().trim().required(translatedErrors.required),
|
||||
firstname: yup.string().trim().required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Firstname is required',
|
||||
}),
|
||||
lastname: yup.string().nullable(),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, translatedErrors.minLength)
|
||||
.matches(/[a-z]/, 'components.Input.error.contain.lowercase')
|
||||
.matches(/[A-Z]/, 'components.Input.error.contain.uppercase')
|
||||
.matches(/\d/, 'components.Input.error.contain.number')
|
||||
.required(translatedErrors.required),
|
||||
.min(8, {
|
||||
id: translatedErrors.minLength,
|
||||
defaultMessage: 'Password must be at least 8 characters',
|
||||
values: { min: 8 },
|
||||
})
|
||||
.matches(/[a-z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.lowercase',
|
||||
defaultMessage: 'Password must contain at least 1 lowercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/[A-Z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.uppercase',
|
||||
defaultMessage: 'Password must contain at least 1 uppercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/\d/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.number',
|
||||
defaultMessage: 'Password must contain at least 1 number',
|
||||
},
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Password is required',
|
||||
}),
|
||||
confirmPassword: yup
|
||||
.string()
|
||||
.oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch')
|
||||
.required(translatedErrors.required),
|
||||
registrationToken: yup.string().required(translatedErrors.required),
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Confirm password is required',
|
||||
})
|
||||
.oneOf([yup.ref('password'), null], {
|
||||
id: 'components.Input.error.password.noMatch',
|
||||
defaultMessage: 'Passwords must match',
|
||||
}),
|
||||
registrationToken: yup.string().required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Registration token is required',
|
||||
}),
|
||||
});
|
||||
|
||||
const REGISTER_ADMIN_SCHEMA = yup.object().shape({
|
||||
firstname: yup.string().trim().required(translatedErrors.required),
|
||||
firstname: yup.string().trim().required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Firstname is required',
|
||||
}),
|
||||
lastname: yup.string().nullable(),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, translatedErrors.minLength)
|
||||
.matches(/[a-z]/, 'components.Input.error.contain.lowercase')
|
||||
.matches(/[A-Z]/, 'components.Input.error.contain.uppercase')
|
||||
.matches(/\d/, 'components.Input.error.contain.number')
|
||||
.required(translatedErrors.required),
|
||||
email: yup
|
||||
.string()
|
||||
.email(translatedErrors.email)
|
||||
.strict()
|
||||
.lowercase(translatedErrors.lowercase)
|
||||
.required(translatedErrors.required),
|
||||
.min(8, {
|
||||
id: translatedErrors.minLength,
|
||||
defaultMessage: 'Password must be at least 8 characters',
|
||||
values: { min: 8 },
|
||||
})
|
||||
.matches(/[a-z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.lowercase',
|
||||
defaultMessage: 'Password must contain at least 1 lowercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/[A-Z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.uppercase',
|
||||
defaultMessage: 'Password must contain at least 1 uppercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/\d/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.number',
|
||||
defaultMessage: 'Password must contain at least 1 number',
|
||||
},
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Password is required',
|
||||
}),
|
||||
confirmPassword: yup
|
||||
.string()
|
||||
.oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch')
|
||||
.required(translatedErrors.required),
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Confirm password is required',
|
||||
})
|
||||
.oneOf([yup.ref('password'), null], {
|
||||
id: 'components.Input.error.password.noMatch',
|
||||
defaultMessage: 'Passwords must match',
|
||||
}),
|
||||
email: yup
|
||||
.string()
|
||||
.email({
|
||||
id: translatedErrors.email,
|
||||
defaultMessage: 'Not a valid email',
|
||||
})
|
||||
.strict()
|
||||
.lowercase({
|
||||
id: translatedErrors.lowercase,
|
||||
defaultMessage: 'Email must be lowercase',
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Email is required',
|
||||
}),
|
||||
});
|
||||
|
||||
interface RegisterProps {
|
||||
@ -105,8 +165,6 @@ interface RegisterFormValues {
|
||||
const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
const toggleNotification = useNotification();
|
||||
const navigate = useNavigate();
|
||||
const [passwordShown, setPasswordShown] = React.useState(false);
|
||||
const [confirmPasswordShown, setConfirmPasswordShown] = React.useState(false);
|
||||
const [submitCount, setSubmitCount] = React.useState(0);
|
||||
const [apiError, setApiError] = React.useState<string>();
|
||||
const { trackUsage } = useTracking();
|
||||
@ -145,7 +203,7 @@ const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
|
||||
const handleRegisterAdmin = async (
|
||||
{ news, ...body }: RegisterAdmin.Request['body'] & { news: boolean },
|
||||
setFormErrors: FormikHelpers<RegisterFormValues>['setErrors']
|
||||
setFormErrors: FormHelpers<RegisterFormValues>['setErrors']
|
||||
) => {
|
||||
const res = await registerAdmin(body);
|
||||
|
||||
@ -191,7 +249,7 @@ const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
|
||||
const handleRegisterUser = async (
|
||||
{ news, ...body }: RegisterUser.Request['body'] & { news: boolean },
|
||||
setFormErrors: FormikHelpers<RegisterFormValues>['setErrors']
|
||||
setFormErrors: FormHelpers<RegisterFormValues>['setErrors']
|
||||
) => {
|
||||
const res = await registerUser(body);
|
||||
|
||||
@ -259,8 +317,8 @@ const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
</Typography>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
<Form
|
||||
method="POST"
|
||||
initialValues={
|
||||
{
|
||||
firstname: userInfo?.firstname || '',
|
||||
@ -272,7 +330,7 @@ const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
news: false,
|
||||
} satisfies RegisterFormValues
|
||||
}
|
||||
onSubmit={async (data, formik) => {
|
||||
onSubmit={async (data, helpers) => {
|
||||
const normalizedData = normalizeData(data);
|
||||
|
||||
try {
|
||||
@ -294,200 +352,131 @@ const Register = ({ hasAdmin }: RegisterProps) => {
|
||||
registrationToken: normalizedData.registrationToken,
|
||||
news: normalizedData.news,
|
||||
},
|
||||
formik.setErrors
|
||||
helpers.setErrors
|
||||
);
|
||||
} else {
|
||||
await handleRegisterAdmin(
|
||||
omit(normalizedData, ['registrationToken', 'confirmPassword']),
|
||||
formik.setErrors
|
||||
helpers.setErrors
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ValidationError) {
|
||||
const errors = getYupInnerErrors(err);
|
||||
|
||||
formik.setErrors(errors);
|
||||
helpers.setErrors(
|
||||
err.inner.reduce<Record<string, string>>((acc, { message, path }) => {
|
||||
if (path && typeof message === 'object') {
|
||||
acc[path] = formatMessage(message);
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
setSubmitCount(submitCount + 1);
|
||||
}
|
||||
}}
|
||||
validateOnChange={false}
|
||||
>
|
||||
{({ values, errors, handleChange }) => {
|
||||
return (
|
||||
<Form>
|
||||
<Main>
|
||||
<Flex direction="column" alignItems="stretch" gap={6} marginTop={7}>
|
||||
<Grid gap={4}>
|
||||
<GridItem col={6}>
|
||||
<TextInput
|
||||
name="firstname"
|
||||
required
|
||||
value={values.firstname}
|
||||
error={
|
||||
errors.firstname
|
||||
? formatMessage(errors.firstname as MessageDescriptor)
|
||||
: undefined
|
||||
}
|
||||
onChange={handleChange}
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.firstname.label',
|
||||
defaultMessage: 'Firstname',
|
||||
<Flex direction="column" alignItems="stretch" gap={6} marginTop={7}>
|
||||
<Grid gap={4}>
|
||||
{[
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.firstname.label',
|
||||
defaultMessage: 'Firstname',
|
||||
}),
|
||||
name: 'firstname',
|
||||
required: true,
|
||||
size: 6,
|
||||
type: 'string' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.lastname.label',
|
||||
defaultMessage: 'Lastname',
|
||||
}),
|
||||
name: 'lastname',
|
||||
size: 6,
|
||||
type: 'string' as const,
|
||||
},
|
||||
{
|
||||
disabled: !isAdminRegistration,
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.email.label',
|
||||
defaultMessage: 'Email',
|
||||
}),
|
||||
name: 'email',
|
||||
required: true,
|
||||
size: 12,
|
||||
type: 'email' as const,
|
||||
},
|
||||
{
|
||||
hint: formatMessage({
|
||||
id: 'Auth.form.password.hint',
|
||||
defaultMessage:
|
||||
'Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number',
|
||||
}),
|
||||
label: formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
}),
|
||||
name: 'password',
|
||||
required: true,
|
||||
size: 12,
|
||||
type: 'password' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.confirmPassword.label',
|
||||
defaultMessage: 'Confirm Password',
|
||||
}),
|
||||
name: 'confirmPassword',
|
||||
required: true,
|
||||
size: 12,
|
||||
type: 'password' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage(
|
||||
{
|
||||
id: 'Auth.form.register.news.label',
|
||||
defaultMessage:
|
||||
'Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).',
|
||||
},
|
||||
{
|
||||
terms: (
|
||||
<A target="_blank" href="https://strapi.io/terms" rel="noreferrer">
|
||||
{formatMessage({
|
||||
id: 'Auth.privacy-policy-agreement.terms',
|
||||
defaultMessage: 'terms',
|
||||
})}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem col={6}>
|
||||
<TextInput
|
||||
name="lastname"
|
||||
value={values.lastname}
|
||||
onChange={handleChange}
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.lastname.label',
|
||||
defaultMessage: 'Lastname',
|
||||
</A>
|
||||
),
|
||||
policy: (
|
||||
<A target="_blank" href="https://strapi.io/privacy" rel="noreferrer">
|
||||
{formatMessage({
|
||||
id: 'Auth.privacy-policy-agreement.policy',
|
||||
defaultMessage: 'policy',
|
||||
})}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<TextInput
|
||||
name="email"
|
||||
disabled={!isAdminRegistration}
|
||||
value={values.email}
|
||||
onChange={handleChange}
|
||||
error={
|
||||
errors.email ? formatMessage(errors.email as MessageDescriptor) : undefined
|
||||
}
|
||||
required
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.email.label',
|
||||
defaultMessage: 'Email',
|
||||
})}
|
||||
type="email"
|
||||
/>
|
||||
<PasswordInput
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
error={
|
||||
errors.password
|
||||
? formatMessage(errors.password as MessageDescriptor)
|
||||
: undefined
|
||||
}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPasswordShown((prev) => !prev);
|
||||
}}
|
||||
label={formatMessage(
|
||||
passwordShown
|
||||
? {
|
||||
id: 'Auth.form.password.show-password',
|
||||
defaultMessage: 'Show password',
|
||||
}
|
||||
: {
|
||||
id: 'Auth.form.password.hide-password',
|
||||
defaultMessage: 'Hide password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{passwordShown ? <Eye /> : <EyeStriked />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
hint={formatMessage({
|
||||
id: 'Auth.form.password.hint',
|
||||
defaultMessage:
|
||||
'Must be at least 8 characters, 1 uppercase, 1 lowercase & 1 number',
|
||||
})}
|
||||
required
|
||||
label={formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
type={passwordShown ? 'text' : 'password'}
|
||||
/>
|
||||
<PasswordInput
|
||||
name="confirmPassword"
|
||||
onChange={handleChange}
|
||||
value={values.confirmPassword}
|
||||
error={
|
||||
errors.confirmPassword
|
||||
? formatMessage(errors.confirmPassword as MessageDescriptor)
|
||||
: undefined
|
||||
}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setConfirmPasswordShown((prev) => !prev);
|
||||
}}
|
||||
label={formatMessage(
|
||||
confirmPasswordShown
|
||||
? {
|
||||
id: 'Auth.form.password.show-password',
|
||||
defaultMessage: 'Show password',
|
||||
}
|
||||
: {
|
||||
id: 'Auth.form.password.hide-password',
|
||||
defaultMessage: 'Hide password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{confirmPasswordShown ? <Eye /> : <EyeStriked />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
required
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.confirmPassword.label',
|
||||
defaultMessage: 'Confirm Password',
|
||||
})}
|
||||
type={confirmPasswordShown ? 'text' : 'password'}
|
||||
/>
|
||||
<Checkbox
|
||||
onValueChange={(checked) => {
|
||||
handleChange({ target: { value: checked, name: 'news' } });
|
||||
}}
|
||||
value={values.news}
|
||||
name="news"
|
||||
aria-label="news"
|
||||
>
|
||||
{formatMessage(
|
||||
{
|
||||
id: 'Auth.form.register.news.label',
|
||||
defaultMessage:
|
||||
'Keep me updated about new features & upcoming improvements (by doing this you accept the {terms} and the {policy}).',
|
||||
},
|
||||
{
|
||||
terms: (
|
||||
<A target="_blank" href="https://strapi.io/terms" rel="noreferrer">
|
||||
{formatMessage({
|
||||
id: 'Auth.privacy-policy-agreement.terms',
|
||||
defaultMessage: 'terms',
|
||||
})}
|
||||
</A>
|
||||
),
|
||||
policy: (
|
||||
<A target="_blank" href="https://strapi.io/privacy" rel="noreferrer">
|
||||
{formatMessage({
|
||||
id: 'Auth.privacy-policy-agreement.policy',
|
||||
defaultMessage: 'policy',
|
||||
})}
|
||||
</A>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Checkbox>
|
||||
<Button fullWidth size="L" type="submit">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.button.register',
|
||||
defaultMessage: "Let's start",
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Main>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</A>
|
||||
),
|
||||
}
|
||||
),
|
||||
name: 'news',
|
||||
size: 12,
|
||||
type: 'checkbox' as const,
|
||||
},
|
||||
].map(({ size, ...field }) => (
|
||||
<GridItem key={field.name} col={size}>
|
||||
<InputRenderer {...field} />
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
<Button fullWidth size="L" type="submit">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.button.register',
|
||||
defaultMessage: "Let's start",
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
{match?.params.authType === 'register' && (
|
||||
<Box paddingTop={4}>
|
||||
<Flex justifyContent="center">
|
||||
@ -560,11 +549,5 @@ const A = styled.a`
|
||||
color: ${({ theme }) => theme.colors.primary600};
|
||||
`;
|
||||
|
||||
const PasswordInput = styled(TextInput)`
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export { Register };
|
||||
export type { RegisterProps };
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Flex, Main, TextInput, Typography } from '@strapi/design-system';
|
||||
import { Box, Button, Flex, Main, Typography } from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import { Form, translatedErrors, useAPIErrorHandler, useQuery } from '@strapi/helper-plugin';
|
||||
import { Eye, EyeStriked } from '@strapi/icons';
|
||||
import { Formik } from 'formik';
|
||||
import { translatedErrors, useAPIErrorHandler, useQuery } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, Navigate, useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { NavLink, useNavigate, Navigate } from 'react-router-dom';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { ResetPassword } from '../../../../../shared/contracts/authentication';
|
||||
import { Form } from '../../../components/Form';
|
||||
import { InputRenderer } from '../../../components/FormInputs/Renderer';
|
||||
import { Logo } from '../../../components/UnauthenticatedLogo';
|
||||
import { useAuth } from '../../../features/Auth';
|
||||
import {
|
||||
@ -21,25 +18,49 @@ import {
|
||||
import { useResetPasswordMutation } from '../../../services/auth';
|
||||
import { isBaseQueryError } from '../../../utils/baseQuery';
|
||||
|
||||
import { FieldActionWrapper } from './FieldActionWrapper';
|
||||
|
||||
const RESET_PASSWORD_SCHEMA = yup.object().shape({
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, translatedErrors.minLength)
|
||||
.matches(/[a-z]/, 'components.Input.error.contain.lowercase')
|
||||
.matches(/[A-Z]/, 'components.Input.error.contain.uppercase')
|
||||
.matches(/\d/, 'components.Input.error.contain.number')
|
||||
.required(translatedErrors.required),
|
||||
.min(8, {
|
||||
id: translatedErrors.minLength,
|
||||
defaultMessage: 'Password must be at least 8 characters',
|
||||
values: { min: 8 },
|
||||
})
|
||||
.matches(/[a-z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.lowercase',
|
||||
defaultMessage: 'Password must contain at least 1 lowercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/[A-Z]/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.uppercase',
|
||||
defaultMessage: 'Password must contain at least 1 uppercase letter',
|
||||
},
|
||||
})
|
||||
.matches(/\d/, {
|
||||
message: {
|
||||
id: 'components.Input.error.contain.number',
|
||||
defaultMessage: 'Password must contain at least 1 number',
|
||||
},
|
||||
})
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Password is required',
|
||||
}),
|
||||
confirmPassword: yup
|
||||
.string()
|
||||
.oneOf([yup.ref('password'), null], 'components.Input.error.password.noMatch')
|
||||
.required(translatedErrors.required),
|
||||
.required({
|
||||
id: translatedErrors.required,
|
||||
defaultMessage: 'Confirm password is required',
|
||||
})
|
||||
.oneOf([yup.ref('password'), null], {
|
||||
id: 'components.Input.error.password.noMatch',
|
||||
defaultMessage: 'Passwords must match',
|
||||
}),
|
||||
});
|
||||
|
||||
const ResetPassword = () => {
|
||||
const [passwordShown, setPasswordShown] = React.useState(false);
|
||||
const [confirmPasswordShown, setConfirmPasswordShown] = React.useState(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const query = useQuery();
|
||||
@ -90,8 +111,8 @@ const ResetPassword = () => {
|
||||
</Typography>
|
||||
) : null}
|
||||
</Column>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
<Form
|
||||
method="POST"
|
||||
initialValues={{
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
@ -101,111 +122,43 @@ const ResetPassword = () => {
|
||||
handleSubmit({ password: values.password, resetPasswordToken: query.get('code')! });
|
||||
}}
|
||||
validationSchema={RESET_PASSWORD_SCHEMA}
|
||||
validateOnChange={false}
|
||||
>
|
||||
{({ values, errors, handleChange }) => (
|
||||
<Form>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
<PasswordInput
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={values.password}
|
||||
error={
|
||||
errors.password
|
||||
? formatMessage(
|
||||
{
|
||||
id: errors.password,
|
||||
defaultMessage: 'This field is required.',
|
||||
},
|
||||
{
|
||||
min: 8,
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setPasswordShown((prev) => !prev);
|
||||
}}
|
||||
label={formatMessage(
|
||||
passwordShown
|
||||
? {
|
||||
id: 'Auth.form.password.show-password',
|
||||
defaultMessage: 'Show password',
|
||||
}
|
||||
: {
|
||||
id: 'Auth.form.password.hide-password',
|
||||
defaultMessage: 'Hide password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{passwordShown ? <Eye /> : <EyeStriked />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
hint={formatMessage({
|
||||
id: 'Auth.form.password.hint',
|
||||
defaultMessage:
|
||||
'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number',
|
||||
})}
|
||||
required
|
||||
label={formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
type={passwordShown ? 'text' : 'password'}
|
||||
/>
|
||||
<PasswordInput
|
||||
name="confirmPassword"
|
||||
onChange={handleChange}
|
||||
value={values.confirmPassword}
|
||||
error={
|
||||
errors.confirmPassword
|
||||
? formatMessage({
|
||||
id: errors.confirmPassword,
|
||||
defaultMessage: 'This value is required.',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setConfirmPasswordShown((prev) => !prev);
|
||||
}}
|
||||
label={formatMessage(
|
||||
passwordShown
|
||||
? {
|
||||
id: 'Auth.form.password.show-password',
|
||||
defaultMessage: 'Show password',
|
||||
}
|
||||
: {
|
||||
id: 'Auth.form.password.hide-password',
|
||||
defaultMessage: 'Hide password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{confirmPasswordShown ? <Eye /> : <EyeStriked />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
required
|
||||
label={formatMessage({
|
||||
id: 'Auth.form.confirmPassword.label',
|
||||
defaultMessage: 'Confirm Password',
|
||||
})}
|
||||
type={confirmPasswordShown ? 'text' : 'password'}
|
||||
/>
|
||||
<Button fullWidth type="submit">
|
||||
{formatMessage({
|
||||
id: 'global.change-password',
|
||||
defaultMessage: 'Change password',
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
{[
|
||||
{
|
||||
hint: formatMessage({
|
||||
id: 'Auth.form.password.hint',
|
||||
defaultMessage:
|
||||
'Password must contain at least 8 characters, 1 uppercase, 1 lowercase and 1 number',
|
||||
}),
|
||||
label: formatMessage({
|
||||
id: 'global.password',
|
||||
defaultMessage: 'Password',
|
||||
}),
|
||||
name: 'password',
|
||||
required: true,
|
||||
type: 'password' as const,
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: 'Auth.form.confirmPassword.label',
|
||||
defaultMessage: 'Confirm Password',
|
||||
}),
|
||||
name: 'confirmPassword',
|
||||
required: true,
|
||||
type: 'password' as const,
|
||||
},
|
||||
].map((field) => (
|
||||
<InputRenderer key={field.name} {...field} />
|
||||
))}
|
||||
<Button fullWidth type="submit">
|
||||
{formatMessage({
|
||||
id: 'global.change-password',
|
||||
defaultMessage: 'Change password',
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
</LayoutContent>
|
||||
<Flex justifyContent="center">
|
||||
<Box paddingTop={4}>
|
||||
@ -220,10 +173,4 @@ const ResetPassword = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const PasswordInput = styled(TextInput)`
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export { ResetPassword };
|
||||
|
||||
@ -27,7 +27,7 @@ describe('ResetPassword', () => {
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Send Email' }));
|
||||
|
||||
expect(await findByText('This email is invalid.')).toBeInTheDocument();
|
||||
expect(await findByText('This is not a valid email.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ describe('Register', () => {
|
||||
/keep me updated about new features & upcoming improvements \(by doing this you accept the and the \)\./i
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(getByRole('checkbox', { name: /news/i })).toBeInTheDocument();
|
||||
expect(getByRole('checkbox', { name: /Keep me updated/i })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: /let's start/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ describe('ResetPassword', () => {
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Change password' }));
|
||||
|
||||
expect(await findByText('This value is required.')).toBeInTheDocument();
|
||||
expect(await findByText('Passwords must match')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fail if we do not fill in the password field', async () => {
|
||||
@ -43,7 +43,8 @@ describe('ResetPassword', () => {
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Change password' }));
|
||||
|
||||
expect(await findByText('This value is required.')).toBeInTheDocument();
|
||||
expect(await findByText('Password must be at least 8 characters')).toBeInTheDocument();
|
||||
expect(await findByText('Passwords must match')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fail if the passwords do not match', async () => {
|
||||
@ -56,7 +57,7 @@ describe('ResetPassword', () => {
|
||||
|
||||
fireEvent.click(getByRole('button', { name: 'Change password' }));
|
||||
|
||||
expect(await findByText('This value is required.')).toBeInTheDocument();
|
||||
expect(await findByText('Passwords must match')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
FieldAction,
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
Form,
|
||||
GenericInput,
|
||||
GenericInputProps,
|
||||
LoadingIndicatorPage,
|
||||
@ -30,7 +29,7 @@ import {
|
||||
useAPIErrorHandler,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Check, Eye, EyeStriked } from '@strapi/icons';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { ContentLayout, Flex, Main } from '@strapi/design-system';
|
||||
import {
|
||||
CheckPagePermissions,
|
||||
Form,
|
||||
SettingsPageTitle,
|
||||
useAPIErrorHandler,
|
||||
useFocusWhenNavigate,
|
||||
@ -13,7 +12,7 @@ import {
|
||||
useRBAC,
|
||||
useTracking,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLocation, useMatch, useNavigate } from 'react-router-dom';
|
||||
|
||||
@ -201,8 +200,8 @@ export const EditView = () => {
|
||||
if (isCreating) {
|
||||
const res = await createToken({
|
||||
...body,
|
||||
// in case a token has a lifespan of "unlimited" the API only accepts zero as a number
|
||||
lifespan: body.lifespan === '0' ? parseInt(body.lifespan) : null,
|
||||
// lifespan must be "null" for unlimited (0 would mean instantly expired and isn't accepted)
|
||||
lifespan: body?.lifespan || null,
|
||||
permissions: body.type === 'custom' ? state.selectedActions : null,
|
||||
});
|
||||
|
||||
@ -341,7 +340,7 @@ export const EditView = () => {
|
||||
name: apiToken?.name || '',
|
||||
description: apiToken?.description || '',
|
||||
type: apiToken?.type,
|
||||
lifespan: apiToken?.lifespan ? apiToken.lifespan.toString() : apiToken?.lifespan,
|
||||
lifespan: apiToken?.lifespan,
|
||||
}}
|
||||
enableReinitialize
|
||||
onSubmit={(body, actions) => handleSubmit(body, actions)}
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import {
|
||||
CheckPagePermissions,
|
||||
Form,
|
||||
LoadingIndicatorPage,
|
||||
SettingsPageTitle,
|
||||
useNotification,
|
||||
@ -27,7 +26,7 @@ import {
|
||||
} from '@strapi/helper-plugin';
|
||||
import { ArrowLeft } from '@strapi/icons';
|
||||
import { format } from 'date-fns';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, useNavigate, useMatch } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
CheckPagePermissions,
|
||||
Form,
|
||||
LoadingIndicatorPage,
|
||||
SettingsPageTitle,
|
||||
useAPIErrorHandler,
|
||||
@ -26,7 +25,7 @@ import {
|
||||
translatedErrors,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Check } from '@strapi/icons';
|
||||
import { Formik, FormikErrors, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikErrors, FormikHelpers } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLocation, useNavigate, useMatch } from 'react-router-dom';
|
||||
import * as yup from 'yup';
|
||||
@ -150,6 +149,8 @@ const EditView = () => {
|
||||
if (isCreating) {
|
||||
const res = await createToken({
|
||||
...body,
|
||||
// lifespan must be "null" for unlimited (0 would mean instantly expired and isn't accepted)
|
||||
lifespan: body?.lifespan || null,
|
||||
permissions,
|
||||
});
|
||||
|
||||
@ -181,7 +182,7 @@ const EditView = () => {
|
||||
tokenType: TRANSFER_TOKEN_TYPE,
|
||||
});
|
||||
|
||||
navigate(res.data.id.toString(), {
|
||||
navigate(`../transfer-tokens/${res.data.id.toString()}`, {
|
||||
replace: true,
|
||||
state: { transferToken: res.data },
|
||||
});
|
||||
@ -254,7 +255,7 @@ const EditView = () => {
|
||||
{
|
||||
name: transferToken?.name || '',
|
||||
description: transferToken?.description || '',
|
||||
lifespan: transferToken?.lifespan ?? null,
|
||||
lifespan: transferToken?.lifespan || null,
|
||||
/**
|
||||
* We need to cast the permissions to satisfy the type for `permissions`
|
||||
* in the request body incase we don't have a transferToken and instead
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import {
|
||||
Form,
|
||||
GenericInput,
|
||||
LoadingIndicatorPage,
|
||||
SettingsPageTitle,
|
||||
@ -25,8 +24,7 @@ import {
|
||||
useRBAC,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, Check } from '@strapi/icons';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import omit from 'lodash/omit';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import pick from 'lodash/pick';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { NavLink, Navigate, useLocation, useMatch, useNavigate } from 'react-router-dom';
|
||||
@ -177,13 +175,10 @@ const EditPage = () => {
|
||||
confirmPassword: '',
|
||||
} satisfies InitialData;
|
||||
|
||||
/**
|
||||
* TODO: Convert this to react-query.
|
||||
*/
|
||||
const handleSubmit = async (body: InitialData, actions: FormikHelpers<InitialData>) => {
|
||||
lockApp?.();
|
||||
|
||||
const { confirmPassword, password, ...bodyRest } = body;
|
||||
const { confirmPassword: _confirmPassword, password, ...bodyRest } = body;
|
||||
|
||||
const res = await updateUser({
|
||||
id,
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import { Breadcrumbs, Crumb } from '@strapi/design-system/v2';
|
||||
import {
|
||||
Form,
|
||||
GenericInput,
|
||||
useNotification,
|
||||
useOverlayBlocker,
|
||||
@ -22,7 +21,7 @@ import {
|
||||
useAPIErrorHandler,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Entity } from '@strapi/types';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as yup from 'yup';
|
||||
|
||||
|
||||
@ -11,10 +11,9 @@ import {
|
||||
TextInput,
|
||||
} from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import { Form } from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, Check, Play as Publish } from '@strapi/icons';
|
||||
import { Webhook } from '@strapi/types';
|
||||
import { Field, FormikHelpers, FormikProvider, useFormik } from 'formik';
|
||||
import { Field, Form, FormikHelpers, FormikProvider, useFormik } from 'formik';
|
||||
import { IntlShape, useIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import * as yup from 'yup';
|
||||
|
||||
@ -591,7 +591,7 @@
|
||||
"components.Input.error.contentTypeName.taken": "This name already exists",
|
||||
"components.Input.error.custom-error": "{errorMessage} ",
|
||||
"components.Input.error.password.noMatch": "Passwords do not match",
|
||||
"components.Input.error.validation.email": "This is an invalid email",
|
||||
"components.Input.error.validation.email": "This is not a valid email",
|
||||
"components.Input.error.validation.json": "This doesn't match the JSON format",
|
||||
"components.Input.error.validation.lowercase": "The value must be a lowercase string",
|
||||
"components.Input.error.validation.max": "The value is too high (max: {max}).",
|
||||
|
||||
@ -2,7 +2,7 @@ import { Combobox, ComboboxOption, Field, Flex } from '@strapi/design-system';
|
||||
import { useAPIErrorHandler, useNotification, useRBAC } from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useField } from '../../../../../../../admin/src/content-manager/components/Form';
|
||||
import { useField } from '../../../../../../../admin/src/components/Form';
|
||||
import { useDoc } from '../../../../../../../admin/src/content-manager/hooks/useDocument';
|
||||
import { getDisplayName } from '../../../../../../../admin/src/content-manager/utils/users';
|
||||
import { useTypedSelector } from '../../../../../../../admin/src/core/store/hooks';
|
||||
|
||||
@ -14,7 +14,7 @@ import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
||||
import { Entity } from '@strapi/types';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useField } from '../../../../../../../admin/src/content-manager/components/Form';
|
||||
import { useField } from '../../../../../../../admin/src/components/Form';
|
||||
import { useDoc } from '../../../../../../../admin/src/content-manager/hooks/useDocument';
|
||||
import { useLicenseLimits } from '../../../../hooks/useLicenseLimits';
|
||||
import { LimitsModal } from '../../../../pages/SettingsPage/pages/ReviewWorkflows/components/LimitsModal';
|
||||
|
||||
@ -2,7 +2,7 @@ import { render as renderRTL, waitFor, server } from '@tests/utils';
|
||||
import { rest } from 'msw';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Form } from '../../../../../../../../admin/src/content-manager/components/Form';
|
||||
import { Form } from '../../../../../../../../admin/src/components/Form';
|
||||
import { AssigneeSelect } from '../AssigneeSelect';
|
||||
import { ASSIGNEE_ATTRIBUTE_NAME } from '../constants';
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { render as renderRTL, waitFor, server } from '@tests/utils';
|
||||
import { rest } from 'msw';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Form } from '../../../../../../../../admin/src/content-manager/components/Form';
|
||||
import { Form } from '../../../../../../../../admin/src/components/Form';
|
||||
import { StageSelect } from '../StageSelect';
|
||||
|
||||
/**
|
||||
|
||||
@ -33,7 +33,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useDragAndDrop } from '../../../../../../../../admin/src/content-manager/hooks/useDragAndDrop';
|
||||
import { composeRefs } from '../../../../../../../../admin/src/content-manager/utils/refs';
|
||||
import { composeRefs } from '../../../../../../../../admin/src/utils/refs';
|
||||
import { StagePermission } from '../../../../../../../../shared/contracts/review-workflows';
|
||||
import {
|
||||
cloneStage,
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
CheckPagePermissions,
|
||||
Form,
|
||||
LoadingIndicatorPage,
|
||||
SettingsPageTitle,
|
||||
translatedErrors,
|
||||
@ -27,7 +26,7 @@ import {
|
||||
useRBAC,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Check } from '@strapi/icons';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import * as yup from 'yup';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import crypto from 'crypto';
|
||||
import { omit, difference, isNil, isEmpty, map, isArray, uniq } from 'lodash/fp';
|
||||
import { omit, difference, isNil, isEmpty, map, isArray, uniq, isNumber } from 'lodash/fp';
|
||||
import { errors } from '@strapi/utils';
|
||||
import type { Update, ApiToken, ApiTokenBody } from '../../../shared/contracts/api-token';
|
||||
import constants from './constants';
|
||||
@ -61,14 +61,25 @@ const assertCustomTokenPermissionsValidity = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that a token's lifespan is valid
|
||||
* Check if a token's lifespan is valid
|
||||
*/
|
||||
const assertValidLifespan = (lifespan: ApiTokenBody['lifespan']) => {
|
||||
const isValidLifespan = (lifespan: unknown) => {
|
||||
if (isNil(lifespan)) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan as number)) {
|
||||
if (!isNumber(lifespan) || !Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that a token's lifespan is valid
|
||||
*/
|
||||
const assertValidLifespan = (lifespan: unknown) => {
|
||||
if (!isValidLifespan(lifespan)) {
|
||||
throw new ValidationError(
|
||||
`lifespan must be one of the following values:
|
||||
${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}`
|
||||
@ -138,14 +149,14 @@ const hash = (accessKey: string) => {
|
||||
|
||||
const getExpirationFields = (lifespan: ApiTokenBody['lifespan']) => {
|
||||
// it must be nil or a finite number >= 0
|
||||
const isValidNumber = Number.isFinite(lifespan) && (lifespan as number) > 0;
|
||||
const isValidNumber = isNumber(lifespan) && Number.isFinite(lifespan) && lifespan > 0;
|
||||
if (!isValidNumber && !isNil(lifespan)) {
|
||||
throw new ValidationError('lifespan must be a positive number or null');
|
||||
}
|
||||
|
||||
return {
|
||||
lifespan: lifespan || null,
|
||||
expiresAt: lifespan ? Date.now() + (lifespan as number) : null,
|
||||
expiresAt: lifespan ? Date.now() + lifespan : null,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
import assert from 'assert';
|
||||
import { map, isArray, omit, uniq, isNil, difference, isEmpty } from 'lodash/fp';
|
||||
import { map, isArray, omit, uniq, isNil, difference, isEmpty, isNumber } from 'lodash/fp';
|
||||
import { errors } from '@strapi/utils';
|
||||
import '@strapi/types';
|
||||
import constants from '../constants';
|
||||
@ -79,7 +79,7 @@ const create = async (attributes: TokenCreatePayload): Promise<TransferToken> =>
|
||||
delete attributes.accessKey;
|
||||
|
||||
assertTokenPermissionsValidity(attributes);
|
||||
assertValidLifespan(attributes);
|
||||
assertValidLifespan(attributes.lifespan);
|
||||
|
||||
const result = (await strapi.db.transaction(async () => {
|
||||
const transferToken = await strapi.query(TRANSFER_TOKEN_UID).create({
|
||||
@ -131,7 +131,7 @@ const update = async (
|
||||
}
|
||||
|
||||
assertTokenPermissionsValidity(attributes);
|
||||
assertValidLifespan(attributes);
|
||||
assertValidLifespan(attributes.lifespan);
|
||||
|
||||
return strapi.db.transaction(async () => {
|
||||
const updatedToken = await strapi.query(TRANSFER_TOKEN_UID).update({
|
||||
@ -281,11 +281,9 @@ const regenerate = async (id: string | number): Promise<TransferToken> => {
|
||||
};
|
||||
};
|
||||
|
||||
const getExpirationFields = (
|
||||
lifespan: number | null
|
||||
): { lifespan: null | number; expiresAt: null | number } => {
|
||||
const getExpirationFields = (lifespan: TransferToken['lifespan']) => {
|
||||
// it must be nil or a finite number >= 0
|
||||
const isValidNumber = Number.isFinite(lifespan) && lifespan !== null && lifespan > 0;
|
||||
const isValidNumber = isNumber(lifespan) && Number.isFinite(lifespan) && lifespan > 0;
|
||||
if (!isValidNumber && !isNil(lifespan)) {
|
||||
throw new ValidationError('lifespan must be a positive number or null');
|
||||
}
|
||||
@ -359,14 +357,28 @@ const assertTokenPermissionsValidity = (attributes: TokenUpdatePayload) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that a token's lifespan is valid
|
||||
* Check if a token's lifespan is valid
|
||||
*/
|
||||
const assertValidLifespan = ({ lifespan }: { lifespan?: TransferToken['lifespan'] }) => {
|
||||
const isValidLifespan = (lifespan: unknown) => {
|
||||
if (isNil(lifespan)) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Object.values(constants.TRANSFER_TOKEN_LIFESPANS).includes(lifespan)) {
|
||||
if (
|
||||
!isNumber(lifespan) ||
|
||||
!Object.values(constants.TRANSFER_TOKEN_LIFESPANS).includes(lifespan)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that a token's lifespan is valid
|
||||
*/
|
||||
const assertValidLifespan = (lifespan: unknown) => {
|
||||
if (!isValidLifespan(lifespan)) {
|
||||
throw new ValidationError(
|
||||
`lifespan must be one of the following values:
|
||||
${Object.values(constants.TRANSFER_TOKEN_LIFESPANS).join(', ')}`
|
||||
|
||||
@ -8,7 +8,7 @@ export type ApiToken = {
|
||||
expiresAt: string;
|
||||
id: Entity.ID;
|
||||
lastUsedAt: string | null;
|
||||
lifespan: string | number;
|
||||
lifespan: string | number | null;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
type: 'custom' | 'full-access' | 'read-only';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { errors } from '@strapi/utils';
|
||||
|
||||
export interface TransferTokenPermission {
|
||||
id: number | string;
|
||||
id: number | `${number}`;
|
||||
action: 'push' | 'pull' | 'push-pull';
|
||||
token: TransferToken | number;
|
||||
}
|
||||
@ -12,7 +12,7 @@ export interface DatabaseTransferToken {
|
||||
description: string;
|
||||
accessKey: string;
|
||||
lastUsedAt?: number;
|
||||
lifespan: number | null;
|
||||
lifespan: string | number | null;
|
||||
expiresAt: number;
|
||||
permissions: TransferTokenPermission[];
|
||||
}
|
||||
|
||||
@ -298,6 +298,9 @@ const TimezoneComponent = ({ timezoneOptions }: { timezoneOptions: ITimezoneOpti
|
||||
onChange={(timezone) => {
|
||||
setFieldValue('timezone', timezone);
|
||||
}}
|
||||
onTextValueChange={(timezone) => {
|
||||
setFieldValue('timezone', timezone);
|
||||
}}
|
||||
onClear={() => {
|
||||
setFieldValue('timezone', '');
|
||||
}}
|
||||
|
||||
@ -138,7 +138,7 @@ describe('CMReleasesContainer', () => {
|
||||
})
|
||||
);
|
||||
|
||||
render(<CMReleasesContainer />);
|
||||
render();
|
||||
|
||||
const informationBox = await screen.findByRole('complementary', { name: 'Releases' });
|
||||
const release1 = await within(informationBox).findByText('01/01/2024 at 11:00 (UTC+01:00)');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user