mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
Merge branch 'develop' into feat/conditional-fields-visibility
This commit is contained in:
commit
05f7d0b2bd
63
docs/docs/guides/06-guided-tour.md
Normal file
63
docs/docs/guides/06-guided-tour.md
Normal 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>
|
||||
```
|
@ -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:*",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
@ -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 };
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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.
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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)')) {
|
||||
|
@ -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');
|
||||
});
|
||||
|
88
yarn.lock
88
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user