mirror of
https://github.com/strapi/strapi.git
synced 2025-07-24 17:40:18 +00:00
feat(content-releases): Edit Release (#18956)
* first draft implementation edit release * add dialog unit test * add permission to the edit button * add permissions type and remove old unit test * add createAction to the PermissionMap * fix type errors * fix unit test * fix lint error * fix review comments * change state naming * change dialog to modal
This commit is contained in:
parent
9b4c03b10b
commit
b4936e04a9
@ -7,65 +7,29 @@ import {
|
||||
TextInput,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { RELEASE_SCHEMA } from '../../../shared/validation-schemas';
|
||||
import { isAxiosError } from '../services/axios';
|
||||
import { useCreateReleaseMutation } from '../services/release';
|
||||
|
||||
interface FormValues {
|
||||
export interface FormValues {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
name: '',
|
||||
} satisfies FormValues;
|
||||
|
||||
interface AddReleaseDialogProps {
|
||||
interface ReleaseModalProps {
|
||||
handleClose: () => void;
|
||||
handleSubmit: (values: FormValues) => void;
|
||||
isLoading?: boolean;
|
||||
initialValues: FormValues;
|
||||
}
|
||||
|
||||
export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
|
||||
export const ReleaseModal = ({
|
||||
handleClose,
|
||||
handleSubmit,
|
||||
initialValues,
|
||||
isLoading = false,
|
||||
}: ReleaseModalProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const toggleNotification = useNotification();
|
||||
const { formatAPIError } = useAPIErrorHandler();
|
||||
const { push } = useHistory();
|
||||
|
||||
const [createRelease, { isLoading }] = useCreateReleaseMutation();
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
const response = await createRelease({
|
||||
name: values.name,
|
||||
});
|
||||
|
||||
if ('data' in response) {
|
||||
// When the response returns an object with 'data', handle success
|
||||
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 (isAxiosError(response.error)) {
|
||||
// When the response returns an object with 'error', handle axios error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(response.error),
|
||||
});
|
||||
} else {
|
||||
// Otherwise, the response returns an object with 'error', handle a generic error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={handleClose} labelledBy="title">
|
||||
@ -80,7 +44,7 @@ export const AddReleaseDialog = ({ handleClose }: AddReleaseDialogProps) => {
|
||||
<Formik
|
||||
validateOnChange={false}
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={INITIAL_VALUES}
|
||||
initialValues={initialValues}
|
||||
validationSchema={RELEASE_SCHEMA}
|
||||
>
|
||||
{({ values, errors, handleChange }) => (
|
@ -1,26 +0,0 @@
|
||||
import { within } from '@testing-library/react';
|
||||
import { render, screen } from '@tests/utils';
|
||||
|
||||
import { AddReleaseDialog } from '../AddReleaseDialog';
|
||||
|
||||
describe('AddReleaseDialog', () => {
|
||||
it('renders correctly the dialog content', async () => {
|
||||
const handleCloseMocked = jest.fn();
|
||||
const { user } = render(<AddReleaseDialog handleClose={handleCloseMocked} />);
|
||||
const dialogContainer = screen.getByRole('dialog');
|
||||
const dialogCancelButton = within(dialogContainer).getByRole('button', {
|
||||
name: /cancel/i,
|
||||
});
|
||||
expect(dialogCancelButton).toBeInTheDocument();
|
||||
await user.click(dialogCancelButton);
|
||||
expect(handleCloseMocked).toHaveBeenCalledTimes(1);
|
||||
|
||||
// enable the submit button when there is content inside the input
|
||||
const dialogContinueButton = within(dialogContainer).getByRole('button', {
|
||||
name: /continue/i,
|
||||
});
|
||||
const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i });
|
||||
await user.type(inputElement, 'new release');
|
||||
expect(dialogContinueButton).toBeEnabled();
|
||||
});
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import { within } from '@testing-library/react';
|
||||
import { render, screen } from '@tests/utils';
|
||||
|
||||
import { ReleaseModal } from '../ReleaseModal';
|
||||
|
||||
describe('ReleaseModal', () => {
|
||||
it('renders correctly the dialog content on create', async () => {
|
||||
const handleCloseMocked = jest.fn();
|
||||
const { user } = render(
|
||||
<ReleaseModal
|
||||
handleClose={handleCloseMocked}
|
||||
handleSubmit={jest.fn()}
|
||||
initialValues={{ name: '' }}
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
const dialogContainer = screen.getByRole('dialog');
|
||||
const dialogCancelButton = within(dialogContainer).getByRole('button', {
|
||||
name: /cancel/i,
|
||||
});
|
||||
expect(dialogCancelButton).toBeInTheDocument();
|
||||
await user.click(dialogCancelButton);
|
||||
expect(handleCloseMocked).toHaveBeenCalledTimes(1);
|
||||
|
||||
// the initial field value is empty
|
||||
const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i });
|
||||
expect(inputElement).toHaveValue('');
|
||||
|
||||
// enable the submit button when there is content inside the input
|
||||
const dialogContinueButton = within(dialogContainer).getByRole('button', {
|
||||
name: /continue/i,
|
||||
});
|
||||
await user.type(inputElement, 'new release');
|
||||
expect(dialogContinueButton).toBeEnabled();
|
||||
});
|
||||
it('renders correctly the dialog content on update', async () => {
|
||||
const handleCloseMocked = jest.fn();
|
||||
render(
|
||||
<ReleaseModal
|
||||
handleClose={handleCloseMocked}
|
||||
handleSubmit={jest.fn()}
|
||||
initialValues={{ name: 'title' }}
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
const dialogContainer = screen.getByRole('dialog');
|
||||
|
||||
// the initial field value is the title
|
||||
const inputElement = within(dialogContainer).getByRole('textbox', { name: /name/i });
|
||||
expect(inputElement).toHaveValue('title');
|
||||
|
||||
// enable the submit button when there is content inside the input
|
||||
const dialogContinueButton = within(dialogContainer).getByRole('button', {
|
||||
name: /continue/i,
|
||||
});
|
||||
expect(dialogContinueButton).toBeEnabled();
|
||||
});
|
||||
});
|
@ -1,20 +1,62 @@
|
||||
export const PERMISSIONS = {
|
||||
import { Permission } from '@strapi/helper-plugin';
|
||||
|
||||
interface PermissionMap {
|
||||
main: Permission[];
|
||||
create: Permission[];
|
||||
update: Permission[];
|
||||
delete: Permission[];
|
||||
createAction: Permission[];
|
||||
}
|
||||
|
||||
export const PERMISSIONS: PermissionMap = {
|
||||
main: [
|
||||
{
|
||||
id: 293,
|
||||
action: 'plugin::content-releases.read',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
actionParameters: [],
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
create: [
|
||||
{
|
||||
id: 294,
|
||||
action: 'plugin::content-releases.create',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
actionParameters: [],
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
id: 295,
|
||||
action: 'plugin::content-releases.update',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
actionParameters: [],
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
delete: [
|
||||
{
|
||||
id: 296,
|
||||
action: 'plugin::content-releases.delete',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
actionParameters: [],
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
createAction: [
|
||||
{
|
||||
id: 297,
|
||||
action: 'plugin::content-releases.create-action',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
actionParameters: [],
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -12,12 +12,16 @@ import {
|
||||
Popover,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { CheckPermissions } from '@strapi/helper-plugin';
|
||||
import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, EmptyDocuments, More, Pencil, Trash } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ReleaseModal, FormValues } from '../components/ReleaseModal';
|
||||
import { PERMISSIONS } from '../constants';
|
||||
import { isAxiosError } from '../services/axios';
|
||||
import { useUpdateReleaseMutation } from '../services/release';
|
||||
|
||||
const PopoverButton = styled(Flex)`
|
||||
align-self: stretch;
|
||||
@ -46,10 +50,14 @@ const ReleaseInfoWrapper = styled(Flex)`
|
||||
`;
|
||||
|
||||
const ReleaseDetailsPage = () => {
|
||||
const { releaseId } = useParams<{ releaseId: string }>();
|
||||
const [releaseModalShown, setReleaseModalShown] = React.useState(false);
|
||||
const [isPopoverVisible, setIsPopoverVisible] = React.useState(false);
|
||||
const moreButtonRef = React.useRef<HTMLButtonElement>(null!);
|
||||
const { formatMessage } = useIntl();
|
||||
// TODO: get the title from the API
|
||||
const toggleNotification = useNotification();
|
||||
const { formatAPIError } = useAPIErrorHandler();
|
||||
// TODO: get title from the API
|
||||
const title = 'Release title';
|
||||
|
||||
const totalEntries = 0; // TODO: replace it with the total number of entries
|
||||
@ -60,6 +68,47 @@ const ReleaseDetailsPage = () => {
|
||||
setIsPopoverVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
const toggleEditReleaseModal = () => {
|
||||
setReleaseModalShown((prev) => !prev);
|
||||
};
|
||||
|
||||
const openReleaseModal = () => {
|
||||
toggleEditReleaseModal();
|
||||
handleTogglePopover();
|
||||
};
|
||||
|
||||
const [updateRelease, { isLoading }] = useUpdateReleaseMutation();
|
||||
|
||||
const handleEditRelease = async (values: FormValues) => {
|
||||
const response = await updateRelease({
|
||||
id: releaseId,
|
||||
name: values.name,
|
||||
});
|
||||
if ('data' in response) {
|
||||
// When the response returns an object with 'data', handle success
|
||||
toggleNotification({
|
||||
type: 'success',
|
||||
message: formatMessage({
|
||||
id: 'content-releases.modal.release-updated-notification-success',
|
||||
defaultMessage: 'Release updated.',
|
||||
}),
|
||||
});
|
||||
} else if (isAxiosError(response.error)) {
|
||||
// When the response returns an object with 'error', handle axios error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(response.error),
|
||||
});
|
||||
} else {
|
||||
// Otherwise, the response returns an object with 'error', handle a generic error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
|
||||
});
|
||||
}
|
||||
toggleEditReleaseModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<HeaderLayout
|
||||
@ -100,6 +149,28 @@ const ReleaseDetailsPage = () => {
|
||||
minWidth="242px"
|
||||
>
|
||||
<Flex alignItems="center" justifyContent="center" direction="column" padding={1}>
|
||||
<CheckPermissions permissions={PERMISSIONS.update}>
|
||||
<PopoverButton
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
as="button"
|
||||
hasRadius
|
||||
onClick={openReleaseModal}
|
||||
>
|
||||
<PencilIcon />
|
||||
<Typography ellipsis>
|
||||
{formatMessage({
|
||||
id: 'content-releases.header.actions.edit',
|
||||
defaultMessage: 'Edit',
|
||||
})}
|
||||
</Typography>
|
||||
</PopoverButton>
|
||||
</CheckPermissions>
|
||||
|
||||
<PopoverButton
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
@ -108,25 +179,7 @@ const ReleaseDetailsPage = () => {
|
||||
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"
|
||||
hasRadius
|
||||
>
|
||||
<TrashIcon />
|
||||
<Typography ellipsis textColor="danger600">
|
||||
@ -187,6 +240,14 @@ const ReleaseDetailsPage = () => {
|
||||
icon={<EmptyDocuments width="10rem" />}
|
||||
/>
|
||||
</ContentLayout>
|
||||
{releaseModalShown && (
|
||||
<ReleaseModal
|
||||
handleClose={toggleEditReleaseModal}
|
||||
handleSubmit={handleEditRelease}
|
||||
isLoading={isLoading}
|
||||
initialValues={{ name: title }}
|
||||
/>
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
@ -25,15 +25,23 @@ import {
|
||||
PageSizeURLQuery,
|
||||
PaginationURLQuery,
|
||||
useQueryParams,
|
||||
useAPIErrorHandler,
|
||||
useNotification,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { EmptyDocuments, Plus } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { GetReleases } from '../../../shared/contracts/releases';
|
||||
import { AddReleaseDialog } from '../components/AddReleaseDialog';
|
||||
import { ReleaseModal, FormValues } from '../components/ReleaseModal';
|
||||
import { PERMISSIONS } from '../constants';
|
||||
import { useGetReleasesQuery, GetReleasesQueryParams } from '../services/release';
|
||||
import { isAxiosError } from '../services/axios';
|
||||
import {
|
||||
useGetReleasesQuery,
|
||||
GetReleasesQueryParams,
|
||||
useCreateReleaseMutation,
|
||||
} from '../services/release';
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* ReleasesLayout
|
||||
@ -164,21 +172,29 @@ const ReleasesGrid = ({ sectionTitle, releases = [], isError = false }: Releases
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* ReleasesPage
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
const INITIAL_FORM_VALUES = {
|
||||
name: '',
|
||||
} satisfies FormValues;
|
||||
|
||||
const ReleasesPage = () => {
|
||||
const [addReleaseDialogIsShown, setAddReleaseDialogIsShown] = React.useState(false);
|
||||
const [releaseModalShown, setReleaseModalShown] = React.useState(false);
|
||||
const toggleNotification = useNotification();
|
||||
const { formatMessage } = useIntl();
|
||||
const { push } = useHistory();
|
||||
const { formatAPIError } = useAPIErrorHandler();
|
||||
const [{ query }, setQuery] = useQueryParams<GetReleasesQueryParams>();
|
||||
const response = useGetReleasesQuery(query);
|
||||
const [createRelease, { isLoading: isSubmittingForm }] = useCreateReleaseMutation();
|
||||
|
||||
const { isLoading, isSuccess, isError } = response;
|
||||
|
||||
const toggleAddReleaseDialog = () => {
|
||||
setAddReleaseDialogIsShown((prev) => !prev);
|
||||
const toggleAddReleaseModal = () => {
|
||||
setReleaseModalShown((prev) => !prev);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ReleasesLayout onClickAddRelease={toggleAddReleaseDialog} isLoading>
|
||||
<ReleasesLayout onClickAddRelease={toggleAddReleaseModal} isLoading>
|
||||
<ContentLayout>
|
||||
<LoadingIndicatorPage />
|
||||
</ContentLayout>
|
||||
@ -203,8 +219,38 @@ const ReleasesPage = () => {
|
||||
|
||||
const activeTab = response?.currentData?.meta?.activeTab || 'pending';
|
||||
|
||||
const handleAddRelease = async (values: FormValues) => {
|
||||
const response = await createRelease({
|
||||
name: values.name,
|
||||
});
|
||||
if ('data' in response) {
|
||||
// When the response returns an object with 'data', handle success
|
||||
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 (isAxiosError(response.error)) {
|
||||
// When the response returns an object with 'error', handle axios error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(response.error),
|
||||
});
|
||||
} else {
|
||||
// Otherwise, the response returns an object with 'error', handle a generic error
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatMessage({ id: 'notification.error', defaultMessage: 'An error occurred' }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ReleasesLayout onClickAddRelease={toggleAddReleaseDialog} totalReleases={totalReleases}>
|
||||
<ReleasesLayout onClickAddRelease={toggleAddReleaseModal} totalReleases={totalReleases}>
|
||||
<ContentLayout>
|
||||
<>
|
||||
<TabGroup
|
||||
@ -266,7 +312,14 @@ const ReleasesPage = () => {
|
||||
)}
|
||||
</>
|
||||
</ContentLayout>
|
||||
{addReleaseDialogIsShown && <AddReleaseDialog handleClose={toggleAddReleaseDialog} />}
|
||||
{releaseModalShown && (
|
||||
<ReleaseModal
|
||||
handleClose={toggleAddReleaseModal}
|
||||
handleSubmit={handleAddRelease}
|
||||
isLoading={isSubmittingForm}
|
||||
initialValues={INITIAL_FORM_VALUES}
|
||||
/>
|
||||
)}
|
||||
</ReleasesLayout>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,36 @@
|
||||
import { render, screen } from '@tests/utils';
|
||||
import { render, screen, server } from '@tests/utils';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { ReleaseDetailsPage } from '../ReleaseDetailsPage';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn().mockImplementation(() => ({ id: '1' })),
|
||||
}));
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
// eslint-disable-next-line
|
||||
CheckPermissions: ({ children }: { children: JSX.Element}) => <div>{children}</div>
|
||||
}));
|
||||
|
||||
describe('Release details page', () => {
|
||||
it('renders correctly the heading content', async () => {
|
||||
server.use(
|
||||
rest.put('/content-releases/1', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
data: {
|
||||
id: 2,
|
||||
name: 'Release title focus',
|
||||
releasedAt: null,
|
||||
createdAt: '2023-11-30T16:02:40.908Z',
|
||||
updatedAt: '2023-12-01T11:12:04.441Z',
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
const { user } = render(<ReleaseDetailsPage />);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Release title');
|
||||
// if there are 0 entries
|
||||
|
@ -5,9 +5,9 @@ import { pluginId } from '../pluginId';
|
||||
|
||||
import { axiosBaseQuery } from './axios';
|
||||
|
||||
import type { CreateRelease, GetReleases } from '../../../shared/contracts/releases';
|
||||
import type { CreateRelease, GetReleases, UpdateRelease } from '../../../shared/contracts/releases';
|
||||
|
||||
interface GetReleasesQueryParams {
|
||||
export interface GetReleasesQueryParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filters?: {
|
||||
@ -104,6 +104,19 @@ const releaseApi = createApi({
|
||||
},
|
||||
invalidatesTags: ['Releases'],
|
||||
}),
|
||||
updateRelease: build.mutation<
|
||||
void,
|
||||
UpdateRelease.Request['params'] & UpdateRelease.Request['body']
|
||||
>({
|
||||
query({ id, ...data }) {
|
||||
return {
|
||||
url: `/content-releases/${id}`,
|
||||
method: 'PUT',
|
||||
data,
|
||||
};
|
||||
},
|
||||
invalidatesTags: ['Releases'],
|
||||
}),
|
||||
createReleaseAction: build.mutation<
|
||||
CreateReleaseAction.Response,
|
||||
CreateReleaseAction.Request
|
||||
@ -126,6 +139,7 @@ const {
|
||||
useGetReleasesForEntryQuery,
|
||||
useCreateReleaseMutation,
|
||||
useCreateReleaseActionMutation,
|
||||
useUpdateReleaseMutation,
|
||||
} = releaseApi;
|
||||
|
||||
export {
|
||||
@ -133,7 +147,6 @@ export {
|
||||
useGetReleasesForEntryQuery,
|
||||
useCreateReleaseMutation,
|
||||
useCreateReleaseActionMutation,
|
||||
useUpdateReleaseMutation,
|
||||
releaseApi,
|
||||
};
|
||||
|
||||
export type { GetReleasesQueryParams };
|
||||
|
@ -17,8 +17,9 @@
|
||||
"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}",
|
||||
"header.actions.created.description": "{number, plural, =0 {# days} one {# day} other {# days}} ago by {createdBy}",
|
||||
"modal.release-created-notification-success": "Release created",
|
||||
"modal.release-updated-notification-success": "Release updated",
|
||||
"modal.add-release-title": "New Release",
|
||||
"modal.form.input.label.release-name": "Name",
|
||||
"modal.form.button.submit": "Continue",
|
||||
|
Loading…
x
Reference in New Issue
Block a user