Skip to content

Commit

Permalink
feat(app-headless-cms): enable full-screen editor for content entries (
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo authored Dec 16, 2024
1 parent d523e8f commit 7588daa
Show file tree
Hide file tree
Showing 24 changed files with 411 additions and 71 deletions.
4 changes: 3 additions & 1 deletion packages/app-headless-cms/src/ContentEntryEditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useTemplate
} from "~/admin/plugins/fieldRenderers/dynamicZone";
import { ContentEntryForm as BaseContentEntryForm } from "./admin/components/ContentEntryForm/ContentEntryForm";
import { Header as ContentEntryFormHeader } from "./admin/components/ContentEntryForm/Header";
import { ContentEntryFormPreview } from "./admin/components/ContentEntryForm/ContentEntryFormPreview";
import { useContentEntryForm } from "./admin/components/ContentEntryForm/useContentEntryForm";
import { DefaultLayout } from "~/admin/components/ContentEntryForm/DefaultLayout";
Expand All @@ -21,7 +22,8 @@ export const ContentEntryEditorConfig = Object.assign(BaseContentEntryEditorConf
useContentEntry,
DefaultLayout,
ContentEntryForm: Object.assign(BaseContentEntryForm, {
useContentEntryForm
useContentEntryForm,
Header: ContentEntryFormHeader
}),
ContentEntryFormPreview
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const FormWrapper = styled("div")({
overflow: "auto"
});

export interface ContentEntryFormProps {
export interface ContentEntryFormProps extends React.HTMLAttributes<HTMLDivElement> {
entry: Partial<CmsContentEntry>;
/**
* This callback is executed when an entry, or a revision, are created.
Expand All @@ -45,7 +45,8 @@ export const ContentEntryForm = makeDecoratable(
persistEntry,
onAfterCreate,
setSaveEntry,
header = true
header = true,
...props
}: ContentEntryFormProps) => {
const formElementRef = useRef<HTMLDivElement>(null);
const { model } = useModel();
Expand Down Expand Up @@ -78,7 +79,7 @@ export const ContentEntryForm = makeDecoratable(
>
<ModelProvider model={model}>
{header ? header : null}
<FormWrapper data-testid={"cms-content-form"} ref={formElementRef}>
<FormWrapper {...props} data-testid={"cms-content-form"} ref={formElementRef}>
{formRenderer ? (
<CustomLayout model={model} formRenderer={formRenderer} />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ import { Bind } from "@webiny/form";
import { CmsModel } from "@webiny/app-headless-cms-common/types";
import { Fields } from "~/admin/components/ContentEntryForm/Fields";

export interface DefaultLayoutProps {
export interface DefaultLayoutProps extends React.HTMLAttributes<HTMLDivElement> {
model: CmsModel;
}

export const DefaultLayout = makeDecoratable("DefaultLayout", ({ model }: DefaultLayoutProps) => {
return (
<Fields
contentModel={model}
fields={model.fields || []}
layout={model.layout || []}
/**
* TODO @ts-refactor
* Figure out type for Bind.
*/
// @ts-expect-error
Bind={Bind}
/>
);
});
export const DefaultLayout = makeDecoratable(
"DefaultLayout",
({ model, ...props }: DefaultLayoutProps) => {
return (
<div {...props}>
<Fields
contentModel={model}
fields={model.fields || []}
layout={model.layout || []}
/**
* TODO @ts-refactor
* Figure out type for Bind.
*/
// @ts-expect-error
Bind={Bind}
/>
</div>
);
}
);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { Buttons } from "@webiny/app-admin";
import { Buttons, makeDecoratable } from "@webiny/app-admin";

import { useContentEntryEditorConfig } from "~/admin/config/contentEntries";

Expand All @@ -20,11 +20,11 @@ const Actions = styled.div`
align-items: center;
`;

export const Header = () => {
export const Header = makeDecoratable("ContentEntryFormHeader", () => {
const { buttonActions } = useContentEntryEditorConfig();

return (
<ToolbarGrid>
<ToolbarGrid id="headerToolbarGrid">
<div>
<RevisionSelector />
</div>
Expand All @@ -34,4 +34,4 @@ export const Header = () => {
</Actions>
</ToolbarGrid>
);
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { ReactComponent as ListIcon } from "@material-design-icons/svg/outlined/checklist.svg";
import { ContentEntryEditorConfig } from "~/admin/config/contentEntries";
import { useFullScreenContentEntry } from "~/admin/views/contentEntries/ContentEntry/FullScreenContentEntry/useFullScreenContentEntry";

export const ShowRevisionList = () => {
const { openRevisionList } = useFullScreenContentEntry();
const { useOptionsMenuItem } = ContentEntryEditorConfig.Actions.MenuItemAction;
const { OptionsMenuItem } = useOptionsMenuItem();

return (
<OptionsMenuItem
icon={<ListIcon />}
label={"Show entry revisions"}
onAction={() => openRevisionList(true)}
data-testid={"cms.content-form.header.show-revisions"}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ShowRevisionList";
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import { ShowConfirmationOnDelete } from "~/admin/components/Decorators/ShowConf
import { ShowConfirmationOnPublish } from "~/admin/components/Decorators/ShowConfirmationOnPublish";
import { ShowConfirmationOnUnpublish } from "~/admin/components/Decorators/ShowConfirmationOnUnpublish";
import { ShowConfirmationOnDeleteRevision } from "~/admin/components/Decorators/ShowConfirmationOnDeleteRevision";
import { FullScreenContentEntry } from "~/admin/views/contentEntries/ContentEntry/FullScreenContentEntry";
import { ShowRevisionList } from "~/admin/components/ContentEntryForm/Header/ShowRevisionsList";
import { featureFlags } from "@webiny/feature-flags";

const { Browser } = ContentEntryListConfig;
const { Actions } = ContentEntryEditorConfig;
Expand Down Expand Up @@ -111,7 +114,17 @@ export const ContentEntriesModule = () => {
<Actions.ButtonAction name={"save"} element={<SaveContentButton />} />
<Actions.ButtonAction name={"publish"} element={<SaveAndPublishButton />} />
<Actions.MenuItemAction name={"delete"} element={<DeleteEntryMenuItem />} />
{/*
The following Menu Action registration is needed
only when the 'allowCmsFullScreenEditor' feature is enabled.
*/}
<Actions.MenuItemAction
name={"showRevisionsList"}
element={<ShowRevisionList />}
remove={!featureFlags.allowCmsFullScreenEditor}
/>
</ContentEntryEditorConfig>
<FullScreenContentEntry />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ContentEntry = makeDecoratable("ContentEntry", () => {
return (
<DetailsContainer>
<test-id data-testid="cms-content-details">
<Tabs value={activeTab} onActivate={setActiveTab}>
<Tabs id={"cms-content-details-tabs"} value={activeTab} onActivate={setActiveTab}>
<Tab
label={"Content"}
disabled={loading}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import styled from "@emotion/styled";
import { css } from "emotion";

export const FullScreenContentEntryContainer = styled.div`
background: var(--mdc-theme-background);
z-index: 4;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
#headerToolbarGrid {
border: 0;
padding: 0;
margin: 0;
}
#cms-content-details-tabs .webiny-ui-tabs__tab-bar {
display: none;
}
`;

/**
* HEADER
*/
export const FullScreenContentEntryHeader = styled.div`
background: var(--mdc-theme-surface);
position: fixed;
display: flex;
justify-content: space-between;
box-sizing: border-box;
width: 100%;
z-index: 4;
`;

export const FullScreenContentEntryHeaderContent = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
`;

export const TitleWrapper = styled.div`
display: flex;
align-items: baseline;
justify-content: flex-start;
flex-direction: column;
color: var(--mdc-theme-text-primary-on-background);
position: relative;
width: 100%;
margin-left: 10px;
`;

export const EntryTitle = styled.div`
width: 100%;
display: flex;
align-items: center;
`;

interface EntryNameProps {
isNewEntry?: boolean;
}

export const EntryName = styled.div<EntryNameProps>`
font-family: var(--mdc-typography-font-family);
font-size: 20px;
line-height: 1.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: ${props => (props.isNewEntry ? 0.3 : 1)};
`;

export const EntryVersion = styled.span`
font-size: 20px;
color: var(--mdc-theme-text-secondary-on-background);
margin-left: 5px;
line-height: 120%;
@media (max-width: 800px) {
display: none;
}
`;

export const EntryMeta = styled.div`
height: 20px;
margin: -2px 2px 2px 2px;
@media (max-width: 960px) {
display: none;
}
`;

/**
* FORM
*/
export const FullScreenContentEntryContent = styled.div`
overflow-y: scroll;
height: calc(100vh - 64px);
margin-top: 64px;
`;

export const FullScreenContentEntryContentFormWrapper = styled.div`
display: flex;
justify-content: center;
`;

export const FullScreenContentEntryContentFormInner = styled.div`
flex-shrink: 1;
flex-basis: 920px;
`;

export const FullScreenContentEntryContentFormInnerCss = css`
height: 100% !important;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useState } from "react";
import { createPortal } from "react-dom";
import { featureFlags } from "@webiny/feature-flags";
import { CircularProgress } from "@webiny/ui/Progress";
import { useContentEntry } from "~/admin/views/contentEntries/hooks";
import { RevisionListDrawer } from "./RevisionListDrawer";
import { FullScreenContentEntryHeaderLeft } from "./FullScreenContentEntryHeaderLeft";
import {
FullScreenContentEntryContainer,
FullScreenContentEntryContent,
FullScreenContentEntryContentFormInner,
FullScreenContentEntryContentFormInnerCss,
FullScreenContentEntryContentFormWrapper,
FullScreenContentEntryHeader,
FullScreenContentEntryHeaderContent
} from "./FullScreenContentEntry.styled";
import { FullScreenContentEntryProvider } from "./useFullScreenContentEntry";
import { ContentEntryEditorConfig } from "~/ContentEntryEditorConfig";

const { ContentEntry } = ContentEntryEditorConfig;

const FullScreenContentEntryDecorator = ContentEntry.createDecorator(Original => {
return function ContentEntry() {
const { loading } = useContentEntry();
const [isRevisionListOpen, openRevisionList] = useState<boolean>(false);

return (
<FullScreenContentEntryProvider
openRevisionList={openRevisionList}
isRevisionListOpen={isRevisionListOpen}
>
<FullScreenContentEntryContainer>
<FullScreenContentEntryHeader>
<FullScreenContentEntryHeaderContent style={{ width: "33%" }}>
<FullScreenContentEntryHeaderLeft />
</FullScreenContentEntryHeaderContent>
<FullScreenContentEntryHeaderContent>
{/*
Empty div to relocate Entry Form Header via React Portal in full-screen mode.
Ensures layout flexibility without disrupting React context and state.
*/}
<div id={"cms-content-entry-header-right"} />
</FullScreenContentEntryHeaderContent>
</FullScreenContentEntryHeader>
{loading && <CircularProgress style={{ zIndex: 10 }} />}
<FullScreenContentEntryContent>
<FullScreenContentEntryContentFormWrapper>
<FullScreenContentEntryContentFormInner>
<Original />
</FullScreenContentEntryContentFormInner>
</FullScreenContentEntryContentFormWrapper>
</FullScreenContentEntryContent>
<RevisionListDrawer />
</FullScreenContentEntryContainer>
</FullScreenContentEntryProvider>
);
};
});

const FullScreenContentEntryFormDecorator = ContentEntry.ContentEntryForm.createDecorator(
Original => {
return function ContentEntryForm(props) {
return <Original {...props} className={FullScreenContentEntryContentFormInnerCss} />;
};
}
);

const FullScreenContentEntryFormHeaderDecorator =
ContentEntry.ContentEntryForm.Header.createDecorator(Original => {
return function ContentEntryFormHeader() {
const headerRightElement = document.getElementById("cms-content-entry-header-right");

if (!headerRightElement) {
return <Original />;
}

return createPortal(<Original />, headerRightElement);
};
});

export const FullScreenContentEntry = () => {
if (!featureFlags.allowCmsFullScreenEditor) {
return null;
}

return (
<>
<FullScreenContentEntryDecorator />
<FullScreenContentEntryFormDecorator />
<FullScreenContentEntryFormHeaderDecorator />
</>
);
};
Loading

0 comments on commit 7588daa

Please sign in to comment.