mirror of
https://github.com/strapi/strapi.git
synced 2025-09-07 23:57:19 +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">
|
||||
{release.name}
|
||||
</Typography>
|
||||
<ReleaseActionMenu
|
||||
releaseId={release.id}
|
||||
actionId={release.action.id}
|
||||
hasTriggerBorder
|
||||
/>
|
||||
<ReleaseActionMenu.Root hasTriggerBorder>
|
||||
<ReleaseActionMenu.DeleteReleaseActionItem
|
||||
releaseId={release.id}
|
||||
actionId={release.action.id}
|
||||
/>
|
||||
</ReleaseActionMenu.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,55 +1,69 @@
|
||||
import { Flex, IconButton, Typography } from '@strapi/design-system';
|
||||
import { Menu } from '@strapi/design-system/v2';
|
||||
import * as React from 'react';
|
||||
|
||||
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 { Cross, More } from '@strapi/icons';
|
||||
import { Cross, More, Pencil } from '@strapi/icons';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
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 { 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 {
|
||||
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 {
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.danger600};
|
||||
fill: ${({ theme, variant = 'neutral' }) => theme.colors[`${variant}600`]};
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.danger600};
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: ${({ theme }) => theme.colors.neutral800};
|
||||
}
|
||||
|
||||
span,
|
||||
a {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCross = styled(Cross)`
|
||||
padding: ${({ theme }) => theme.spaces[1]};
|
||||
`;
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* DeleteReleaseActionItemProps
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
const StyledIconButton = styled(IconButton)`
|
||||
/* Setting this style inline with borderColor will not apply the style */
|
||||
border: ${({ theme }) => `1px solid ${theme.colors.neutral200}`};
|
||||
`;
|
||||
|
||||
interface ReleaseActionMenuProps {
|
||||
interface DeleteReleaseActionItemProps {
|
||||
releaseId: DeleteReleaseAction.Request['params']['releaseId'];
|
||||
actionId: DeleteReleaseAction.Request['params']['actionId'];
|
||||
hasTriggerBorder?: boolean;
|
||||
}
|
||||
|
||||
export const ReleaseActionMenu = ({
|
||||
releaseId,
|
||||
actionId,
|
||||
hasTriggerBorder = false,
|
||||
}: ReleaseActionMenuProps) => {
|
||||
const DeleteReleaseActionItem = ({ releaseId, actionId }: DeleteReleaseActionItemProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const toggleNotification = useNotification();
|
||||
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 (
|
||||
// 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]}>
|
||||
@ -115,21 +220,15 @@ export const ReleaseActionMenu = ({
|
||||
Refactor this once fixed in the DS
|
||||
*/}
|
||||
<Menu.Content top={1} popoverPlacement="bottom-end">
|
||||
<CheckPermissions permissions={PERMISSIONS.deleteAction}>
|
||||
<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>
|
||||
{children}
|
||||
</Menu.Content>
|
||||
</Menu.Root>
|
||||
</CheckPermissions>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReleaseActionMenu = {
|
||||
Root,
|
||||
DeleteReleaseActionItem,
|
||||
ReleaseActionEntryLinkItem,
|
||||
};
|
||||
|
@ -10,12 +10,22 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
|
||||
describe('ReleaseActionMenu', () => {
|
||||
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' });
|
||||
expect(menuTrigger).toBeInTheDocument();
|
||||
|
||||
await user.click(menuTrigger);
|
||||
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.LoadingBody />
|
||||
<Table.Body>
|
||||
{releaseActions[key].map(({ id, type, entry }) => (
|
||||
{releaseActions[key].map(({ id, type, entry, contentType, locale }) => (
|
||||
<Tr key={id}>
|
||||
<Td width={'25%'}>
|
||||
<Typography ellipsis>{`${
|
||||
@ -680,7 +680,17 @@ const ReleaseDetailsBody = () => {
|
||||
</Td>
|
||||
<Td>
|
||||
<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>
|
||||
</Td>
|
||||
</>
|
||||
|
@ -10,6 +10,7 @@
|
||||
"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.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.release-action-menu": "Release action options",
|
||||
"content-manager.notification.entry-error": "Failed to get entry data",
|
||||
|
@ -17,6 +17,19 @@ const initialState = {
|
||||
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(
|
||||
contentTypeUids
|
||||
);
|
||||
const allLocales: Locale[] = await strapi.plugin('i18n').service('locales').find();
|
||||
const allLocalesDictionary = allLocales.reduce<LocaleDictionary>((acc, locale) => {
|
||||
acc[locale.code] = { name: locale.name, code: locale.code };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
const allLocalesDictionary = await this.getLocalesDataForActions();
|
||||
|
||||
const formattedData = actions.map((action: ReleaseAction) => {
|
||||
const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
|
||||
@ -289,6 +284,19 @@ const createReleaseService = ({ strapi }: { strapi: LoadedStrapi }) => ({
|
||||
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'][]) {
|
||||
const contentManagerContentTypeService = strapi
|
||||
.plugin('content-manager')
|
||||
|
Loading…
x
Reference in New Issue
Block a user