mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 03:43:34 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									5ca75a1812
								
							
						
					
					
						commit
						f9fb2e7c49
					
				@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								packages/core/content-releases/admin/.eslintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								packages/core/content-releases/admin/.eslintrc
									
									
									
									
									
										Normal 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"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  extends: ['custom/front/typescript'],
 | 
			
		||||
};
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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[] }) {
 | 
			
		||||
 | 
			
		||||
@ -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 };
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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 };
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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}}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								packages/core/content-releases/admin/src/utils/data.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								packages/core/content-releases/admin/src/utils/data.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/core/content-releases/admin/src/utils/errors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/core/content-releases/admin/src/utils/errors.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
							
								
								
									
										4
									
								
								packages/core/content-releases/admin/tests/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								packages/core/content-releases/admin/tests/server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
// import { rest } from 'msw';
 | 
			
		||||
import { setupServer } from 'msw/node';
 | 
			
		||||
 | 
			
		||||
export const server = setupServer(...[]);
 | 
			
		||||
							
								
								
									
										13
									
								
								packages/core/content-releases/admin/tests/setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/core/content-releases/admin/tests/setup.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
import { server } from './server';
 | 
			
		||||
 | 
			
		||||
beforeAll(() => {
 | 
			
		||||
  server.listen();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
  server.resetHandlers();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
afterAll(() => {
 | 
			
		||||
  server.close();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/core/content-releases/admin/tests/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/core/content-releases/admin/tests/store.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
							
								
								
									
										117
									
								
								packages/core/content-releases/admin/tests/utils.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								packages/core/content-releases/admin/tests/utils.tsx
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
							
								
								
									
										9
									
								
								packages/core/content-releases/admin/tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/core/content-releases/admin/tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "./admin/tsconfig.json",
 | 
			
		||||
  "include": ["./admin/src", "./admin/custom.d.ts", "./shared"],
 | 
			
		||||
  "exclude": ["tests", "**/*.test.*"],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "rootDir": ".",
 | 
			
		||||
    "outDir": "./dist"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								packages/core/content-releases/shared/contracts/releases.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/core/content-releases/shared/contracts/releases.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "tsconfig/client.json",
 | 
			
		||||
  "include": [
 | 
			
		||||
    "./admin",
 | 
			
		||||
    "./server"
 | 
			
		||||
  ],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "declarationDir": "./dist",
 | 
			
		||||
    "outDir": "./dist"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user