Merge branch 'develop' into feat/conditional-fields-visibility

This commit is contained in:
Bassel Kanso 2025-06-19 15:58:33 +03:00 committed by GitHub
commit 05f7d0b2bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 816 additions and 324 deletions

View File

@ -0,0 +1,63 @@
---
title: Guided Tour
---
This document explains how to create and use Guided Tours in the Strapi CMS.
## Creating tours
To create a tour use the `createTour` factory function.
```tsx
const tours = {
contentManager: createTour('contentManager', [
{
name: 'TheFeatureStepName',
content: () => (
<>
<div>This is the content for Step 1 of some feature</div>
</>
),
},
]),
} as const;
```
Tours for the CMS are defined in the `packages/core/admin/admin/src/components/UnstableGuidedTour/Tours.tsx` file.
The tours are then passed to the `UnstableGuidedTourContext` provider.
```tsx
import { tours } from '../UnstableGuidedTour/Tours';
import { UnstableGuidedTourContext } from '../UnstableGuidedTour/Context';
function App() {
return (
<UnstableGuidedTourContext tours={tours}>
<Outlet />
</UnstableGuidedTourContext>
);
}
```
The provider derives the tour state from the tours object to create an object where each tour's name points to its current step index.
Continuing our example from above the intial tour state would be:
```ts
{
contentManager: 0;
}
```
## Displaying tours in the CMS
The tours object is exported from strapi admin and can be accessed anywhere in the CMS. Wrapping an element will anchor the tour tooltip to that element.
```tsx
import { tours } from '../UnstableGuidedTour/Tours';
<tours.contentManager.TheFeatureStepName>
<div>A part of a feature I want to show off<div>
</tours.contentManager.TheFeatureStepName>
```

View File

@ -13,7 +13,7 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/icons": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/plugin-color-picker": "workspace:*",
"@strapi/plugin-documentation": "workspace:*",
"@strapi/plugin-graphql": "workspace:*",

View File

@ -27,8 +27,8 @@
"@strapi/strapi": "workspace:*"
},
"dependencies": {
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"eslint": "8.50.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",

View File

@ -13,6 +13,8 @@ import { TrackingProvider } from '../features/Tracking';
import { GuidedTourProvider } from './GuidedTour/Provider';
import { LanguageProvider } from './LanguageProvider';
import { Theme } from './Theme';
import { UnstableGuidedTourContext } from './UnstableGuidedTour/Context';
import { tours } from './UnstableGuidedTour/Tours';
import type { Store } from '../core/store/configure';
import type { StrapiApp } from '../StrapiApp';
@ -57,13 +59,15 @@ const Providers = ({ children, strapi, store }: ProvidersProps) => {
<NotificationsProvider>
<TrackingProvider>
<GuidedTourProvider>
<ConfigurationProvider
defaultAuthLogo={strapi.configurations.authLogo}
defaultMenuLogo={strapi.configurations.menuLogo}
showReleaseNotification={strapi.configurations.notifications.releases}
>
{children}
</ConfigurationProvider>
<UnstableGuidedTourContext tours={tours}>
<ConfigurationProvider
defaultAuthLogo={strapi.configurations.authLogo}
defaultMenuLogo={strapi.configurations.menuLogo}
showReleaseNotification={strapi.configurations.notifications.releases}
>
{children}
</ConfigurationProvider>
</UnstableGuidedTourContext>
</GuidedTourProvider>
</TrackingProvider>
</NotificationsProvider>

View File

@ -0,0 +1,66 @@
import * as React from 'react';
import { produce } from 'immer';
import { createContext } from '../Context';
import type { Tours } from './Tours';
/* -------------------------------------------------------------------------------------------------
* GuidedTourProvider
* -----------------------------------------------------------------------------------------------*/
// Infer valid tour names from the tours object
type ValidTourName = keyof Tours;
// Now use ValidTourName in all type definitions
type Action = {
type: 'next_step';
payload: ValidTourName;
};
type State = {
currentSteps: Record<ValidTourName, number>;
};
const [GuidedTourProviderImpl, unstableUseGuidedTour] = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
}>('GuidedTour');
function reducer(state: State, action: Action): State {
return produce(state, (draft) => {
if (action.type === 'next_step') {
draft.currentSteps[action.payload] += 1;
}
});
}
const UnstableGuidedTourContext = ({
children,
tours,
}: {
children: React.ReactNode;
tours: Tours;
}) => {
// Derive the tour steps from the tours object
const currentSteps = Object.keys(tours).reduce(
(acc, tourName) => {
acc[tourName as ValidTourName] = 0;
return acc;
},
{} as Record<ValidTourName, number>
);
const [state, dispatch] = React.useReducer(reducer, {
currentSteps,
});
return (
<GuidedTourProviderImpl state={state} dispatch={dispatch}>
{children}
</GuidedTourProviderImpl>
);
};
export type { Action, State, ValidTourName };
export { UnstableGuidedTourContext, unstableUseGuidedTour, reducer };

View File

@ -0,0 +1,78 @@
import type { State, Action } from './Context';
/* -------------------------------------------------------------------------------------------------
* Tours
* -----------------------------------------------------------------------------------------------*/
const tours = {
contentManager: createTour('contentManager', [
{
name: 'TEST',
content: () => (
<>
<div>This is TEST</div>
</>
),
},
]),
} as const;
type Tours = typeof tours;
/* -------------------------------------------------------------------------------------------------
* Tour factory
* -----------------------------------------------------------------------------------------------*/
type TourStep<P extends string> = {
name: P;
content: Content;
};
type Content = ({
state,
dispatch,
}: {
state: State;
dispatch: React.Dispatch<Action>;
}) => React.ReactNode;
function createTour<const T extends ReadonlyArray<TourStep<string>>>(tourName: string, steps: T) {
type Components = {
[K in T[number]['name']]: React.ComponentType<{ children: React.ReactNode }>;
};
const tour = steps.reduce((acc, step, index) => {
if (step.name in acc) {
throw Error(`The tour: ${tourName} with step: ${step.name} has already been registered`);
}
acc[step.name as keyof Components] = ({ children }: { children: React.ReactNode }) => (
<div>
<div>TODO: GuidedTourTooltip goes here and receives these props</div>
<div style={{ display: 'flex', gap: 2 }}>
<span>content:</span>
{step.content({ state: { currentSteps: { contentManager: 0 } }, dispatch: () => {} })}
</div>
<div style={{ display: 'flex', gap: 2 }}>
<span>children:</span>
{children}
</div>
<div style={{ display: 'flex', gap: 2 }}>
<span>tourName:</span>
{tourName}
</div>
<div style={{ display: 'flex', gap: 2 }}>
<span>step:</span>
{index}
</div>
</div>
);
return acc;
}, {} as Components);
return tour;
}
export type { Content, Tours };
export { tours };

View File

@ -0,0 +1,49 @@
import { type Action, reducer } from '../Context';
describe('GuidedTour | reducer', () => {
describe('next_step', () => {
it('should increment the step count for the specified tour', () => {
const initialState = {
currentSteps: {
contentManager: 0,
},
};
const action: Action = {
type: 'next_step',
payload: 'contentManager',
};
const expectedState = {
currentSteps: {
contentManager: 1,
},
};
expect(reducer(initialState, action)).toEqual(expectedState);
});
it('should preserve other tour states when advancing a specific tour', () => {
const initialState = {
currentSteps: {
contentManager: 1,
contentTypeBuilder: 2,
},
};
const action: Action = {
type: 'next_step',
payload: 'contentManager',
};
const expectedState = {
currentSteps: {
contentManager: 2,
contentTypeBuilder: 2,
},
};
expect(reducer(initialState, action)).toEqual(expectedState);
});
});
});

View File

@ -26,6 +26,7 @@ export * from './components/SubNav';
export * from './components/GradientBadge';
export { useGuidedTour } from './components/GuidedTour/Provider';
export { tours as unstable_tours } from './components/UnstableGuidedTour/Tours';
/**
* Features

View File

@ -84,8 +84,8 @@
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-toolbar": "1.0.4",
"@reduxjs/toolkit": "1.9.7",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/permissions": "5.16.0",
"@strapi/types": "5.16.0",
"@strapi/typescript-utils": "5.16.0",

View File

@ -6,6 +6,7 @@ import {
useForm,
useNotification,
useFocusInputField,
useRBAC,
} from '@strapi/admin/strapi-admin';
import {
Box,
@ -21,8 +22,10 @@ import {
Field,
FlexComponent,
BoxComponent,
Loader,
EmptyStateLayout,
} from '@strapi/design-system';
import { Cross, Drag, ArrowClockwise, Link as LinkIcon, Plus } from '@strapi/icons';
import { Cross, Drag, ArrowClockwise, Link as LinkIcon, Plus, WarningCircle } from '@strapi/icons';
import { generateNKeysBetween } from 'fractional-indexing';
import pipe from 'lodash/fp/pipe';
import { getEmptyImage } from 'react-dnd-html5-backend';
@ -33,6 +36,8 @@ import { styled } from 'styled-components';
import { RelationDragPreviewProps } from '../../../../../components/DragPreviews/RelationDragPreview';
import { COLLECTION_TYPES } from '../../../../../constants/collections';
import { ItemTypes } from '../../../../../constants/dragAndDrop';
import { PERMISSIONS } from '../../../../../constants/plugin';
import { DocumentRBAC, useDocumentRBAC } from '../../../../../features/DocumentRBAC';
import { useDebounce } from '../../../../../hooks/useDebounce';
import { useDocument } from '../../../../../hooks/useDocument';
import { type DocumentMeta, useDocumentContext } from '../../../../../hooks/useDocumentContext';
@ -54,6 +59,7 @@ import { DocumentStatus } from '../../DocumentStatus';
import { useComponent } from '../ComponentContext';
import { RelationModalRenderer, getCollectionType } from '../Relations/RelationModal';
import type { FindAvailable } from '../../../../../../../shared/contracts/relations';
import type { Schema } from '@strapi/types';
/**
@ -484,7 +490,6 @@ const RelationsInput = ({
isRelatedToCurrentDocument,
...props
}: RelationsInputProps) => {
const [textValue, setTextValue] = React.useState<string | undefined>('');
const [searchParams, setSearchParams] = React.useState({
_q: '',
page: 1,
@ -492,7 +497,7 @@ const RelationsInput = ({
const { toggleNotification } = useNotification();
const { currentDocumentMeta } = useDocumentContext('RelationsInput');
const { formatMessage } = useIntl();
const fieldRef = useFocusInputField<HTMLInputElement>(name);
const field = useField<RelationsFormValue>(name);
const searchParamsDebounced = useDebounce(searchParams, 300);
@ -540,10 +545,6 @@ const RelationsInput = ({
currentDocumentMeta.params,
]);
const handleSearch = async (search: string) => {
setSearchParams((s) => ({ ...s, _q: search, page: 1 }));
};
const hasNextPage = data?.pagination ? data.pagination.page < data.pagination.pageCount : false;
const options = data?.results ?? [];
@ -582,18 +583,6 @@ const RelationsInput = ({
onChange(relation);
};
const handleLoadMore = () => {
if (!data || !data.pagination) {
return;
} else if (data.pagination.page < data.pagination.pageCount) {
setSearchParams((s) => ({ ...s, page: s.page + 1 }));
}
};
React.useLayoutEffect(() => {
setTextValue('');
}, [field.value]);
const relation = {
collectionType: COLLECTION_TYPES,
// @ts-expect-error targetModel does exist on the attribute. But it's not typed.
@ -602,25 +591,127 @@ const RelationsInput = ({
params: currentDocumentMeta.params,
} as DocumentMeta;
const { componentUID } = useComponent('RelationsField', ({ uid }) => ({
componentUID: uid,
}));
const {
permissions = [],
isLoading: isLoadingPermissions,
error,
} = useRBAC(
PERMISSIONS.map((action) => ({
action,
subject: relation.model,
}))
);
if (error) {
return (
<Flex alignItems="center" height="100%" justifyContent="center">
<EmptyStateLayout
icon={<WarningCircle width="16rem" />}
content={formatMessage({
id: 'anErrorOccurred',
defaultMessage: 'Whoops! Something went wrong. Please, try again.',
})}
/>
</Flex>
);
}
return (
<Field.Root error={field.error} hint={hint} name={name} required={required}>
<Field.Label action={labelAction}>{label}</Field.Label>
<RelationModalRenderer>
{({ dispatch }) => (
<Combobox
ref={fieldRef}
creatable="visible"
createMessage={() =>
formatMessage({
id: getTranslation('relation.create'),
defaultMessage: 'Create a relation',
})
}
onCreateOption={() => {
<DocumentRBAC permissions={permissions} model={relation.model}>
<RelationModalWithContext
relation={relation}
name={name}
placeholder={placeholder}
hasNextPage={hasNextPage}
isLoadingPermissions={isLoadingPermissions}
isLoadingSearchRelations={isLoading}
handleChange={handleChange}
setSearchParams={setSearchParams}
data={data}
mainField={mainField}
fieldValue={field.value}
{...props}
/>
</DocumentRBAC>
<Field.Error />
<Field.Hint />
</Field.Root>
);
};
interface RelationModalWithContextProps
extends Omit<RelationsInputProps, 'onChange' | 'label' | 'model' | 'isRelatedToCurrentDocument'> {
relation: DocumentMeta;
hasNextPage: boolean;
isLoadingSearchRelations: boolean;
isLoadingPermissions: boolean;
handleChange: (relationId?: string) => void;
data?: FindAvailable.Response;
fieldValue?: RelationsFormValue;
setSearchParams: React.Dispatch<
React.SetStateAction<{
_q: string;
page: number;
}>
>;
}
const RelationModalWithContext = ({
relation,
name,
placeholder,
hasNextPage,
isLoadingSearchRelations,
isLoadingPermissions,
handleChange,
mainField,
setSearchParams,
fieldValue,
data,
...props
}: RelationModalWithContextProps) => {
const [textValue, setTextValue] = React.useState<string | undefined>('');
const { formatMessage } = useIntl();
const canCreate = useDocumentRBAC('RelationModalWrapper', (state) => state.canCreate);
const fieldRef = useFocusInputField<HTMLInputElement>(name);
const { componentUID } = useComponent('RelationsField', ({ uid }) => ({
componentUID: uid,
}));
const handleLoadMore = () => {
if (!data || !data.pagination) {
return;
} else if (data.pagination.page < data.pagination.pageCount) {
setSearchParams((s) => ({ ...s, page: s.page + 1 }));
}
};
const options = data?.results ?? [];
React.useLayoutEffect(() => {
setTextValue('');
}, [fieldValue]);
const handleSearch = async (search: string) => {
setSearchParams((s) => ({ ...s, _q: search, page: 1 }));
};
return (
<RelationModalRenderer>
{({ dispatch }) => (
<Combobox
ref={fieldRef}
creatable="visible"
creatableDisabled={!canCreate}
createMessage={() =>
formatMessage({
id: getTranslation('relation.create'),
defaultMessage: 'Create a relation',
})
}
onCreateOption={() => {
if (canCreate) {
dispatch({
type: 'GO_TO_RELATION',
payload: {
@ -630,64 +721,62 @@ const RelationsInput = ({
fieldToConnectUID: componentUID,
},
});
}}
creatableStartIcon={<Plus fill="neutral500" />}
name={name}
autocomplete="list"
placeholder={
placeholder ||
formatMessage({
id: getTranslation('relation.add'),
defaultMessage: 'Add relation',
})
}
hasMoreItems={hasNextPage}
loading={isLoading}
onOpenChange={() => {
handleSearch(textValue ?? '');
}}
noOptionsMessage={() =>
formatMessage({
id: getTranslation('relation.notAvailable'),
defaultMessage: 'No relations available',
})
}
loadingMessage={formatMessage({
id: getTranslation('relation.isLoading'),
defaultMessage: 'Relations are loading',
})}
onLoadMore={handleLoadMore}
textValue={textValue}
onChange={handleChange}
onTextValueChange={(text) => {
setTextValue(text);
}}
onInputChange={(event) => {
handleSearch(event.currentTarget.value);
}}
{...props}
>
{options.map((opt) => {
const textValue = getRelationLabel(opt, mainField);
}}
creatableStartIcon={<Plus fill="neutral500" />}
name={name}
autocomplete="list"
placeholder={
placeholder ||
formatMessage({
id: getTranslation('relation.add'),
defaultMessage: 'Add relation',
})
}
hasMoreItems={hasNextPage}
loading={isLoadingSearchRelations || isLoadingPermissions}
onOpenChange={() => {
handleSearch(textValue ?? '');
}}
noOptionsMessage={() =>
formatMessage({
id: getTranslation('relation.notAvailable'),
defaultMessage: 'No relations available',
})
}
loadingMessage={formatMessage({
id: getTranslation('relation.isLoading'),
defaultMessage: 'Relations are loading',
})}
onLoadMore={handleLoadMore}
textValue={textValue}
onChange={handleChange}
onTextValueChange={(text) => {
setTextValue(text);
}}
onInputChange={(event) => {
handleSearch(event.currentTarget.value);
}}
{...props}
>
{options?.map((opt) => {
const textValue = getRelationLabel(opt, mainField);
return (
<ComboboxOption key={opt.id} value={opt.id.toString()} textValue={textValue}>
<Flex gap={2} justifyContent="space-between">
<Flex gap={2}>
<LinkIcon fill="neutral500" />
<Typography ellipsis>{textValue}</Typography>
</Flex>
{opt.status ? <DocumentStatus status={opt.status} /> : null}
return (
<ComboboxOption key={opt.id} value={opt.id.toString()} textValue={textValue}>
<Flex gap={2} justifyContent="space-between">
<Flex gap={2}>
<LinkIcon fill="neutral500" />
<Typography ellipsis>{textValue}</Typography>
</Flex>
</ComboboxOption>
);
})}
</Combobox>
)}
</RelationModalRenderer>
<Field.Error />
<Field.Hint />
</Field.Root>
{opt.status ? <DocumentStatus status={opt.status} /> : null}
</Flex>
</ComboboxOption>
);
})}
</Combobox>
)}
</RelationModalRenderer>
);
};

View File

@ -1,12 +1,14 @@
import { RenderOptions, fireEvent, render as renderRTL, screen } from '@tests/utils';
import { RenderOptions, fireEvent, render as renderRTL, screen, waitFor } from '@tests/utils';
import { Route, Routes } from 'react-router-dom';
import { RelationsInput, RelationsFieldProps } from '../Relations';
const render = ({
initialEntries,
...props
}: Partial<RelationsFieldProps> & Pick<RenderOptions, 'initialEntries'> = {}) =>
const render = (
{
initialEntries,
...props
}: Partial<RelationsFieldProps> & Pick<RenderOptions, 'initialEntries'> = { initialEntries: [] }
) =>
renderRTL(
<RelationsInput
attribute={{
@ -55,13 +57,26 @@ describe('Relations', () => {
});
it('should render the relations list when there is data from the API', async () => {
render();
render({
initialEntries: ['/content-manager/collection-types/api::address.address/12345'],
});
expect(screen.getByLabelText('relations')).toBe(screen.getByRole('combobox'));
expect(await screen.findAllByRole('listitem')).toHaveLength(3);
// Wait for the loading state to finish
await waitFor(() => {
expect(screen.queryByText('Relations are loading')).not.toBeInTheDocument();
});
expect(screen.getByLabelText('relations (3)')).toBe(screen.getByRole('combobox'));
// Wait for the combobox to be rendered with the correct label
await screen.findByLabelText(/relations/);
// Wait for the list items to be rendered
const listItems = await screen.findAllByRole('listitem');
expect(listItems).toHaveLength(3);
// Wait for the combobox to be updated with the count
await screen.findByLabelText(/relations \(3\)/);
// Check for the relation buttons
expect(screen.getByRole('button', { name: 'Relation entity 1' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Relation entity 2' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Relation entity 3' })).toBeInTheDocument();
@ -70,7 +85,9 @@ describe('Relations', () => {
it('should be disabled when the prop is passed', async () => {
render({ disabled: true });
expect(await screen.findAllByRole('listitem')).toHaveLength(3);
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
expect(screen.getByRole('combobox')).toBeDisabled();
});
@ -78,7 +95,20 @@ describe('Relations', () => {
it('should render a hint when the prop is passed', async () => {
render({ hint: 'This is a hint' });
expect(await screen.findAllByRole('listitem')).toHaveLength(3);
// Wait for the combobox to be rendered with the correct label
await waitFor(() => {
expect(screen.getByLabelText(/relations/)).toBeInTheDocument();
});
// Wait for the loading state to finish
await waitFor(() => {
expect(screen.queryByText('Relations are loading')).not.toBeInTheDocument();
});
// Wait for the list items to be rendered
await waitFor(() => {
expect(screen.getAllByRole('listitem')).toHaveLength(3);
});
expect(screen.getByText('This is a hint')).toBeInTheDocument();
});
@ -92,56 +122,84 @@ describe('Relations', () => {
it.todo('should disconnect a relation');
describe.skip('Accessibility', () => {
it('should have have description text', () => {
const { getByText } = render();
it('should have have description text', async () => {
render();
expect(getByText('Press spacebar to grab and re-order')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Press spacebar to grab and re-order')).toBeInTheDocument();
});
});
it('should update the live text when an item has been grabbed', async () => {
const { getByText, getAllByText } = render();
render();
const [draggedItem] = getAllByText('Drag');
await waitFor(() => {
expect(screen.getAllByText('Drag')).toHaveLength(3);
});
const [draggedItem] = screen.getAllByText('Drag');
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
expect(
getByText(/Press up and down arrow to change position, Spacebar to drop, Escape to cancel/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(
/Press up and down arrow to change position, Spacebar to drop, Escape to cancel/
)
).toBeInTheDocument();
});
});
it('should change the live text when an item has been moved', () => {
const { getByText, getAllByText } = render();
it('should change the live text when an item has been moved', async () => {
render();
const [draggedItem] = getAllByText('Drag');
await waitFor(() => {
expect(screen.getAllByText('Drag')).toHaveLength(3);
});
const [draggedItem] = screen.getAllByText('Drag');
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
expect(getByText(/New position in list/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/New position in list/)).toBeInTheDocument();
});
});
it('should change the live text when an item has been dropped', () => {
const { getByText, getAllByText } = render();
it('should change the live text when an item has been dropped', async () => {
render();
const [draggedItem] = getAllByText('Drag');
await waitFor(() => {
expect(screen.getAllByText('Drag')).toHaveLength(3);
});
const [draggedItem] = screen.getAllByText('Drag');
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
fireEvent.keyDown(draggedItem, { key: 'ArrowDown', code: 'ArrowDown' });
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
expect(getByText(/Final position in list/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Final position in list/)).toBeInTheDocument();
});
});
it('should change the live text after the reordering interaction has been cancelled', () => {
const { getAllByText, getByText } = render();
it('should change the live text after the reordering interaction has been cancelled', async () => {
render();
const [draggedItem] = getAllByText('Drag');
await waitFor(() => {
expect(screen.getAllByText('Drag')).toHaveLength(3);
});
const [draggedItem] = screen.getAllByText('Drag');
fireEvent.keyDown(draggedItem, { key: ' ', code: 'Space' });
fireEvent.keyDown(draggedItem, { key: 'Escape', code: 'Escape' });
expect(getByText(/Re-order cancelled/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Re-order cancelled/)).toBeInTheDocument();
});
});
});
});

View File

@ -67,8 +67,8 @@
"@radix-ui/react-toolbar": "1.0.4",
"@reduxjs/toolkit": "1.9.7",
"@sindresorhus/slugify": "1.1.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/types": "5.16.0",
"@strapi/utils": "5.16.0",
"codemirror5": "npm:codemirror@^5.65.11",

View File

@ -60,8 +60,8 @@
"dependencies": {
"@reduxjs/toolkit": "1.9.7",
"@strapi/database": "5.16.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/types": "5.16.0",
"@strapi/utils": "5.16.0",
"date-fns": "2.30.0",

View File

@ -66,9 +66,9 @@
"@dnd-kit/utilities": "3.2.2",
"@reduxjs/toolkit": "1.9.7",
"@sindresorhus/slugify": "1.1.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/generators": "5.16.0",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"date-fns": "2.30.0",
"fs-extra": "11.2.0",

View File

@ -56,8 +56,8 @@
"watch": "run -T rollup -c -w"
},
"dependencies": {
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/provider-email-sendmail": "5.16.0",
"@strapi/utils": "5.16.0",
"koa2-ratelimit": "^1.1.3",

View File

@ -57,8 +57,8 @@
},
"dependencies": {
"@reduxjs/toolkit": "1.9.7",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"fractional-indexing": "3.2.0",
"react-dnd": "16.0.1",

View File

@ -61,8 +61,8 @@
},
"dependencies": {
"@mux/mux-player-react": "3.1.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/provider-upload-local": "5.16.0",
"@strapi/utils": "5.16.0",
"byte-size": "8.1.1",

View File

@ -40,8 +40,8 @@
"watch": "run -T rollup -c -w"
},
"dependencies": {
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"react-intl": "6.6.2"
},
"devDependencies": {

View File

@ -57,8 +57,8 @@
"watch": "run -T rollup -c -w"
},
"dependencies": {
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"react-colorful": "5.6.1",
"react-intl": "6.6.2"
},

View File

@ -60,8 +60,8 @@
"dependencies": {
"@reduxjs/toolkit": "1.9.7",
"@strapi/admin": "5.16.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"bcryptjs": "2.4.3",
"cheerio": "^1.0.0",

View File

@ -57,8 +57,8 @@
"@graphql-tools/schema": "10.0.3",
"@graphql-tools/utils": "^10.1.3",
"@koa/cors": "5.0.0",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"graphql": "^16.8.1",
"graphql-depth-limit": "^1.1.0",

View File

@ -57,8 +57,8 @@
},
"dependencies": {
"@reduxjs/toolkit": "1.9.7",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"lodash": "4.17.21",
"qs": "6.11.1",

View File

@ -54,8 +54,8 @@
},
"dependencies": {
"@sentry/node": "7.112.2",
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26"
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27"
},
"devDependencies": {
"@strapi/strapi": "5.16.0",

View File

@ -48,8 +48,8 @@
"watch": "run -T rollup -c -w"
},
"dependencies": {
"@strapi/design-system": "2.0.0-rc.26",
"@strapi/icons": "2.0.0-rc.26",
"@strapi/design-system": "2.0.0-rc.27",
"@strapi/icons": "2.0.0-rc.27",
"@strapi/utils": "5.16.0",
"bcryptjs": "2.4.3",
"formik": "2.4.5",

View File

@ -47,3 +47,6 @@ export const ADMIN_PASSWORD = 'Testing123!';
export const EDITOR_EMAIL_ADDRESS = 'editor@testing.com';
export const EDITOR_PASSWORD = 'Testing123!';
export const AUTHOR_EMAIL_ADDRESS = 'author@testing.com';
export const AUTHOR_PASSWORD = 'Testing123!';

Binary file not shown.

View File

@ -322,9 +322,10 @@ test.describe('Edit View', () => {
.getByRole('listitem')
.filter({ has: page.getByRole('heading') })
.all();
expect(components).toHaveLength(2);
expect(components).toHaveLength(3);
expect(components[0]).toHaveText(/product carousel/i);
expect(components[1]).toHaveText(/content and image/i);
expect(components[2]).toHaveText(/product carousel/i);
// Add components at specific locations:
// - very last position
@ -346,12 +347,13 @@ test.describe('Edit View', () => {
.filter({ has: page.getByRole('heading') })
.allTextContents();
expect(componentTexts.length).toBe(5);
expect(componentTexts.length).toBe(6);
expect(componentTexts[0].toLowerCase()).toContain('hero image');
expect(componentTexts[1].toLowerCase()).toContain('product carousel');
expect(componentTexts[2].toLowerCase()).toContain('hero image');
expect(componentTexts[3].toLowerCase()).toContain('content and image');
expect(componentTexts[4].toLowerCase()).toContain('product carousel');
expect(componentTexts[5].toLowerCase()).toContain('product carousel');
});
});
});

View File

@ -0,0 +1,144 @@
import { test, expect } from '@playwright/test';
import { login } from '../../../utils/login';
import { resetDatabaseAndImportDataFromPath } from '../../../utils/dts-import';
import { clickAndWait } from '../../../utils/shared';
test.describe('Relations on the fly - Create a Relation and Save', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('with-admin.tar');
await page.goto('/admin');
});
test('I want to create a new relation, save the related document and check if the new relation is added to the parent document', async ({
page,
}) => {
// Step 0. Login as admin
await login({ page });
// Step 1. Got to Article collection-type and open one article
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
await clickAndWait(page, page.getByRole('gridcell', { name: 'West Ham post match analysis' }));
// Step 2. Open the relation modal
await page.getByRole('combobox', { name: 'authors' }).click();
await page.getByRole('option', { name: 'Create a relation' }).click();
// Step 3. Edit the form
await expect(page.getByRole('banner').getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
const name = page.getByRole('textbox', { name: 'name' });
await expect(name).toHaveValue('');
await name.fill('Mr. Plop');
await expect(name).toHaveValue('Mr. Plop');
// Step 4. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Mr. Plop');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 5. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Mr. Plop' })).toBeVisible({ timeout: 20000 });
});
test('I want to create a new relation, publish the related document and check if the new relation is added to the parent document', async ({
page,
}) => {
// Step 0. Login as admin
await login({ page });
// Step 1. Got to Article collection-type and open one article
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
await clickAndWait(page, page.getByRole('gridcell', { name: 'West Ham post match analysis' }));
// Step 2. Open the relation modal
await page.getByRole('combobox', { name: 'authors' }).click();
await page.getByRole('option', { name: 'Create a relation' }).click();
// Step 3. Edit the form
const name = page.getByRole('textbox', { name: 'name' });
await expect(name).toHaveValue('');
await name.fill('Mr. Fred Passo');
await expect(name).toHaveValue('Mr. Fred Passo');
// Step 4. Publish the related document
await clickAndWait(page, page.getByRole('button', { name: 'Publish' }));
await expect(name).toHaveValue('Mr. Fred Passo');
await expect(page.getByRole('status', { name: 'Published' }).first()).toBeVisible();
// Step 5. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Mr. Fred Passo' })).toBeVisible();
});
test('I want to create a relation inside a component, and save', async ({ page }) => {
// Step 0. Login as admin
await login({ page });
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
// Step 1. Got to Shop single-type
await clickAndWait(page, page.getByRole('link', { name: 'Shop' }));
// Step 2. Choose the product carousel component and open its toggle
await page.getByRole('button', { name: 'Product carousel', exact: true }).click();
// Step 3. Select a product
await page.getByRole('combobox', { name: 'products' }).click();
// Step 4. Open the relation modal
await page.getByRole('option', { name: 'Create a relation' }).click();
await expect(page.getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
// Change the name of the article
const name = page.getByRole('textbox', { name: 'name' });
await name.fill('Nike Zoom Kd Iv Gold C800');
// Step 5. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Nike Zoom Kd Iv Gold C800');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 6. Close the relation modal to see the updated relation on the root document
const closeButton = page.getByRole('button', { name: 'Close modal' });
await closeButton.click();
// Wait for the modal to be closed
await expect(page.getByText('Create a relation')).not.toBeVisible();
// Wait for the button to be visible with a more specific selector
const productButton = page.getByRole('button', { name: 'Nike Zoom Kd Iv Gold C800' });
// add timeout to wait for the button to be visible
await expect(productButton).toBeVisible({ timeout: 20000 });
});
test('I want to create a relation inside a new component, and save', async ({ page }) => {
// Step 0. Login as admin
await login({ page });
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
// Step 1. Got to Shop single-type
await clickAndWait(page, page.getByRole('link', { name: 'Shop' }));
// Step 2. Add a new component
await clickAndWait(page, page.getByRole('button', { name: 'Add a component to content' }));
// Step 3. Choose the new product carousel component and open its toggle
await clickAndWait(page, page.getByRole('button', { name: 'Product carousel' }).first());
// Step 4. Select a product
await page.getByRole('combobox', { name: 'products' }).click();
// Step 5. Open the relation modal
await page.getByRole('option', { name: 'Create a relation' }).click();
await expect(page.getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
// Change the name of the article
const name = page.getByRole('textbox', { name: 'name' });
await name.fill('Nike Zoom Kd Iv Gold C800');
// Step 6. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Nike Zoom Kd Iv Gold C800');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 7. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Nike Zoom Kd Iv Gold C800' })).toBeVisible({
timeout: 20000,
});
});
});

View File

@ -0,0 +1,28 @@
import { test, expect } from '@playwright/test';
import { login } from '../../../utils/login';
import { resetDatabaseAndImportDataFromPath } from '../../../utils/dts-import';
import { clickAndWait } from '../../../utils/shared';
import { AUTHOR_EMAIL_ADDRESS, AUTHOR_PASSWORD } from '../../../constants';
test.describe('Relations on the fly - Create a Relation', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('with-admin.tar');
await page.goto('/admin');
await login({ page, username: AUTHOR_EMAIL_ADDRESS, password: AUTHOR_PASSWORD });
});
test('I want to try to create a relation as an author without the permission to do it', async ({
page,
}) => {
// Step 1. Got to Article collection-type and open one article
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
await clickAndWait(page, page.getByRole('gridcell', { name: 'West Ham post match analysis' }));
// Step 2. Try to Open the create relation modal
await page.getByRole('combobox', { name: 'authors' }).click();
const createRelationButton = page.getByRole('option', { name: 'Create a relation' });
await expect(createRelationButton).toBeDisabled();
await expect(createRelationButton).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@ -9,67 +9,10 @@ test.describe('Relations on the fly - Create a Relation', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('with-admin.tar');
await page.goto('/admin');
// Step 0. Login as admin
await login({ page });
});
test('I want to create a new relation, save the related document and check if the new relation is added to the parent document', async ({
page,
}) => {
// Step 1. Got to Article collection-type and open one article
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
await clickAndWait(page, page.getByRole('gridcell', { name: 'West Ham post match analysis' }));
// Step 2. Open the relation modal
await page.getByRole('combobox', { name: 'authors' }).click();
await page.getByRole('option', { name: 'Create a relation' }).click();
// Step 3. Edit the form
await expect(page.getByRole('banner').getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
const name = page.getByRole('textbox', { name: 'name' });
await expect(name).toHaveValue('');
await name.fill('Mr. Plop');
await expect(name).toHaveValue('Mr. Plop');
// Step 4. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Mr. Plop');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 5. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Mr. Plop' })).toBeVisible();
});
test('I want to create a new relation, publish the related document and check if the new relation is added to the parent document', async ({
page,
}) => {
// Step 1. Got to Article collection-type and open one article
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
await clickAndWait(page, page.getByRole('gridcell', { name: 'West Ham post match analysis' }));
// Step 2. Open the relation modal
await page.getByRole('combobox', { name: 'authors' }).click();
await page.getByRole('option', { name: 'Create a relation' }).click();
// Step 3. Edit the form
const name = page.getByRole('textbox', { name: 'name' });
await expect(name).toHaveValue('');
await name.fill('Mr. Fred Passo');
await expect(name).toHaveValue('Mr. Fred Passo');
// Step 4. Publish the related document
await clickAndWait(page, page.getByRole('button', { name: 'Publish' }));
await expect(name).toHaveValue('Mr. Fred Passo');
await expect(page.getByRole('status', { name: 'Published' }).first()).toBeVisible();
// Step 5. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Mr. Fred Passo' })).toBeVisible();
});
test('I want to create a new relation in a modal and open it in full page', async ({ page }) => {
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
await clickAndWait(page, page.getByRole('link', { name: 'Article' }));
@ -220,62 +163,4 @@ test.describe('Relations on the fly - Create a Relation', () => {
await expect(page.getByRole('heading', { name: 'West Ham post match analysis' })).toBeVisible();
});
test('I want to create a relation inside a component, and save', async ({ page }) => {
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
// Step 1. Got to Shop single-type
await clickAndWait(page, page.getByRole('link', { name: 'Shop' }));
// Step 2. Choose the product carousel component and open its toggle
await page.getByRole('button', { name: 'Product carousel' }).click();
// Step 3. Select a product
await page.getByRole('combobox', { name: 'products' }).click();
// Step 4. Open the relation modal
await page.getByRole('option', { name: 'Create a relation' }).click();
await expect(page.getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
// Change the name of the article
const name = page.getByRole('textbox', { name: 'name' });
await name.fill('Nike Zoom Kd Iv Gold C800');
// Step 5. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Nike Zoom Kd Iv Gold C800');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 6. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Nike Zoom Kd Iv Gold C800' })).toBeVisible();
});
test('I want to create a relation inside a new component, and save', async ({ page }) => {
await clickAndWait(page, page.getByRole('link', { name: 'Content Manager' }));
// Step 1. Got to Shop single-type
await clickAndWait(page, page.getByRole('link', { name: 'Shop' }));
// Step 2. Add a new component
await clickAndWait(page, page.getByRole('button', { name: 'Add a component to content' }));
await clickAndWait(page, page.getByRole('button', { name: 'Product carousel', exact: true }));
// Step 3. Choose the new product carousel component and open its toggle
await page.getByRole('button', { name: 'Product carousel', exact: true }).click();
// Step 4. Select a product
await page.getByRole('combobox', { name: 'products' }).click();
// Step 5. Open the relation modal
await page.getByRole('option', { name: 'Create a relation' }).click();
await expect(page.getByText('Create a relation')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Untitled' })).toBeVisible();
// Change the name of the article
const name = page.getByRole('textbox', { name: 'name' });
await name.fill('Nike Zoom Kd Iv Gold C800');
// Step 6. Save the related document as draft
await clickAndWait(page, page.getByRole('button', { name: 'Save' }));
await expect(name).toHaveValue('Nike Zoom Kd Iv Gold C800');
await expect(page.getByRole('status', { name: 'Draft' }).first()).toBeVisible();
// Step 7. Close the relation modal to see the updated relation on the root document
await page.getByRole('button', { name: 'Close modal' }).click();
await expect(page.getByRole('button', { name: 'Nike Zoom Kd Iv Gold C800' })).toBeVisible();
});
});

View File

@ -211,7 +211,7 @@ test.describe('Relations on the fly - Edit a Relation', () => {
// Step 1. Got to Shop single-type
await clickAndWait(page, page.getByRole('link', { name: 'Shop' }));
// Step 2. Choose the product carousel component and open its toggle
await page.getByRole('button', { name: 'Product carousel' }).click();
await page.getByRole('button', { name: 'Product carousel', exact: true }).click();
// Step 3. Select a product
await page.getByRole('combobox', { name: 'products' }).click();
await page.getByRole('option', { name: 'Nike Mens 23/24 Away Stadium Jersey' }).click();

View File

@ -31,8 +31,12 @@ test.describe('List view', () => {
*/
await expect(page.getByRole('row', { name: 'Nike Mens 23/24 Away Stadium' })).toBeVisible();
await expect(page.getByRole('gridcell', { name: 'Available in' })).toBeVisible();
await expect(page.getByRole('gridcell', { name: 'English (en) (default)' })).toBeVisible();
await expect(page.getByRole('button', { name: 'English (en) (default)' })).toBeVisible();
await expect(
page.getByRole('gridcell', { name: 'English (en) (default)' }).first()
).toBeVisible();
await expect(
page.getByRole('button', { name: 'English (en) (default)' }).first()
).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Select a locale' })).toBeVisible();
await page.getByRole('combobox', { name: 'Select a locale' }).click();
for (const locale of LOCALES) {

View File

@ -212,7 +212,10 @@ test.describe('Settings', () => {
* Lets go back to the list view and assert that the changes are reflected.
*/
await navToHeader(page, ['Content Manager', 'Products'], 'Products');
expect(await page.getByRole('row').all()).toHaveLength(2);
/**
* It is 3 because it contains also the header row
*/
expect(await page.getByRole('row').all()).toHaveLength(3);
await expect(page.getByRole('combobox', { name: 'Select a locale' })).toHaveText('UK English');
await page.getByRole('combobox', { name: 'Select a locale' }).click();
for (const locale of ['UK English', ...LOCALES].filter((locale) => locale !== 'English (en)')) {

View File

@ -14,6 +14,21 @@ test.describe('RBAC - Delete Roles', () => {
skipTour: true,
});
// Navigate to the Users management page
await navToHeader(page, ['Settings', ['Administration Panel', 'Users']], 'Users');
// Locate the author user role
const authorUserRowLocator = page.getByRole('row', { name: 'author' });
// Delete the user
await authorUserRowLocator.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion in the alert dialog
await clickAndWait(
page,
page.getByRole('alertdialog').getByRole('button', { name: 'Confirm' })
);
// Navigate to the Roles management page
await navToHeader(page, ['Settings', ['Administration Panel', 'Roles']], 'Roles');
});

View File

@ -8649,8 +8649,8 @@ __metadata:
"@reduxjs/toolkit": "npm:1.9.7"
"@strapi/admin-test-utils": "npm:5.16.0"
"@strapi/data-transfer": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/permissions": "npm:5.16.0"
"@strapi/types": "npm:5.16.0"
"@strapi/typescript-utils": "npm:5.16.0"
@ -8787,8 +8787,8 @@ __metadata:
"@sindresorhus/slugify": "npm:1.1.0"
"@strapi/admin": "npm:5.16.0"
"@strapi/database": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/react": "npm:15.0.7"
@ -8851,8 +8851,8 @@ __metadata:
"@strapi/admin-test-utils": "npm:5.16.0"
"@strapi/content-manager": "npm:5.16.0"
"@strapi/database": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/dom": "npm:10.1.0"
@ -8897,9 +8897,9 @@ __metadata:
"@reduxjs/toolkit": "npm:1.9.7"
"@sindresorhus/slugify": "npm:1.1.0"
"@strapi/admin": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/generators": "npm:5.16.0"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/dom": "npm:10.1.0"
@ -9076,9 +9076,9 @@ __metadata:
languageName: unknown
linkType: soft
"@strapi/design-system@npm:2.0.0-rc.26":
version: 2.0.0-rc.26
resolution: "@strapi/design-system@npm:2.0.0-rc.26"
"@strapi/design-system@npm:2.0.0-rc.27":
version: 2.0.0-rc.27
resolution: "@strapi/design-system@npm:2.0.0-rc.27"
dependencies:
"@codemirror/lang-json": "npm:6.0.1"
"@floating-ui/react-dom": "npm:2.1.0"
@ -9101,7 +9101,7 @@ __metadata:
"@radix-ui/react-tabs": "npm:1.0.4"
"@radix-ui/react-tooltip": "npm:1.0.7"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
"@strapi/ui-primitives": "npm:2.0.0-rc.26"
"@strapi/ui-primitives": "npm:2.0.0-rc.27"
"@uiw/react-codemirror": "npm:4.22.2"
lodash: "npm:4.17.21"
react-remove-scroll: "npm:2.5.10"
@ -9110,7 +9110,7 @@ __metadata:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
styled-components: ^6.0.0
checksum: 10c0/70d46636fdf257ec56eeb41334240bc0511efe445f11cf444a8ff94354ef9a50104f4a5139174b216ebe3758b71aa2a88de6cd63d1834058c07b04ace9b9cc17
checksum: 10c0/3a29512c8f8c079767dd1e37986f423e7e113c39ce9e925cc33054a186f3945cca54b9276e90dc29d872d99c6ccb9375feb100fd96a405ce4b1028fc31aabb55
languageName: node
linkType: hard
@ -9119,8 +9119,8 @@ __metadata:
resolution: "@strapi/email@workspace:packages/core/email"
dependencies:
"@strapi/admin": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/provider-email-sendmail": "npm:5.16.0"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
@ -9207,8 +9207,8 @@ __metadata:
"@strapi/admin": "npm:5.16.0"
"@strapi/admin-test-utils": "npm:5.16.0"
"@strapi/content-manager": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/react": "npm:15.0.7"
@ -9234,14 +9234,14 @@ __metadata:
languageName: unknown
linkType: soft
"@strapi/icons@npm:2.0.0-rc.26":
version: 2.0.0-rc.26
resolution: "@strapi/icons@npm:2.0.0-rc.26"
"@strapi/icons@npm:2.0.0-rc.27":
version: 2.0.0-rc.27
resolution: "@strapi/icons@npm:2.0.0-rc.27"
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
styled-components: ^6.0.0
checksum: 10c0/ba1ebe0bbfb9bc41dc261c01d2c2417dd6986490284b3e049a947a9ba0be98407932fb380387b46f57aec38d5b10d4957ab748d1d042ea068bf4b2dc39317eaf
checksum: 10c0/74694ed706a1b56f1fcc2ed9680bc2bb2665a48038d08033ed4bd66e777617635f02c2940be9d58fbd2677f16b35d8f21131b53218b1d2d7e97ba9642eb0f5d8
languageName: node
linkType: hard
@ -9305,8 +9305,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@strapi/plugin-cloud@workspace:packages/plugins/cloud"
dependencies:
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
eslint-config-custom: "npm:5.16.0"
react: "npm:18.3.1"
@ -9329,8 +9329,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@strapi/plugin-color-picker@workspace:packages/plugins/color-picker"
dependencies:
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
"@testing-library/react": "npm:15.0.7"
"@testing-library/user-event": "npm:14.5.2"
@ -9358,8 +9358,8 @@ __metadata:
"@reduxjs/toolkit": "npm:1.9.7"
"@strapi/admin": "npm:5.16.0"
"@strapi/admin-test-utils": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
@ -9408,8 +9408,8 @@ __metadata:
"@graphql-tools/schema": "npm:10.0.3"
"@graphql-tools/utils": "npm:^10.1.3"
"@koa/cors": "npm:5.0.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
@ -9448,8 +9448,8 @@ __metadata:
resolution: "@strapi/plugin-sentry@workspace:packages/plugins/sentry"
dependencies:
"@sentry/node": "npm:7.112.2"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
@ -9468,8 +9468,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@strapi/plugin-users-permissions@workspace:packages/plugins/users-permissions"
dependencies:
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/strapi": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/dom": "npm:10.1.0"
@ -9612,8 +9612,8 @@ __metadata:
"@reduxjs/toolkit": "npm:1.9.7"
"@strapi/admin": "npm:5.16.0"
"@strapi/content-manager": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
"@testing-library/react": "npm:15.0.7"
@ -9811,9 +9811,9 @@ __metadata:
languageName: unknown
linkType: soft
"@strapi/ui-primitives@npm:2.0.0-rc.26":
version: 2.0.0-rc.26
resolution: "@strapi/ui-primitives@npm:2.0.0-rc.26"
"@strapi/ui-primitives@npm:2.0.0-rc.27":
version: 2.0.0-rc.27
resolution: "@strapi/ui-primitives@npm:2.0.0-rc.27"
dependencies:
"@radix-ui/number": "npm:1.0.1"
"@radix-ui/primitive": "npm:1.0.1"
@ -9838,7 +9838,7 @@ __metadata:
peerDependencies:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
checksum: 10c0/220a374f74533d81f5fb243781028fbcf9d0372c5e5c12dd5c236f9918fc88905d0482d3998f8b50626573d38c1ceda52c9ca1316540d39990e48615d08bc1ef
checksum: 10c0/8f287a647c80a6f95c5da2180ca04b37ba9801dd8df3a9ba04c6b275651ebf35e943043ed070b435c4305f9961451796e43287ee86b3cf232897230f4beab891
languageName: node
linkType: hard
@ -9876,8 +9876,8 @@ __metadata:
dependencies:
"@mux/mux-player-react": "npm:3.1.0"
"@strapi/admin": "npm:5.16.0"
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/provider-upload-local": "npm:5.16.0"
"@strapi/types": "npm:5.16.0"
"@strapi/utils": "npm:5.16.0"
@ -18898,7 +18898,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "getstarted@workspace:examples/getstarted"
dependencies:
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/plugin-color-picker": "workspace:*"
"@strapi/plugin-documentation": "workspace:*"
"@strapi/plugin-graphql": "workspace:*"
@ -29192,8 +29192,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "strapi-plugin-todo-example@workspace:examples/plugins/todo-example"
dependencies:
"@strapi/design-system": "npm:2.0.0-rc.26"
"@strapi/icons": "npm:2.0.0-rc.26"
"@strapi/design-system": "npm:2.0.0-rc.27"
"@strapi/icons": "npm:2.0.0-rc.27"
"@strapi/sdk-plugin": "npm:^5.2.0"
"@strapi/strapi": "workspace:*"
eslint: "npm:8.50.0"