mirror of
https://github.com/strapi/strapi.git
synced 2025-09-09 16:47:06 +00:00
fix(content-releases): add edit entry to details view (#19161)
Co-authored-by: Mark Kaylor <mark.kaylor@strapi.io>
This commit is contained in:
parent
0e9bc9835c
commit
c9259c6680
@ -356,11 +356,12 @@ export const CMReleasesContainer = () => {
|
|||||||
<Typography fontSize={2} fontWeight="bold" variant="omega" textColor="neutral700">
|
<Typography fontSize={2} fontWeight="bold" variant="omega" textColor="neutral700">
|
||||||
{release.name}
|
{release.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<ReleaseActionMenu
|
<ReleaseActionMenu.Root hasTriggerBorder>
|
||||||
|
<ReleaseActionMenu.DeleteReleaseActionItem
|
||||||
releaseId={release.id}
|
releaseId={release.id}
|
||||||
actionId={release.action.id}
|
actionId={release.action.id}
|
||||||
hasTriggerBorder
|
|
||||||
/>
|
/>
|
||||||
|
</ReleaseActionMenu.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@ -1,55 +1,69 @@
|
|||||||
import { Flex, IconButton, Typography } from '@strapi/design-system';
|
import * as React from 'react';
|
||||||
import { Menu } from '@strapi/design-system/v2';
|
|
||||||
|
import { Flex, IconButton, Typography, Icon } from '@strapi/design-system';
|
||||||
|
import { Menu, Link } from '@strapi/design-system/v2';
|
||||||
import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
import { CheckPermissions, useAPIErrorHandler, useNotification } from '@strapi/helper-plugin';
|
||||||
import { Cross, More } from '@strapi/icons';
|
import { Cross, More, Pencil } from '@strapi/icons';
|
||||||
import { isAxiosError } from 'axios';
|
import { isAxiosError } from 'axios';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { DeleteReleaseAction } from '../../../shared/contracts/release-actions';
|
import { DeleteReleaseAction, ReleaseAction } from '../../../shared/contracts/release-actions';
|
||||||
import { PERMISSIONS } from '../constants';
|
import { PERMISSIONS } from '../constants';
|
||||||
import { useDeleteReleaseActionMutation } from '../services/release';
|
import { useDeleteReleaseActionMutation } from '../services/release';
|
||||||
|
|
||||||
const StyledMenuItem = styled(Menu.Item)`
|
import type { Store } from '@strapi/admin/strapi-admin';
|
||||||
|
import type { Permission } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
|
type RootState = ReturnType<Store['getState']>;
|
||||||
|
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|
||||||
|
const StyledMenuItem = styled(Menu.Item)<{ variant?: 'neutral' | 'danger' }>`
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${({ theme }) => theme.colors.danger100};
|
background: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}100`]};
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path {
|
||||||
|
fill: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}600`]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${({ theme }) => theme.colors.neutral800};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
path {
|
path {
|
||||||
fill: ${({ theme }) => theme.colors.danger600};
|
fill: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}600`]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
a {
|
||||||
svg {
|
color: ${({ theme }) => theme.colors.neutral800};
|
||||||
path {
|
|
||||||
fill: ${({ theme }) => theme.colors.danger600};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span,
|
||||||
|
a {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCross = styled(Cross)`
|
/* -------------------------------------------------------------------------------------------------
|
||||||
padding: ${({ theme }) => theme.spaces[1]};
|
* DeleteReleaseActionItemProps
|
||||||
`;
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
const StyledIconButton = styled(IconButton)`
|
const StyledIconButton = styled(IconButton)`
|
||||||
/* Setting this style inline with borderColor will not apply the style */
|
/* Setting this style inline with borderColor will not apply the style */
|
||||||
border: ${({ theme }) => `1px solid ${theme.colors.neutral200}`};
|
border: ${({ theme }) => `1px solid ${theme.colors.neutral200}`};
|
||||||
`;
|
`;
|
||||||
|
interface DeleteReleaseActionItemProps {
|
||||||
interface ReleaseActionMenuProps {
|
|
||||||
releaseId: DeleteReleaseAction.Request['params']['releaseId'];
|
releaseId: DeleteReleaseAction.Request['params']['releaseId'];
|
||||||
actionId: DeleteReleaseAction.Request['params']['actionId'];
|
actionId: DeleteReleaseAction.Request['params']['actionId'];
|
||||||
hasTriggerBorder?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReleaseActionMenu = ({
|
const DeleteReleaseActionItem = ({ releaseId, actionId }: DeleteReleaseActionItemProps) => {
|
||||||
releaseId,
|
|
||||||
actionId,
|
|
||||||
hasTriggerBorder = false,
|
|
||||||
}: ReleaseActionMenuProps) => {
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const { formatAPIError } = useAPIErrorHandler();
|
const { formatAPIError } = useAPIErrorHandler();
|
||||||
@ -90,6 +104,97 @@ export const ReleaseActionMenu = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckPermissions permissions={PERMISSIONS.deleteAction}>
|
||||||
|
<StyledMenuItem variant="danger" onSelect={handleDeleteAction}>
|
||||||
|
<Flex gap={2}>
|
||||||
|
<Icon as={Cross} padding={1} />
|
||||||
|
<Typography textColor="danger600" variant="omega">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.remove-from-release',
|
||||||
|
defaultMessage: 'Remove from release',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Flex>
|
||||||
|
</StyledMenuItem>
|
||||||
|
</CheckPermissions>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------------------------------
|
||||||
|
* ReleaseActionEntryLinkItem
|
||||||
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
|
interface ReleaseActionEntryLinkItemProps {
|
||||||
|
contentTypeUid: ReleaseAction['contentType'];
|
||||||
|
entryId: ReleaseAction['entry']['id'];
|
||||||
|
locale: ReleaseAction['locale'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReleaseActionEntryLinkItem = ({
|
||||||
|
contentTypeUid,
|
||||||
|
entryId,
|
||||||
|
locale,
|
||||||
|
}: ReleaseActionEntryLinkItemProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
// Confirm user has permissions to access the entry for the given locale
|
||||||
|
const collectionTypePermissions = useTypedSelector(
|
||||||
|
(state) => state.rbacProvider.collectionTypesRelatedPermissions
|
||||||
|
);
|
||||||
|
const updatePermissions = contentTypeUid
|
||||||
|
? collectionTypePermissions[contentTypeUid]?.['plugin::content-manager.explorer.update']
|
||||||
|
: [];
|
||||||
|
const canUpdateEntryForLocale = Boolean(
|
||||||
|
!locale ||
|
||||||
|
updatePermissions?.find((permission: Permission) =>
|
||||||
|
permission.properties?.locales?.includes(locale)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckPermissions
|
||||||
|
permissions={[
|
||||||
|
{
|
||||||
|
action: 'plugin::content-manager.explorer.update',
|
||||||
|
subject: contentTypeUid,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{canUpdateEntryForLocale && (
|
||||||
|
<StyledMenuItem>
|
||||||
|
<Link
|
||||||
|
as={NavLink}
|
||||||
|
// @ts-expect-error TODO: This component from DS is not using types from NavLink
|
||||||
|
to={{
|
||||||
|
pathname: `/content-manager/collection-types/${contentTypeUid}/${entryId}`,
|
||||||
|
search: locale && `?plugins[i18n][locale]=${locale}`,
|
||||||
|
}}
|
||||||
|
startIcon={<Icon as={Pencil} padding={1} />}
|
||||||
|
>
|
||||||
|
<Typography variant="omega">
|
||||||
|
{formatMessage({
|
||||||
|
id: 'content-releases.content-manager-edit-view.edit-entry',
|
||||||
|
defaultMessage: 'Edit entry',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
</StyledMenuItem>
|
||||||
|
)}
|
||||||
|
</CheckPermissions>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------------------------------
|
||||||
|
* Root
|
||||||
|
* -----------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
interface RootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
hasTriggerBorder?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Root = ({ children, hasTriggerBorder = false }: RootProps) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// A user can access the dropdown if they have permissions to delete a release-action OR update a release
|
// A user can access the dropdown if they have permissions to delete a release-action OR update a release
|
||||||
<CheckPermissions permissions={[...PERMISSIONS.deleteAction, ...PERMISSIONS.update]}>
|
<CheckPermissions permissions={[...PERMISSIONS.deleteAction, ...PERMISSIONS.update]}>
|
||||||
@ -115,21 +220,15 @@ export const ReleaseActionMenu = ({
|
|||||||
Refactor this once fixed in the DS
|
Refactor this once fixed in the DS
|
||||||
*/}
|
*/}
|
||||||
<Menu.Content top={1} popoverPlacement="bottom-end">
|
<Menu.Content top={1} popoverPlacement="bottom-end">
|
||||||
<CheckPermissions permissions={PERMISSIONS.deleteAction}>
|
{children}
|
||||||
<StyledMenuItem onSelect={handleDeleteAction}>
|
|
||||||
<Flex gap={2}>
|
|
||||||
<StyledCross />
|
|
||||||
<Typography textColor="danger600" variant="omega">
|
|
||||||
{formatMessage({
|
|
||||||
id: 'content-releases.content-manager-edit-view.remove-from-release',
|
|
||||||
defaultMessage: 'Remove from release',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</Flex>
|
|
||||||
</StyledMenuItem>
|
|
||||||
</CheckPermissions>
|
|
||||||
</Menu.Content>
|
</Menu.Content>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
</CheckPermissions>
|
</CheckPermissions>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReleaseActionMenu = {
|
||||||
|
Root,
|
||||||
|
DeleteReleaseActionItem,
|
||||||
|
ReleaseActionEntryLinkItem,
|
||||||
|
};
|
||||||
|
@ -10,12 +10,22 @@ jest.mock('@strapi/helper-plugin', () => ({
|
|||||||
|
|
||||||
describe('ReleaseActionMenu', () => {
|
describe('ReleaseActionMenu', () => {
|
||||||
it('should render the menu with its options', async () => {
|
it('should render the menu with its options', async () => {
|
||||||
const { user } = render(<ReleaseActionMenu releaseId="1" actionId="1" />);
|
const { user } = render(
|
||||||
|
<ReleaseActionMenu.Root>
|
||||||
|
<ReleaseActionMenu.DeleteReleaseActionItem releaseId="1" actionId="1" />
|
||||||
|
<ReleaseActionMenu.ReleaseActionEntryLinkItem
|
||||||
|
contentTypeUid="api::category.category"
|
||||||
|
locale="en"
|
||||||
|
entryId="1"
|
||||||
|
/>
|
||||||
|
</ReleaseActionMenu.Root>
|
||||||
|
);
|
||||||
|
|
||||||
const menuTrigger = screen.getByRole('button', { name: 'Release action options' });
|
const menuTrigger = screen.getByRole('button', { name: 'Release action options' });
|
||||||
expect(menuTrigger).toBeInTheDocument();
|
expect(menuTrigger).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(menuTrigger);
|
await user.click(menuTrigger);
|
||||||
expect(screen.getByRole('menuitem', { name: 'Remove from release' })).toBeInTheDocument();
|
expect(screen.getByRole('menuitem', { name: 'Remove from release' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('menuitem', { name: 'Edit entry' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -633,7 +633,7 @@ const ReleaseDetailsBody = () => {
|
|||||||
</Table.Head>
|
</Table.Head>
|
||||||
<Table.LoadingBody />
|
<Table.LoadingBody />
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{releaseActions[key].map(({ id, type, entry }) => (
|
{releaseActions[key].map(({ id, type, entry, contentType, locale }) => (
|
||||||
<Tr key={id}>
|
<Tr key={id}>
|
||||||
<Td width={'25%'}>
|
<Td width={'25%'}>
|
||||||
<Typography ellipsis>{`${
|
<Typography ellipsis>{`${
|
||||||
@ -680,7 +680,17 @@ const ReleaseDetailsBody = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Flex justifyContent="flex-end">
|
<Flex justifyContent="flex-end">
|
||||||
<ReleaseActionMenu releaseId={releaseId} actionId={id} />
|
<ReleaseActionMenu.Root>
|
||||||
|
<ReleaseActionMenu.ReleaseActionEntryLinkItem
|
||||||
|
contentTypeUid={contentType}
|
||||||
|
entryId={entry.id}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
<ReleaseActionMenu.DeleteReleaseActionItem
|
||||||
|
releaseId={release.id}
|
||||||
|
actionId={id}
|
||||||
|
/>
|
||||||
|
</ReleaseActionMenu.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Td>
|
</Td>
|
||||||
</>
|
</>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"content-manager-edit-view.add-to-release.redirect-button": "Open the list of releases",
|
"content-manager-edit-view.add-to-release.redirect-button": "Open the list of releases",
|
||||||
"content-manager-edit-view.list-releases.title": "{isPublish, select, true {Will be published in} other {Will be unpublished in}}",
|
"content-manager-edit-view.list-releases.title": "{isPublish, select, true {Will be published in} other {Will be unpublished in}}",
|
||||||
"content-manager-edit-view.remove-from-release": "Remove from release",
|
"content-manager-edit-view.remove-from-release": "Remove from release",
|
||||||
|
"content-releases.content-manager-edit-view.edit-entry": "Edit entry",
|
||||||
"content-manager-edit-view.remove-from-release.notification.success": "Entry removed from release",
|
"content-manager-edit-view.remove-from-release.notification.success": "Entry removed from release",
|
||||||
"content-manager-edit-view.release-action-menu": "Release action options",
|
"content-manager-edit-view.release-action-menu": "Release action options",
|
||||||
"content-manager.notification.entry-error": "Failed to get entry data",
|
"content-manager.notification.entry-error": "Failed to get entry data",
|
||||||
|
@ -17,6 +17,19 @@ const initialState = {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
collectionTypesRelatedPermissions: {
|
||||||
|
'api::category.category': {
|
||||||
|
'plugin::content-manager.explorer.update': [
|
||||||
|
{
|
||||||
|
action: 'plugin::content-manager.explorer.update',
|
||||||
|
subject: 'api::category.category',
|
||||||
|
properties: {
|
||||||
|
locales: ['en'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -261,12 +261,7 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
|
const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
|
||||||
contentTypeUids
|
contentTypeUids
|
||||||
);
|
);
|
||||||
const allLocales: Locale[] = await strapi.plugin('i18n').service('locales').find();
|
const allLocalesDictionary = await this.getLocalesDataForActions();
|
||||||
const allLocalesDictionary = allLocales.reduce<LocaleDictionary>((acc, locale) => {
|
|
||||||
acc[locale.code] = { name: locale.name, code: locale.code };
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const formattedData = actions.map((action: ReleaseAction) => {
|
const formattedData = actions.map((action: ReleaseAction) => {
|
||||||
const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
|
const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
|
||||||
@ -289,6 +284,19 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
|||||||
return _.groupBy(groupName)(formattedData);
|
return _.groupBy(groupName)(formattedData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getLocalesDataForActions() {
|
||||||
|
if (!strapi.plugin('i18n')) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLocales: Locale[] = (await strapi.plugin('i18n').service('locales').find()) || [];
|
||||||
|
return allLocales.reduce<LocaleDictionary>((acc, locale) => {
|
||||||
|
acc[locale.code] = { name: locale.name, code: locale.code };
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
|
||||||
async getContentTypesDataForActions(contentTypesUids: ReleaseAction['contentType'][]) {
|
async getContentTypesDataForActions(contentTypesUids: ReleaseAction['contentType'][]) {
|
||||||
const contentManagerContentTypeService = strapi
|
const contentManagerContentTypeService = strapi
|
||||||
.plugin('content-manager')
|
.plugin('content-manager')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user