feat: make widgets api stable (#23470)

This commit is contained in:
Rémi de Juvigny 2025-05-05 18:21:11 +02:00 committed by GitHub
parent 7ae14fd44d
commit 8c28a74d12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 9 additions and 289 deletions

View File

@ -1,5 +1,3 @@
module.exports = ({ env }) => ({ module.exports = ({ env }) => ({
future: { future: {},
unstableWidgetsApi: true,
},
}); });

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Box, Flex, Grid, Main, Typography } from '@strapi/design-system'; import { Box, Flex, Grid, Main, Typography } from '@strapi/design-system';
import { CheckCircle, Pencil, PuzzlePiece } from '@strapi/icons'; import { PuzzlePiece } from '@strapi/icons';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Link as ReactRouterLink } from 'react-router-dom'; import { Link as ReactRouterLink } from 'react-router-dom';
@ -12,7 +12,6 @@ import { useEnterprise } from '../../ee';
import { useAuth } from '../../features/Auth'; import { useAuth } from '../../features/Auth';
import { useStrapiApp } from '../../features/StrapiApp'; import { useStrapiApp } from '../../features/StrapiApp';
import { LastEditedWidget, LastPublishedWidget } from './components/ContentManagerWidgets';
import { GuidedTour } from './components/GuidedTour'; import { GuidedTour } from './components/GuidedTour';
import type { WidgetType } from '@strapi/admin/strapi-admin'; import type { WidgetType } from '@strapi/admin/strapi-admin';
@ -47,12 +46,7 @@ export const WidgetRoot = ({
setPermissionStatus(shouldGrant ? 'granted' : 'forbidden'); setPermissionStatus(shouldGrant ? 'granted' : 'forbidden');
}; };
if ( if (!permissions || permissions.length === 0) {
// TODO: remove unstable check once widgets API is stable
!window.strapi.future.isEnabled('unstableWidgetsApi') ||
!permissions ||
permissions.length === 0
) {
setPermissionStatus('granted'); setPermissionStatus('granted');
} else { } else {
checkPermissions(); checkPermissions();
@ -127,7 +121,11 @@ const WidgetComponent = ({ component }: { component: () => Promise<React.Compone
return <Component />; return <Component />;
}; };
const UnstableHomePageCe = () => { /* -------------------------------------------------------------------------------------------------
* HomePageCE
* -----------------------------------------------------------------------------------------------*/
const HomePageCE = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const user = useAuth('HomePageCE', (state) => state.user); const user = useAuth('HomePageCE', (state) => state.user);
const displayName = user?.firstname ?? user?.username ?? user?.email; const displayName = user?.firstname ?? user?.username ?? user?.email;
@ -174,67 +172,6 @@ const UnstableHomePageCe = () => {
); );
}; };
/* -------------------------------------------------------------------------------------------------
* HomePageCE
* -----------------------------------------------------------------------------------------------*/
const HomePageCE = () => {
const { formatMessage } = useIntl();
const user = useAuth('HomePageCE', (state) => state.user);
const displayName = user?.firstname ?? user?.username ?? user?.email;
if (window.strapi.future.isEnabled('unstableWidgetsApi')) {
return <UnstableHomePageCe />;
}
return (
<Main>
<Page.Title>
{formatMessage({ id: 'HomePage.head.title', defaultMessage: 'Homepage' })}
</Page.Title>
<Layouts.Header
title={formatMessage(
{ id: 'HomePage.header.title', defaultMessage: 'Hello {name}' },
{ name: displayName }
)}
subtitle={formatMessage({
id: 'HomePage.header.subtitle',
defaultMessage: 'Welcome to your administration panel',
})}
/>
<Layouts.Content>
<Flex direction="column" alignItems="stretch" gap={8} paddingBottom={10}>
<GuidedTour />
<Grid.Root gap={5}>
<Grid.Item col={6} s={12}>
<WidgetRoot
title={{
id: 'content-manager.widget.last-edited.title',
defaultMessage: 'Last edited entries',
}}
icon={Pencil}
>
<LastEditedWidget />
</WidgetRoot>
</Grid.Item>
<Grid.Item col={6} s={12}>
<WidgetRoot
title={{
id: 'content-manager.widget.last-published.title',
defaultMessage: 'Last published entries',
}}
icon={CheckCircle}
>
<LastPublishedWidget />
</WidgetRoot>
</Grid.Item>
</Grid.Root>
</Flex>
</Layouts.Content>
</Main>
);
};
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
* HomePage * HomePage
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/

View File

@ -1,184 +0,0 @@
import { Box, IconButton, Status, Table, Tbody, Td, Tr, Typography } from '@strapi/design-system';
import { Pencil } from '@strapi/icons';
import { useIntl } from 'react-intl';
import { Link, useNavigate } from 'react-router-dom';
import { styled } from 'styled-components';
import { RelativeTime } from '../../../components/RelativeTime';
import { Widget } from '../../../components/WidgetHelpers';
import { useTracking } from '../../../features/Tracking';
import { useGetRecentDocumentsQuery } from '../../../services/homepage';
import { capitalise } from '../../../utils/strings';
import type { RecentDocument } from '../../../../../shared/contracts/homepage';
const CellTypography = styled(Typography).attrs({ maxWidth: '14.4rem', display: 'block' })`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
interface DocumentStatusProps {
status: RecentDocument['status'];
}
const DocumentStatus = ({ status = 'draft' }: DocumentStatusProps) => {
const statusVariant =
status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';
const { formatMessage } = useIntl();
return (
<Status variant={statusVariant} size="XS">
<Typography tag="span" variant="omega" fontWeight="bold">
{formatMessage({
id: `content-manager.containers.List.${status}`,
defaultMessage: capitalise(status),
})}
</Typography>
</Status>
);
};
const RecentDocumentsTable = ({ documents }: { documents: RecentDocument[] }) => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const navigate = useNavigate();
const getEditViewLink = (document: RecentDocument): string => {
const isSingleType = document.kind === 'singleType';
const kindPath = isSingleType ? 'single-types' : 'collection-types';
const queryParams = document.locale ? `?plugins[i18n][locale]=${document.locale}` : '';
return `/content-manager/${kindPath}/${document.contentTypeUid}${isSingleType ? '' : '/' + document.documentId}${queryParams}`;
};
const handleRowClick = (document: RecentDocument) => () => {
trackUsage('willEditEntryFromHome');
const link = getEditViewLink(document);
navigate(link);
};
return (
<Table colCount={5} rowCount={documents?.length ?? 0}>
<Tbody>
{documents?.map((document) => (
<Tr onClick={handleRowClick(document)} cursor="pointer" key={document.documentId}>
<Td>
<CellTypography title={document.title} variant="omega" textColor="neutral800">
{document.title}
</CellTypography>
</Td>
<Td>
<CellTypography variant="omega" textColor="neutral600">
{document.kind === 'singleType'
? formatMessage({
id: 'content-manager.widget.last-edited.single-type',
defaultMessage: 'Single-Type',
})
: formatMessage({
id: document.contentTypeDisplayName,
defaultMessage: document.contentTypeDisplayName,
})}
</CellTypography>
</Td>
<Td>
<Box display="inline-block">
{document.status ? (
<DocumentStatus status={document.status} />
) : (
<Typography textColor="neutral600" aria-hidden>
-
</Typography>
)}
</Box>
</Td>
<Td>
<Typography textColor="neutral600">
<RelativeTime timestamp={new Date(document.updatedAt)} />
</Typography>
</Td>
<Td onClick={(e) => e.stopPropagation()}>
<Box display="inline-block">
<IconButton
tag={Link}
to={getEditViewLink(document)}
onClick={() => trackUsage('willEditEntryFromHome')}
label={formatMessage({
id: 'content-manager.actions.edit.label',
defaultMessage: 'Edit',
})}
variant="ghost"
>
<Pencil />
</IconButton>
</Box>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
};
/* -------------------------------------------------------------------------------------------------
* LastEditedWidget
* -----------------------------------------------------------------------------------------------*/
const LastEditedWidget = () => {
const { formatMessage } = useIntl();
const { data, isLoading, error } = useGetRecentDocumentsQuery({ action: 'update' });
if (isLoading) {
return <Widget.Loading />;
}
if (error || !data) {
return <Widget.Error />;
}
if (data.length === 0) {
return (
<Widget.NoData>
{formatMessage({
id: 'content-manager.widget.last-edited.no-data',
defaultMessage: 'No edited entries',
})}
</Widget.NoData>
);
}
return <RecentDocumentsTable documents={data} />;
};
/* -------------------------------------------------------------------------------------------------
* LastPublishedWidget
* -----------------------------------------------------------------------------------------------*/
const LastPublishedWidget = () => {
const { formatMessage } = useIntl();
const { data, isLoading, error } = useGetRecentDocumentsQuery({ action: 'publish' });
if (isLoading) {
return <Widget.Loading />;
}
if (error || !data) {
return <Widget.Error />;
}
if (data.length === 0) {
return (
<Widget.NoData>
{formatMessage({
id: 'content-manager.widget.last-published.no-data',
defaultMessage: 'No published entries',
})}
</Widget.NoData>
);
}
return <RecentDocumentsTable documents={data} />;
};
export { LastEditedWidget, LastPublishedWidget };

View File

@ -1,29 +0,0 @@
import * as Homepage from '../../../shared/contracts/homepage';
import { adminApi } from './api';
/**
* TODO: Remove this service when the future flag for the widget api is removed
*/
const homepageService = adminApi
.enhanceEndpoints({
addTagTypes: ['RecentDocumentList'],
})
.injectEndpoints({
endpoints: (builder) => ({
getRecentDocuments: builder.query<
Homepage.GetRecentDocuments.Response['data'],
Homepage.GetRecentDocuments.Request['query']
>({
query: (params) => `/content-manager/homepage/recent-documents?action=${params.action}`,
transformResponse: (response: Homepage.GetRecentDocuments.Response) => response.data,
providesTags: (res, _err, { action }) => [
{ type: 'RecentDocumentList' as const, id: action },
],
}),
}),
});
const { useGetRecentDocumentsQuery } = homepageService;
export { useGetRecentDocumentsQuery };

View File

@ -1,7 +1,5 @@
export interface FeaturesConfig { export interface FeaturesConfig {
future?: { future?: object;
unstableWidgetsApi?: boolean;
};
} }
export interface FeaturesService { export interface FeaturesService {