feat: add i18n to admin panel (#1326)
Co-authored-by: shikhantmaungs <shinkhantmaungs@gmail.com> Co-authored-by: Thomas Ghysels <info@thomasg.be> Co-authored-by: Kokutse Djoguenou <kokutse@Kokutses-MacBook-Pro.local> Co-authored-by: Christian Gil <47041342+ChrisGV04@users.noreply.github.com> Co-authored-by: Łukasz Rabiec <lukaszrabiec@gmail.com> Co-authored-by: Jenny <jennifer.eberlei@gmail.com> Co-authored-by: Hung Vu <hunghvu2017@gmail.com> Co-authored-by: Shin Khant Maung <101539335+shinkhantmaungs@users.noreply.github.com> Co-authored-by: Carlo Brualdi <carlo.brualdi@gmail.com> Co-authored-by: Ariel Tonglet <ariel.tonglet@gmail.com> Co-authored-by: Roman Ryzhikov <general+github@ya.ru> Co-authored-by: maekoya <maekoya@stromatolite.jp> Co-authored-by: Emilia Trollros <3m1l1a@emiliatrollros.se> Co-authored-by: Kokutse J Djoguenou <90865585+Julesdj@users.noreply.github.com> Co-authored-by: Mitch Dries <mitch.dries@gmail.com> BREAKING CHANGE: If you assigned labels to collections, globals or block names, you need to update your config! Your GraphQL schema and generated Typescript interfaces may have changed. Payload no longer uses labels for code based naming. To prevent breaking changes to your GraphQL API and typescript types in your project, you can assign the below properties to match what Payload previously generated for you from labels. On Collections Use `graphQL.singularName`, `graphQL.pluralName` for GraphQL schema names. Use `typescript.interface` for typescript generation name. On Globals Use `graphQL.name` for GraphQL Schema name. Use `typescript.interface` for typescript generation name. On Blocks (within Block fields) Use `graphQL.singularName` for graphQL schema names.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
const BeforeLogin: React.FC<{i18n}> = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<h3>Welcome</h3>
|
||||
<h3>{t('general:welcome')}</h3>
|
||||
<p>
|
||||
This demo is a set up to configure Payload for the develop and testing of features. To see a product demo of a Payload project
|
||||
please visit:
|
||||
|
||||
@@ -51,6 +51,15 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
resources: {
|
||||
en: {
|
||||
general: {
|
||||
dashboard: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
@@ -59,13 +68,28 @@ export default buildConfig({
|
||||
},
|
||||
{
|
||||
slug,
|
||||
labels: {
|
||||
singular: {
|
||||
en: 'Post en',
|
||||
es: 'Post es',
|
||||
},
|
||||
plural: {
|
||||
en: 'Posts en',
|
||||
es: 'Posts es',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
description: { en: 'Description en', es: 'Description es' },
|
||||
listSearchableFields: ['title', 'description', 'number'],
|
||||
group: 'One',
|
||||
group: { en: 'One', es: 'Una' },
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: {
|
||||
en: 'Title en',
|
||||
es: 'Title es',
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
@@ -81,7 +105,7 @@ export default buildConfig({
|
||||
{
|
||||
slug: 'group-one-collection-ones',
|
||||
admin: {
|
||||
group: 'One',
|
||||
group: { en: 'One', es: 'Una' },
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -93,7 +117,7 @@ export default buildConfig({
|
||||
{
|
||||
slug: 'group-one-collection-twos',
|
||||
admin: {
|
||||
group: 'One',
|
||||
group: { en: 'One', es: 'Una' },
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -130,6 +154,10 @@ export default buildConfig({
|
||||
globals: [
|
||||
{
|
||||
slug: globalSlug,
|
||||
label: {
|
||||
en: 'Global en',
|
||||
es: 'Global es',
|
||||
},
|
||||
admin: {
|
||||
group: 'Group',
|
||||
},
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('admin', () => {
|
||||
await page.locator('#action-delete').click();
|
||||
await page.locator('#confirm-delete').click();
|
||||
|
||||
await expect(page.locator(`text=Post "${id}" successfully deleted.`)).toBeVisible();
|
||||
await expect(page.locator(`text=Post en "${id}" successfully deleted.`)).toBeVisible();
|
||||
expect(page.url()).toContain(url.list);
|
||||
});
|
||||
|
||||
@@ -173,6 +173,29 @@ describe('admin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n', () => {
|
||||
test('should allow changing language', async () => {
|
||||
await page.goto(url.account);
|
||||
|
||||
const field = page.locator('.account__language .react-select');
|
||||
|
||||
await field.click({ delay: 100 });
|
||||
const options = page.locator('.rs__option');
|
||||
await options.locator('text=Español').click();
|
||||
|
||||
await expect(page.locator('.step-nav')).toContainText('Tablero');
|
||||
|
||||
await field.click({ delay: 100 });
|
||||
await options.locator('text=English').click();
|
||||
await field.click({ delay: 100 });
|
||||
await expect(page.locator('.form-submit .btn')).toContainText('Save');
|
||||
});
|
||||
|
||||
test('should allow custom translation', async () => {
|
||||
await expect(page.locator('.step-nav')).toContainText('Home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list view', () => {
|
||||
const tableRowLocator = 'table >> tbody >> tr';
|
||||
|
||||
@@ -318,6 +341,55 @@ describe('admin', () => {
|
||||
expect(await page.locator('.row-2 .cell-id').innerText()).toEqual(secondId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n', () => {
|
||||
test('should display translated collections and globals config options', async () => {
|
||||
await page.goto(url.list);
|
||||
|
||||
// collection label
|
||||
await expect(page.locator('#nav-posts')).toContainText('Posts en');
|
||||
|
||||
// global label
|
||||
await expect(page.locator('#nav-global-global')).toContainText('Global en');
|
||||
|
||||
// view description
|
||||
await expect(page.locator('.view-description')).toContainText('Description en');
|
||||
});
|
||||
|
||||
test('should display translated field titles', async () => {
|
||||
await createPost();
|
||||
|
||||
// column controls
|
||||
await page.locator('.list-controls__toggle-columns').click();
|
||||
await expect(await page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
|
||||
|
||||
// filters
|
||||
await page.locator('.list-controls__toggle-where').click();
|
||||
await page.locator('.where-builder__add-first-filter').click();
|
||||
await page.locator('.condition__field .rs__control').click();
|
||||
const options = page.locator('.rs__option');
|
||||
await expect(await options.locator('text=Title en')).toHaveText('Title en');
|
||||
|
||||
// list columns
|
||||
await expect(await page.locator('#heading-title .sort-column__label')).toHaveText('Title en');
|
||||
await expect(await page.locator('.search-filter input')).toHaveAttribute('placeholder', /(Title en)/);
|
||||
});
|
||||
|
||||
test('should use fallback language on field titles', async () => {
|
||||
// change language German
|
||||
await page.goto(url.account);
|
||||
const field = page.locator('.account__language .react-select');
|
||||
await field.click({ delay: 100 });
|
||||
const languageSelect = page.locator('.rs__option');
|
||||
// text field does not have a 'de' label
|
||||
await languageSelect.locator('text=Deutsch').click();
|
||||
|
||||
await page.goto(url.list);
|
||||
await page.locator('.list-controls__toggle-columns').click();
|
||||
// expecting the label to fall back to english as default fallbackLng
|
||||
await expect(await page.locator('.column-selector__column >> text=Title en')).toHaveText('Title en');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -86,6 +86,13 @@ const MyConfig: Config = {
|
||||
res.json({ message: 'Hello, world!' });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${applicationEndpoint}/i18n`,
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: req.t('general:backToDashboard') });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${rootEndpoint}`,
|
||||
method: 'get',
|
||||
@@ -102,7 +109,7 @@ const MyConfig: Config = {
|
||||
express.json({ type: 'application/json' }),
|
||||
(req: PayloadRequest, res: Response): void => {
|
||||
res.json(req.body);
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -115,6 +122,6 @@ const MyConfig: Config = {
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default buildConfig(MyConfig);
|
||||
|
||||
@@ -54,6 +54,13 @@ describe('Endpoints', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(params).toMatchObject(data);
|
||||
});
|
||||
|
||||
it('should have i18n on req', async () => {
|
||||
const { status, data } = await client.endpoint(`/api/${applicationEndpoint}/i18n`, 'get');
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(data.message).toStrictEqual('Back to Dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Root', () => {
|
||||
|
||||
@@ -95,7 +95,7 @@ const ArrayFields: CollectionConfig = {
|
||||
admin: {
|
||||
description: 'Row labels rendered from a function.',
|
||||
components: {
|
||||
RowLabel: ({ data, fallback }) => data.title || fallback,
|
||||
RowLabel: ({ data }) => data.title,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,6 +29,11 @@ export const blocksFieldSeedData = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
blockName: 'I18n Block',
|
||||
blockType: 'i18n-text',
|
||||
text: 'first block',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const blocksField: Field = {
|
||||
@@ -154,6 +159,48 @@ const BlockFields: CollectionConfig = {
|
||||
name: 'localizedBlocks',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'i18nBlocks',
|
||||
label: {
|
||||
en: 'Block en',
|
||||
es: 'Block es',
|
||||
},
|
||||
labels: {
|
||||
singular: {
|
||||
en: 'Block en',
|
||||
es: 'Block es',
|
||||
},
|
||||
plural: {
|
||||
en: 'Blocks en',
|
||||
es: 'Blocks es',
|
||||
},
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
slug: 'text',
|
||||
graphQL: {
|
||||
singularName: 'I18nText',
|
||||
},
|
||||
labels: {
|
||||
singular: {
|
||||
en: 'Text en',
|
||||
es: 'Text es',
|
||||
},
|
||||
plural: {
|
||||
en: 'Texts en',
|
||||
es: 'Texts es',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const CollapsibleFields: CollectionConfig = {
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
// TODO: change group name, to not be a duplicate of the above collapsible
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
|
||||
34
test/fields/collections/Radio/index.ts
Normal file
34
test/fields/collections/Radio/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { CollectionConfig } from '../../../../src/collections/config/types';
|
||||
|
||||
const RadioFields: CollectionConfig = {
|
||||
slug: 'radio-fields',
|
||||
fields: [
|
||||
{
|
||||
name: 'radio',
|
||||
label: {
|
||||
en: 'Radio en', es: 'Radio es',
|
||||
},
|
||||
type: 'radio',
|
||||
options: [
|
||||
{
|
||||
label: { en: 'Value One', es: 'Value Uno' },
|
||||
value: 'one',
|
||||
},
|
||||
{
|
||||
label: 'Value Two',
|
||||
value: 'two',
|
||||
},
|
||||
{
|
||||
label: 'Value Three',
|
||||
value: 'three',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const radiosDoc = {
|
||||
radio: 'one',
|
||||
};
|
||||
|
||||
export default RadioFields;
|
||||
@@ -147,7 +147,7 @@ const TabsFields: CollectionConfig = {
|
||||
},
|
||||
{
|
||||
name: 'localizedTab',
|
||||
label: 'Localized Tab',
|
||||
label: { en: 'Localized Tab en', es: 'Localized Tab es' },
|
||||
localized: true,
|
||||
description: 'This tab is localized and requires a name',
|
||||
fields: [
|
||||
|
||||
@@ -18,6 +18,24 @@ const TextFields: CollectionConfig = {
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'i18nText',
|
||||
type: 'text',
|
||||
label: {
|
||||
en: 'Text en',
|
||||
es: 'Text es',
|
||||
},
|
||||
admin: {
|
||||
placeholder: {
|
||||
en: 'en placeholder',
|
||||
es: 'es placeholder',
|
||||
},
|
||||
description: {
|
||||
en: 'en description',
|
||||
es: 'es description',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'defaultFunction',
|
||||
type: 'text',
|
||||
|
||||
@@ -20,6 +20,7 @@ import IndexedFields from './collections/Indexed';
|
||||
import NumberFields, { numberDoc } from './collections/Number';
|
||||
import CodeFields, { codeDoc } from './collections/Code';
|
||||
import RelationshipFields from './collections/Relationship';
|
||||
import RadioFields, { radiosDoc } from './collections/Radio';
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
@@ -41,6 +42,7 @@ export default buildConfig({
|
||||
CollapsibleFields,
|
||||
ConditionalLogic,
|
||||
DateFields,
|
||||
RadioFields,
|
||||
GroupFields,
|
||||
IndexedFields,
|
||||
NumberFields,
|
||||
@@ -70,6 +72,7 @@ export default buildConfig({
|
||||
await payload.create({ collection: 'conditional-logic', data: conditionalLogicDoc });
|
||||
await payload.create({ collection: 'group-fields', data: groupDoc });
|
||||
await payload.create({ collection: 'select-fields', data: selectsDoc });
|
||||
await payload.create({ collection: 'radio-fields', data: radiosDoc });
|
||||
await payload.create({ collection: 'tabs-fields', data: tabsDoc });
|
||||
await payload.create({ collection: 'point-fields', data: pointDoc });
|
||||
await payload.create({ collection: 'date-fields', data: dateDoc });
|
||||
|
||||
@@ -28,13 +28,64 @@ describe('fields', () => {
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
let url: AdminUrlUtil;
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, 'text-fields');
|
||||
});
|
||||
|
||||
test('should display field in list view', async () => {
|
||||
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'text-fields');
|
||||
await page.goto(url.list);
|
||||
const textCell = page.locator('.row-1 .cell-text');
|
||||
await expect(textCell)
|
||||
.toHaveText(textDoc.text);
|
||||
});
|
||||
|
||||
test('should display i18n label in cells when missing field data', async () => {
|
||||
await page.goto(url.list);
|
||||
const textCell = page.locator('.row-1 .cell-i18nText');
|
||||
await expect(textCell)
|
||||
.toHaveText('<No Text en>');
|
||||
});
|
||||
|
||||
test('should show i18n label', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
await expect(page.locator('label[for="field-i18nText"]')).toHaveText('Text en');
|
||||
});
|
||||
|
||||
test('should show i18n placeholder', async () => {
|
||||
await page.goto(url.create);
|
||||
await expect(await page.locator('#field-i18nText')).toHaveAttribute('placeholder', 'en placeholder');
|
||||
});
|
||||
|
||||
test('should show i18n descriptions', async () => {
|
||||
await page.goto(url.create);
|
||||
const description = page.locator('.field-description-i18nText');
|
||||
await expect(description).toHaveText('en description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('radio', () => {
|
||||
let url: AdminUrlUtil;
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, 'radio-fields');
|
||||
});
|
||||
|
||||
test('should show i18n label in list', async () => {
|
||||
await page.goto(url.list);
|
||||
await expect(page.locator('.cell-radio')).toHaveText('Value One');
|
||||
});
|
||||
|
||||
test('should show i18n label while editing', async () => {
|
||||
await page.goto(url.create);
|
||||
await expect(page.locator('label[for="field-radio"]')).toHaveText('Radio en');
|
||||
});
|
||||
|
||||
test('should show i18n radio labels', async () => {
|
||||
await page.goto(url.create);
|
||||
await expect(await page.locator('label[for="field-radio-one"] .radio-input__label'))
|
||||
.toHaveText('Value One');
|
||||
});
|
||||
});
|
||||
|
||||
describe('point', () => {
|
||||
@@ -94,6 +145,28 @@ describe('fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('blocks', () => {
|
||||
let url: AdminUrlUtil;
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, 'block-fields');
|
||||
});
|
||||
|
||||
test('should use i18n block labels', async () => {
|
||||
await page.goto(url.create);
|
||||
await expect(page.locator('#field-i18nBlocks .blocks-field__header')).toContainText('Block en');
|
||||
|
||||
const addButton = page.locator('#field-i18nBlocks .btn__label');
|
||||
await expect(addButton).toContainText('Add Block en');
|
||||
await addButton.click();
|
||||
|
||||
const blockSelector = page.locator('#field-i18nBlocks .block-selector .block-selection').first();
|
||||
await expect(blockSelector).toContainText('Text en');
|
||||
await blockSelector.click();
|
||||
|
||||
await expect(page.locator('#i18nBlocks-row-0 .blocks-field__block-pill-text')).toContainText('Text en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields - array', () => {
|
||||
let url: AdminUrlUtil;
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -157,6 +157,24 @@ describe('Fields', () => {
|
||||
expect(definitions.collapsibleTextUnique).toEqual(1);
|
||||
expect(options.collapsibleTextUnique).toMatchObject({ unique: true });
|
||||
});
|
||||
|
||||
it('should throw validation error saving on unique fields', async () => {
|
||||
const data = {
|
||||
text: 'a',
|
||||
uniqueText: 'a',
|
||||
};
|
||||
await payload.create({
|
||||
collection: 'indexed-fields',
|
||||
data,
|
||||
});
|
||||
expect(async () => {
|
||||
const result = await payload.create({
|
||||
collection: 'indexed-fields',
|
||||
data,
|
||||
});
|
||||
return result.error;
|
||||
}).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('point', () => {
|
||||
|
||||
@@ -32,6 +32,14 @@ export interface ArrayField {
|
||||
text?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
rowLabelAsFunction: {
|
||||
title?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
rowLabelAsComponent: {
|
||||
title?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -170,6 +178,12 @@ export interface BlockField {
|
||||
blockType: 'tabs';
|
||||
}
|
||||
)[];
|
||||
i18nBlocks: {
|
||||
text?: string;
|
||||
id?: string;
|
||||
blockName?: string;
|
||||
blockType: 'i18n-text';
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -201,6 +215,13 @@ export interface CollapsibleField {
|
||||
};
|
||||
};
|
||||
someText?: string;
|
||||
functionTitleField?: string;
|
||||
componentTitleField?: string;
|
||||
nestedTitle?: string;
|
||||
arrayWithCollapsibles: {
|
||||
innerCollapsible?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -216,6 +237,30 @@ export interface ConditionalLogic {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "date-fields".
|
||||
*/
|
||||
export interface DateField {
|
||||
id: string;
|
||||
default: string;
|
||||
timeOnly?: string;
|
||||
dayOnly?: string;
|
||||
dayAndTime?: string;
|
||||
monthOnly?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "radio-fields".
|
||||
*/
|
||||
export interface RadioField {
|
||||
id: string;
|
||||
radio?: 'one' | 'two' | 'three';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "group-fields".
|
||||
@@ -240,6 +285,49 @@ export interface GroupField {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "indexed-fields".
|
||||
*/
|
||||
export interface IndexedField {
|
||||
id: string;
|
||||
text: string;
|
||||
uniqueText?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
point?: [number, number];
|
||||
group: {
|
||||
localizedUnique?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
point?: [number, number];
|
||||
};
|
||||
collapsibleLocalizedUnique?: string;
|
||||
collapsibleTextUnique?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "number-fields".
|
||||
*/
|
||||
export interface NumberField {
|
||||
id: string;
|
||||
number?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
positiveNumber?: number;
|
||||
negativeNumber?: number;
|
||||
decimalMin?: number;
|
||||
decimalMax?: number;
|
||||
defaultNumber?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "point-fields".
|
||||
@@ -266,6 +354,40 @@ export interface PointField {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relationship-fields".
|
||||
*/
|
||||
export interface RelationshipField {
|
||||
id: string;
|
||||
relationship:
|
||||
| {
|
||||
value: string | TextField;
|
||||
relationTo: 'text-fields';
|
||||
}
|
||||
| {
|
||||
value: string | ArrayField;
|
||||
relationTo: 'array-fields';
|
||||
};
|
||||
relationToSelf?: string | RelationshipField;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "text-fields".
|
||||
*/
|
||||
export interface TextField {
|
||||
id: string;
|
||||
text: string;
|
||||
localizedText?: string;
|
||||
i18nText?: string;
|
||||
defaultFunction?: string;
|
||||
defaultAsync?: string;
|
||||
overrideLength?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "rich-text-fields".
|
||||
@@ -276,6 +398,9 @@ export interface RichTextField {
|
||||
richText: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
richTextReadOnly?: {
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -286,6 +411,7 @@ export interface RichTextField {
|
||||
export interface SelectField {
|
||||
id: string;
|
||||
select?: 'one' | 'two' | 'three';
|
||||
selectReadOnly?: 'one' | 'two' | 'three';
|
||||
selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -379,36 +505,6 @@ export interface TabsField {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "text-fields".
|
||||
*/
|
||||
export interface TextField {
|
||||
id: string;
|
||||
text: string;
|
||||
localizedText?: string;
|
||||
defaultFunction?: string;
|
||||
defaultAsync?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "number-fields".
|
||||
*/
|
||||
export interface NumberField {
|
||||
id: string;
|
||||
number?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
positiveNumber?: number;
|
||||
negativeNumber?: number;
|
||||
decimalMin?: number;
|
||||
decimalMax?: number;
|
||||
defaultNumber?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "uploads".
|
||||
@@ -416,6 +512,7 @@ export interface NumberField {
|
||||
export interface Upload {
|
||||
id: string;
|
||||
text?: string;
|
||||
media?: string | Upload;
|
||||
url?: string;
|
||||
filename?: string;
|
||||
mimeType?: string;
|
||||
@@ -425,46 +522,6 @@ export interface Upload {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "indexed-fields".
|
||||
*/
|
||||
export interface IndexedField {
|
||||
id: string;
|
||||
text: string;
|
||||
uniqueText?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
point?: [number, number];
|
||||
group: {
|
||||
localizedUnique?: string;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
*/
|
||||
point?: [number, number];
|
||||
};
|
||||
collapsibleLocalizedUnique?: string;
|
||||
collapsibleTextUnique?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "date-fields".
|
||||
*/
|
||||
export interface DateField {
|
||||
id: string;
|
||||
default: string;
|
||||
timeOnly?: string;
|
||||
dayOnly?: string;
|
||||
dayAndTime?: string;
|
||||
monthOnly?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export class AdminUrlUtil {
|
||||
account: string;
|
||||
|
||||
admin: string;
|
||||
|
||||
list: string;
|
||||
@@ -6,6 +8,7 @@ export class AdminUrlUtil {
|
||||
create: string;
|
||||
|
||||
constructor(serverURL: string, slug: string) {
|
||||
this.account = `${serverURL}/admin/account`;
|
||||
this.admin = `${serverURL}/admin`;
|
||||
this.list = `${this.admin}/collections/${slug}`;
|
||||
this.create = `${this.list}/create`;
|
||||
|
||||
@@ -3,10 +3,10 @@ import { BeforeLoginHook, CollectionConfig } from '../../../../src/collections/c
|
||||
import { AuthenticationError } from '../../../../src/errors';
|
||||
import { devUser, regularUser } from '../../../credentials';
|
||||
|
||||
const beforeLoginHook: BeforeLoginHook = ({ user }) => {
|
||||
const beforeLoginHook: BeforeLoginHook = ({ user, req }) => {
|
||||
const isAdmin = user.roles.includes('admin') ? user : undefined;
|
||||
if (!isAdmin) {
|
||||
throw new AuthenticationError();
|
||||
throw new AuthenticationError(req.t);
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user