feat(content-releases): Redirect to /content-releases/:releaseId onCreate and add header on details page (#18720)

* draft implementation details header

* implementation details ui with mock data

* fix unit tests

* fix fernando comments

* update pages structure

* first raw implementation store with rtk

* refactor(releases): redux toolkit query work

* rename releases page

* merge feature/content-releases

* test(releases): setup test harness for working with the admin app (#18817)

* test(releases): setup test harness for working with the admin app

* chore: remove file that shouldn't be here

* rename  releases page

* merge "content-releases/release-details-redirect-after-creation"

* test(releases): setup test harness for working with the admin app

* rename  releases page

* merge "content-releases/release-details-redirect-after-creation"

---------

Co-authored-by: Simone Taeggi <startae14@gmail.com>

* fix Fernando's review comments

---------

Co-authored-by: Josh <37798644+joshuaellis@users.noreply.github.com>
This commit is contained in:
Simone 2023-11-17 16:59:01 +01:00 committed by GitHub
parent 5ca75a1812
commit f9fb2e7c49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 689 additions and 71 deletions

View File

@ -64,6 +64,7 @@ const getAdminDependencyAliases = (monorepo?: StrapiMonorepo) =>
*/
const devAliases: Record<string, string> = {
'@strapi/admin/strapi-admin': './packages/core/admin/admin/src',
'@strapi/content-releases/strapi-admin': './packages/core/content-releases/admin/src',
'@strapi/content-type-builder/strapi-admin': './packages/core/content-type-builder/admin/src',
'@strapi/email/strapi-admin': './packages/core/email/admin/src',
'@strapi/upload/strapi-admin': './packages/core/upload/admin/src',

View File

@ -0,0 +1,18 @@
{
"root": true,
"extends": ["custom/front/typescript"],
"overrides": [
{
"files": ["./tests/*", "**/*.test.*"],
"env": {
"jest": true
},
"rules": {
/**
* So we can do `import { render } from '@tests/utils'`
*/
"import/no-unresolved": "off"
}
}
]
}

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ['custom/front/typescript'],
};

View File

@ -7,18 +7,26 @@ import {
TextInput,
Typography,
} from '@strapi/design-system';
import { useNotification } from '@strapi/helper-plugin';
import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
import { Formik, Form } from 'formik';
import { useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import * as yup from 'yup';
const releaseSchema = yup.object({
import { useCreateReleaseMutation } from '../modules/releaseSlice';
import { isErrorAxiosError } from '../utils/errors';
const RELEASE_SCHEMA = yup.object({
name: yup.string().required(),
});
interface FormValues {
name: string;
}
const INITIAL_VALUES = {
name: '',
};
} satisfies FormValues;
interface AddReleaseDialogProps {
handleClose: () => void;
@ -27,17 +35,36 @@ interface AddReleaseDialogProps {
export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
const { formatMessage } = useIntl();
const toggleNotification = useNotification();
const { push } = useHistory();
const { formatAPIError } = useAPIErrorHandler();
const handleSubmit = () => {
handleClose();
const [createRelease, { isLoading }] = useCreateReleaseMutation();
toggleNotification({
type: 'success',
message: formatMessage({
id: 'content-releases.modal.release-created-notification-success',
defaultMessage: 'Release created.',
}),
const handleSubmit = async (values: FormValues) => {
const response = await createRelease({
name: values.name,
});
if ('data' in response) {
toggleNotification({
type: 'success',
message: formatMessage({
id: 'content-releases.modal.release-created-notification-success',
defaultMessage: 'Release created.',
}),
});
push(`/plugins/content-releases/${response.data.data.id}`);
} else if (isErrorAxiosError(response.error)) {
toggleNotification({
type: 'warning',
message: formatAPIError(response.error),
});
} else {
toggleNotification({
type: 'warning',
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
});
}
};
return (
@ -54,7 +81,7 @@ export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
validateOnChange={false}
onSubmit={handleSubmit}
initialValues={INITIAL_VALUES}
validationSchema={releaseSchema}
validationSchema={RELEASE_SCHEMA}
>
{({ values, errors, handleChange }) => (
<Form>
@ -78,7 +105,7 @@ export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
</Button>
}
endActions={
<Button name="submit" disabled={!values.name} type="submit">
<Button name="submit" loading={isLoading} disabled={!values.name} type="submit">
{formatMessage({
id: 'content-releases.modal.form.button.submit',
defaultMessage: 'Continue',

View File

@ -2,6 +2,7 @@ import { prefixPluginTranslations } from '@strapi/helper-plugin';
import { PaperPlane } from '@strapi/icons';
import { PERMISSIONS } from './constants';
import { releaseApi } from './modules/releaseSlice';
import { pluginId } from './pluginId';
import type { Plugin } from '@strapi/types';
@ -19,11 +20,21 @@ const admin: Plugin.Config.AdminInput = {
defaultMessage: 'Releases',
},
async Component() {
const { Releases } = await import('./pages/App');
return Releases;
const { App } = await import('./pages/App');
return App;
},
permissions: PERMISSIONS.main,
});
/**
* For some reason every middleware you pass has to a function
* that returns the actual middleware. It's annoying but no one knows why....
*/
app.addMiddlewares([() => releaseApi.middleware]);
app.addReducers({
[releaseApi.reducerPath]: releaseApi.reducer,
});
}
},
async registerTrads({ locales }: { locales: string[] }) {

View File

@ -0,0 +1,39 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { pluginId } from '../pluginId';
import { axiosBaseQuery } from '../utils/data';
import type { CreateRelease, GetAllReleases } from '../../../shared/contracts/releases';
const releaseApi = createApi({
reducerPath: pluginId,
baseQuery: axiosBaseQuery,
tagTypes: ['Releases'],
endpoints: (build) => {
return {
getRelease: build.query<GetAllReleases.Response, undefined>({
query() {
return {
url: '/content-releases',
method: 'GET',
};
},
providesTags: ['Releases'],
}),
createRelease: build.mutation<CreateRelease.Response, CreateRelease.Request['body']>({
query(data) {
return {
url: '/content-releases',
method: 'POST',
data,
};
},
invalidatesTags: ['Releases'],
}),
};
},
});
const { useGetReleaseQuery, useCreateReleaseMutation } = releaseApi;
export { useGetReleaseQuery, useCreateReleaseMutation, releaseApi };

View File

@ -3,16 +3,18 @@ import { Route, Switch } from 'react-router-dom';
import { pluginId } from '../pluginId';
import { ProtectedReleaseDetailsPage } from './ReleaseDetailsPage';
import { ProtectedReleasesPage } from './ReleasesPage';
export const Releases = () => {
export const App = () => {
return (
<Main>
<Switch>
<Route exact path={`/plugins/${pluginId}`} component={ProtectedReleasesPage} />
<Route
exact
path={`/plugins/${pluginId}/:releaseId`}
render={() => <div>TODO: This is the DetailsPage</div>}
component={ProtectedReleaseDetailsPage}
/>
</Switch>
</Main>

View File

@ -0,0 +1,199 @@
import * as React from 'react';
import {
Button,
ContentLayout,
EmptyStateLayout,
Flex,
HeaderLayout,
IconButton,
Link,
Popover,
Typography,
} from '@strapi/design-system';
import { CheckPermissions } from '@strapi/helper-plugin';
import { ArrowLeft, EmptyDocuments, More, Pencil, Trash } from '@strapi/icons';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { PERMISSIONS } from '../constants';
const PopoverButton = styled(Flex)`
align-self: stretch;
`;
const PencilIcon = styled(Pencil)`
width: 16px;
height: 16px;
path {
fill: ${({ theme }) => theme.colors.neutral600};
}
`;
const TrashIcon = styled(Trash)`
width: 16px;
height: 16px;
path {
fill: ${({ theme }) => theme.colors.danger600};
}
`;
const ReleaseInfoWrapper = styled(Flex)`
align-self: stretch;
border-radius: 0 0 4px 4px;
border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
`;
const ReleaseDetailsPage = () => {
const [isPopoverVisible, setIsPopoverVisible] = React.useState(false);
const moreButtonRef = React.useRef<HTMLButtonElement>(null!);
const { formatMessage } = useIntl();
// TODO: get the title from the API
const title = 'Release title';
const totalEntries = 0; // TODO: replace it with the total number of entries
const days = 0; // TODO: replace it with the number of days since the release was created
const createdBy = 'John Doe'; // TODO: replace it with the name of the user who created the release
const handleTogglePopover = () => {
setIsPopoverVisible((prev) => !prev);
};
return (
<>
<HeaderLayout
title={title}
subtitle={formatMessage(
{
id: 'content-releases.pages.Details.header-subtitle',
defaultMessage: '{number, plural, =0 {No entries} one {# entry} other {# entries}}',
},
{ number: totalEntries }
)}
navigationAction={
<Link startIcon={<ArrowLeft />} to="/plugins/content-releases">
{formatMessage({
id: 'global.back',
defaultMessage: 'Back',
})}
</Link>
}
primaryAction={
<Flex gap={2}>
<IconButton
label={formatMessage({
id: 'content-releases.header.actions.open-release-actions',
defaultMessage: 'Release actions',
})}
onClick={handleTogglePopover}
ref={moreButtonRef}
>
<More />
</IconButton>
{isPopoverVisible && (
<Popover
source={moreButtonRef}
placement="bottom-end"
onDismiss={handleTogglePopover}
spacing={4}
minWidth="242px"
>
<Flex alignItems="center" justifyContent="center" direction="column" padding={1}>
<PopoverButton
paddingTop={2}
paddingBottom={2}
paddingLeft={4}
paddingRight={4}
alignItems="center"
gap={2}
as="button"
borderRadius="4px"
>
<PencilIcon />
<Typography ellipsis>
{formatMessage({
id: 'content-releases.header.actions.edit',
defaultMessage: 'Edit',
})}
</Typography>
</PopoverButton>
<PopoverButton
paddingTop={2}
paddingBottom={2}
paddingLeft={4}
paddingRight={4}
alignItems="center"
gap={2}
as="button"
borderRadius="4px"
>
<TrashIcon />
<Typography ellipsis textColor="danger600">
{formatMessage({
id: 'content-releases.header.actions.delete',
defaultMessage: 'Delete',
})}
</Typography>
</PopoverButton>
</Flex>
<ReleaseInfoWrapper
direction="column"
justifyContent="center"
alignItems="flex-start"
gap={1}
padding={5}
>
<Typography variant="pi" fontWeight="bold">
{formatMessage({
id: 'content-releases.header.actions.created',
defaultMessage: 'Created',
})}
</Typography>
<Typography variant="pi" color="neutral300">
{formatMessage(
{
id: 'content-releases.header.actions.created.description',
defaultMessage:
'{number, plural, =0 {# days} one {# day} other {# days}} ago by {createdBy}',
},
{ number: days, createdBy }
)}
</Typography>
</ReleaseInfoWrapper>
</Popover>
)}
<Button size="S" variant="tertiary">
{formatMessage({
id: 'content-releases.header.actions.refresh',
defaultMessage: 'Refresh',
})}
</Button>
<Button size="S" disabled={true} variant="default">
{formatMessage({
id: 'content-releases.header.actions.release',
defaultMessage: 'Release',
})}
</Button>
</Flex>
}
/>
<ContentLayout>
<EmptyStateLayout
content={formatMessage({
id: 'content-releases.pages.Details.empty-state.content',
defaultMessage: 'This release is empty.',
})}
icon={<EmptyDocuments width="10rem" />}
/>
</ContentLayout>
</>
);
};
const ProtectedReleaseDetailsPage = () => (
<CheckPermissions permissions={PERMISSIONS.main}>
<ReleaseDetailsPage />
</CheckPermissions>
);
export { ReleaseDetailsPage, ProtectedReleaseDetailsPage };

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import { Button, HeaderLayout } from '@strapi/design-system';
import { CheckPermissions, CheckPagePermissions } from '@strapi/helper-plugin';
import { CheckPermissions } from '@strapi/helper-plugin';
import { Plus } from '@strapi/icons';
import { useIntl } from 'react-intl';

View File

@ -0,0 +1,36 @@
import { render, screen } from '@tests/utils';
import { ReleaseDetailsPage } from '../ReleaseDetailsPage';
describe('Release details page', () => {
it('renders correctly the heading content', async () => {
const { user } = render(<ReleaseDetailsPage />);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Release title');
// if there are 0 entries
expect(screen.getByText('No entries')).toBeInTheDocument();
const refreshButton = screen.getByRole('button', { name: 'Refresh' });
expect(refreshButton).toBeInTheDocument();
const releaseButton = screen.getByRole('button', { name: 'Release' });
expect(releaseButton).toBeInTheDocument();
const moreButton = screen.getByRole('button', { name: 'Release actions' });
expect(moreButton).toBeInTheDocument();
await user.click(moreButton);
// shows the popover actions
const editButton = screen.getByRole('button', { name: 'Edit' });
expect(editButton).toBeInTheDocument();
const deleteButton = screen.getByRole('button', { name: 'Delete' });
expect(deleteButton).toBeInTheDocument();
});
it('shows empty content if there are no entries', async () => {
render(<ReleaseDetailsPage />);
expect(screen.getByText('No entries')).toBeInTheDocument();
});
});

View File

@ -1,36 +1,18 @@
import { lightTheme, ThemeProvider } from '@strapi/design-system';
import { render as renderRTL, within, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl';
import { within, screen } from '@testing-library/react';
import { render } from '@tests/utils';
import { ReleasesPage } from '../ReleasesPage';
const user = userEvent.setup();
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
// eslint-disable-next-line
CheckPermissions: ({ children }: { children: JSX.Element}) => <div>{children}</div>
}));
const render = () =>
renderRTL(
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}} defaultLocale="en">
<ReleasesPage />
</IntlProvider>
</ThemeProvider>
);
describe('Releases home page', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders correctly the heading content', async () => {
render();
() => expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Releases');
const { user } = render(<ReleasesPage />);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Releases');
// if there are 0 releases
expect(screen.getByText('No releases')).toBeInTheDocument();
@ -51,7 +33,7 @@ describe('Releases home page', () => {
});
it('hides the dialog', async () => {
render();
const { user } = render(<ReleasesPage />);
const newReleaseButton = screen.getByRole('button', { name: 'New release' });
await user.click(newReleaseButton);
@ -65,7 +47,7 @@ describe('Releases home page', () => {
});
it('enables the submit button when there is content in the input', async () => {
render();
const { user } = render(<ReleasesPage />);
const newReleaseButton = screen.getByRole('button', { name: 'New release' });
await user.click(newReleaseButton);

View File

@ -3,8 +3,16 @@
"pages.Releases.title": "Releases",
"pages.Releases.header-subtitle": "{number, plural, =0 {No releases} one {# release} other {# releases}}",
"header.actions.add-release": "New Release",
"header.actions.refresh": "Refresh",
"header.actions.release": "Release",
"header.actions.open-release-actions": "Release actions",
"header.actions.edit": "Edit",
"header.actions.delete": "Delete",
"header.actions.created": "Created",
"header.actions.created.description": "{number, plural, =0 {# days} one {# day} other {# days}} ago by {user}",
"modal.release-created-notification-success": "Release created",
"modal.add-release-title": "New Release",
"modal.form.input.label.release-name": "Name",
"modal.form.button.submit": "Continue"
"modal.form.button.submit": "Continue",
"pages.Details.header-subtitle": "{number, plural, =0 {No entries} one {# entry} other {# entries}}"
}

View File

@ -0,0 +1,61 @@
import { getFetchClient } from '@strapi/helper-plugin';
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
export interface QueryArguments<TSend> {
url: string;
method: 'PUT' | 'GET' | 'POST' | 'DELETE';
data?: TSend;
config?: AxiosRequestConfig<TSend>;
}
const axiosBaseQuery = async <TData = any, TSend = any>({
url,
method,
data,
config,
}: QueryArguments<TSend>) => {
try {
const { get, post, del, put } = getFetchClient();
if (method === 'POST') {
const res = await post<TData, AxiosResponse<TData>, TSend>(url, data, config);
return res;
}
if (method === 'DELETE') {
const res = await del<TData, AxiosResponse<TData>, TSend>(url, config);
return res;
}
if (method === 'PUT') {
const res = await put<TData, AxiosResponse<TData>, TSend>(url, data, config);
return res;
}
/**
* Default is GET.
*/
const res = await get<TData, AxiosResponse<TData>, TSend>(url, config);
return res;
} catch (error) {
const err = error as AxiosError;
/**
* This format mimics what we want from an AxiosError which is what the
* rest of the app works with, except this format is "serializable" since
* it goes into the redux store.
*
* NOTE passing the whole response will highlight this "serializability" issue.
*/
return {
error: {
status: err.response?.status,
code: err.code,
response: {
data: err.response?.data,
},
},
};
}
};
export { axiosBaseQuery };

View File

@ -0,0 +1,19 @@
import { AxiosError } from 'axios';
/**
* This asserts the errors from redux-toolkit-query are
* axios errors so we can pass them to our utility functions
* to correctly render error messages.
*/
const isErrorAxiosError = (err: unknown): err is AxiosError<{ error: any }> => {
return (
typeof err === 'object' &&
err !== null &&
'response' in err &&
typeof err.response === 'object' &&
err.response !== null &&
'data' in err.response
);
};
export { isErrorAxiosError };

View File

@ -0,0 +1,4 @@
// import { rest } from 'msw';
import { setupServer } from 'msw/node';
export const server = setupServer(...[]);

View File

@ -0,0 +1,13 @@
import { server } from './server';
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});

View File

@ -0,0 +1,23 @@
import { fixtures } from '@strapi/admin-test-utils';
/**
* This is for the redux store in `utils`.
* The more we adopt it, the bigger it will get which is okay.
*/
const initialState = {
admin_app: { permissions: fixtures.permissions.app },
rbacProvider: {
allPermissions: [
...fixtures.permissions.allPermissions,
{
id: 314,
action: 'admin::users.read',
subject: null,
properties: {},
conditions: [],
},
],
},
};
export { initialState };

View File

@ -0,0 +1,117 @@
/* eslint-disable check-file/filename-naming-convention */
import * as React from 'react';
import { configureStore } from '@reduxjs/toolkit';
import { fixtures } from '@strapi/admin-test-utils';
import { DesignSystemProvider } from '@strapi/design-system';
import { NotificationsProvider, Permission, RBACContext } from '@strapi/helper-plugin';
import {
renderHook as renderHookRTL,
render as renderRTL,
waitFor,
RenderOptions as RTLRenderOptions,
RenderResult,
act,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux';
import { MemoryRouter, MemoryRouterProps } from 'react-router-dom';
import { releaseApi } from '../src/modules/releaseSlice';
import { server } from './server';
import { initialState } from './store';
interface ProvidersProps {
children: React.ReactNode;
initialEntries?: MemoryRouterProps['initialEntries'];
}
const Providers = ({ children, initialEntries }: ProvidersProps) => {
const store = configureStore({
preloadedState: initialState,
reducer: {
[releaseApi.reducerPath]: releaseApi.reducer,
admin_app: (state = initialState) => state,
rbacProvider: (state = initialState) => state,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(releaseApi.middleware),
});
// en is the default locale of the admin app.
return (
<Provider store={store}>
<MemoryRouter initialEntries={initialEntries}>
<DesignSystemProvider locale="en">
<IntlProvider locale="en" messages={{}} textComponent="span">
<NotificationsProvider>
<RBACContext.Provider
value={{
refetchPermissions: jest.fn(),
allPermissions: [
...fixtures.permissions.allPermissions,
{
id: 314,
action: 'admin::users.read',
subject: null,
properties: {},
conditions: [],
actionParameters: {},
},
] as Permission[],
}}
>
{children}
</RBACContext.Provider>
</NotificationsProvider>
</IntlProvider>
</DesignSystemProvider>
</MemoryRouter>
</Provider>
);
};
// eslint-disable-next-line react/jsx-no-useless-fragment
const fallbackWrapper = ({ children }: { children: React.ReactNode }) => <>{children}</>;
export interface RenderOptions {
renderOptions?: RTLRenderOptions;
userEventOptions?: Parameters<typeof userEvent.setup>[0];
initialEntries?: MemoryRouterProps['initialEntries'];
}
const render = (
ui: React.ReactElement,
{ renderOptions, userEventOptions, initialEntries }: RenderOptions = {}
): RenderResult & { user: ReturnType<typeof userEvent.setup> } => {
const { wrapper: Wrapper = fallbackWrapper, ...restOptions } = renderOptions ?? {};
return {
...renderRTL(ui, {
wrapper: ({ children }) => (
<Providers initialEntries={initialEntries}>
<Wrapper>{children}</Wrapper>
</Providers>
),
...restOptions,
}),
user: userEvent.setup(userEventOptions),
};
};
const renderHook: typeof renderHookRTL = (hook, options) => {
const { wrapper: Wrapper = fallbackWrapper, ...restOptions } = options ?? {};
return renderHookRTL(hook, {
wrapper: ({ children }) => (
<Providers>
<Wrapper>{children}</Wrapper>
</Providers>
),
...restOptions,
});
};
export { render, renderHook, waitFor, act, screen, server };

View File

@ -0,0 +1,9 @@
{
"extends": "./admin/tsconfig.json",
"include": ["./admin/src", "./admin/custom.d.ts", "./shared"],
"exclude": ["tests", "**/*.test.*"],
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist"
}
}

View File

@ -1,10 +1,12 @@
{
"extends": "tsconfig/client.json",
"include": [
"src",
"custom.d.ts"
],
"compilerOptions": {
"baseUrl": ".",
"rootDir": "../",
}
"paths": {
"@tests/*": ["./tests/*"]
}
},
"include": ["src", "../shared", "tests", "custom.d.ts"],
"exclude": ["node_modules"]
}

View File

@ -3,4 +3,8 @@
module.exports = {
preset: '../../../jest-preset.front.js',
displayName: 'Core Content Releases',
moduleNameMapper: {
'^@tests/(.*)$': '<rootDir>/admin/tests/$1',
},
setupFilesAfterEnv: ['./admin/tests/setup.ts'],
};

View File

@ -40,21 +40,14 @@
"./dist",
"strapi-server.js"
],
"strapi": {
"name": "content-releases",
"description": "Organize and release content",
"kind": "plugin",
"displayName": "Releases",
"required": true
},
"scripts": {
"build": "pack-up build",
"clean": "run -T rimraf ./dist",
"lint": "run -T eslint .",
"prepublishOnly": "yarn clean && yarn build",
"test:front": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js",
"test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
"test:front:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js",
"test:front:watch": "run -T cross-env IS_EE=true jest --config ./jest.config.front.js --watchAll",
"test:front:watch:ce": "run -T cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll",
"test:ts:front": "run -T tsc -p admin/tsconfig.json",
"test:unit": "run -T jest",
@ -68,11 +61,14 @@
"@strapi/icons": "1.13.0",
"@strapi/types": "workspace:*",
"@strapi/utils": "4.15.4",
"axios": "1.6.0",
"formik": "2.4.0",
"react-intl": "6.4.1",
"react-redux": "8.1.1",
"yup": "0.32.9"
},
"devDependencies": {
"@strapi/admin-test-utils": "4.15.4",
"@strapi/pack-up": "workspace:*",
"@strapi/strapi": "4.15.4",
"@testing-library/react": "14.0.0",
@ -80,6 +76,7 @@
"@types/koa": "2.13.4",
"@types/styled-components": "5.1.26",
"koa": "2.13.4",
"msw": "1.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "5.3.4",
@ -101,5 +98,12 @@
"implicitDependencies": [
"!@strapi/strapi"
]
},
"strapi": {
"name": "content-releases",
"description": "Organize and release content",
"kind": "plugin",
"displayName": "Releases",
"required": true
}
}

View File

@ -7,6 +7,7 @@ export default defineConfig({
import: './dist/admin/index.mjs',
require: './dist/admin/index.js',
types: './dist/admin/src/index.d.ts',
tsconfig: './admin/tsconfig.build.json',
runtime: 'web',
},
{
@ -14,6 +15,7 @@ export default defineConfig({
import: './dist/server/index.mjs',
require: './dist/server/index.js',
types: './dist/server/src/index.d.ts',
tsconfig: './server/tsconfig.build.json',
runtime: 'node',
},
],

View File

@ -0,0 +1,48 @@
import { Entity as StrapiEntity } from '@strapi/types';
import { errors } from '@strapi/utils';
export interface Entity {
id: StrapiEntity.ID;
createdAt: string;
updatedAt: string;
}
export interface Release extends Entity {
name: string;
}
/**
* POST /content-releases - Create a single release
*/
export declare namespace CreateRelease {
export interface Request {
query: {};
body: Omit<Release, keyof Entity>;
}
export interface Response {
data: Release;
/**
* TODO: check if we also could recieve errors.YupValidationError
*/
error?: errors.ApplicationError | errors.YupValidationError | errors.UnauthorizedError;
}
}
/**
* GET /content-releases - Get all the release
*/
export declare namespace GetAllReleases {
export interface Request {
query: {};
body: {};
}
/**
* TODO: Validate this with BE
*/
export interface Response {
data: Release[];
error?: errors.ApplicationError;
}
}

View File

@ -1,11 +0,0 @@
{
"extends": "tsconfig/client.json",
"include": [
"./admin",
"./server"
],
"compilerOptions": {
"declarationDir": "./dist",
"outDir": "./dist"
}
}

View File

@ -8764,6 +8764,7 @@ __metadata:
resolution: "@strapi/content-releases@workspace:packages/core/content-releases"
dependencies:
"@reduxjs/toolkit": "npm:1.9.7"
"@strapi/admin-test-utils": "npm:4.15.4"
"@strapi/design-system": "npm:1.13.1"
"@strapi/helper-plugin": "npm:4.15.4"
"@strapi/icons": "npm:1.13.0"
@ -8775,11 +8776,14 @@ __metadata:
"@testing-library/user-event": "npm:14.4.3"
"@types/koa": "npm:2.13.4"
"@types/styled-components": "npm:5.1.26"
axios: "npm:1.6.0"
formik: "npm:2.4.0"
koa: "npm:2.13.4"
msw: "npm:1.3.0"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-intl: "npm:6.4.1"
react-redux: "npm:8.1.1"
react-router-dom: "npm:5.3.4"
styled-components: "npm:5.3.3"
typescript: "npm:5.2.2"