From 1f6cf691b9f060c00f0054e3caf477a1005eb274 Mon Sep 17 00:00:00 2001
From: Pavel Denisjuk
Date: Thu, 25 Apr 2024 15:41:42 +0200
Subject: [PATCH] feat: various improvements and features
---
.../src/components/Table/useTableRow.tsx | 24 +-
packages/app-aco/src/config/table/Column.tsx | 4 +-
packages/app-aco/src/contexts/app.tsx | 8 +-
packages/app-aco/src/contexts/records.tsx | 2 +-
packages/app-aco/src/dialogs/dialogs.tsx | 121 ----
packages/app-aco/src/dialogs/index.tsx | 3 -
packages/app-aco/src/dialogs/styled.tsx | 13 -
.../app-aco/src/dialogs/useCreateDialog.tsx | 4 +-
.../app-aco/src/dialogs/useDeleteDialog.tsx | 4 +-
.../app-aco/src/dialogs/useEditDialog.tsx | 4 +-
.../src/dialogs/useMoveToFolderDialog.tsx | 4 +-
.../src/dialogs/useSetPermissionsDialog.tsx | 4 +-
packages/app-admin/src/base/Admin.tsx | 7 +-
packages/app-admin/src/base/ui/Menu.tsx | 6 +-
packages/app-admin/src/base/ui/Navigation.tsx | 4 +-
.../src/components/Buttons/Buttons.styles.tsx | 1 +
.../src/components/Dialogs/CustomDialog.tsx | 63 ++
.../src/components/Dialogs}/Dialog.tsx | 26 +-
.../src/components/Dialogs/DialogsContext.tsx | 165 +++++
.../src/components/Dialogs/styled.tsx | 15 +
.../src/components/Dialogs}/useDialogs.ts | 6 +-
packages/app-admin/src/hooks/index.ts | 1 +
packages/app-admin/src/index.ts | 1 +
.../src/plugins/cms/ApwOnEntryPublish.tsx | 220 +++----
.../src/components/fields/Aliases.tsx | 2 +-
.../src/components/fields/Tags.tsx | 4 +-
.../configComponents/Browser/Table/Column.tsx | 2 +-
.../FileManagerView/index.tsx | 15 +-
.../src/entries.graphql.ts | 15 +-
.../src/types/index.ts | 5 +
packages/app-headless-cms/src/HeadlessCMS.tsx | 2 -
.../ContentEntryForm/ContentEntryForm.tsx | 277 ++++-----
.../ContentEntryFormPreview.tsx | 138 ++---
.../ContentEntryForm/FieldElement.tsx | 44 +-
.../components/ContentEntryForm/Fields.tsx | 22 +-
.../ContentEntryForm/Header/Header.tsx | 50 +-
.../RevisionSelector.styles.tsx | 1 +
.../useSaveAndPublish.tsx | 11 +-
.../ContentEntryForm/useContentEntryForm.ts | 566 +++++++++---------
.../{Editor.tsx => ContentModelEditor.tsx} | 13 +-
.../ModelFieldProvider/ModelFieldContext.tsx | 14 +
.../ModelFieldProvider/useModelField.ts | 19 +-
.../config/IsApplicableToCurrentModel.tsx | 20 +
.../list/Browser/Table/Column.tsx | 14 +-
.../app-headless-cms/src/admin/constants.ts | 18 +-
.../src/admin/contexts/Cms/index.tsx | 106 ++--
.../plugins/entry/DefaultOnEntryPublish.ts | 98 ---
.../plugins/fieldRenderers/DynamicSection.tsx | 15 +-
.../dynamicZone/AddTemplate.tsx | 8 +-
.../dynamicZone/MultiValueDynamicZone.tsx | 16 +-
.../dynamicZone/SingleValueDynamicZone.tsx | 67 ++-
.../dynamicZone/useTemplateTypename.ts | 35 ++
.../admin/plugins/fieldRenderers/hidden.tsx | 17 +
.../object/multipleObjectsAccordion.tsx | 20 +-
.../object/singleObjectAccordion.tsx | 23 +-
.../object/singleObjectInline.tsx | 37 +-
.../components/SimpleSingleRenderer.tsx | 5 +
.../plugins/fieldValidators/dynamicZone.tsx | 4 +-
.../views/contentEntries/ContentEntries.tsx | 7 +-
.../views/contentEntries/ContentEntry.tsx | 2 -
.../ContentEntry/ContentEntryContext.tsx | 22 +-
.../ContentEntry/RevisionListItem.tsx | 2 +-
.../ContentEntry/useRevision.tsx | 36 +-
.../contentModels/ContentModelEditor.tsx | 4 +-
packages/app-headless-cms/src/allPlugins.ts | 4 +-
packages/app-headless-cms/src/components.ts | 13 +
.../pages/list/Browser/Table/Column.tsx | 2 +-
.../configs/list/Browser/Table/Column.tsx | 2 +-
packages/app/src/core/AddRoute.tsx | 7 +-
69 files changed, 1353 insertions(+), 1161 deletions(-)
delete mode 100644 packages/app-aco/src/dialogs/dialogs.tsx
create mode 100644 packages/app-admin/src/components/Dialogs/CustomDialog.tsx
rename packages/{app-aco/src/dialogs => app-admin/src/components/Dialogs}/Dialog.tsx (66%)
create mode 100644 packages/app-admin/src/components/Dialogs/DialogsContext.tsx
create mode 100644 packages/app-admin/src/components/Dialogs/styled.tsx
rename packages/{app-aco/src/dialogs => app-admin/src/components/Dialogs}/useDialogs.ts (58%)
rename packages/app-headless-cms/src/admin/components/ContentModelEditor/{Editor.tsx => ContentModelEditor.tsx} (89%)
create mode 100644 packages/app-headless-cms/src/admin/config/IsApplicableToCurrentModel.tsx
delete mode 100644 packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryPublish.ts
create mode 100644 packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/useTemplateTypename.ts
create mode 100644 packages/app-headless-cms/src/admin/plugins/fieldRenderers/hidden.tsx
diff --git a/packages/app-aco/src/components/Table/useTableRow.tsx b/packages/app-aco/src/components/Table/useTableRow.tsx
index fb99f99604f..b81e06a23c7 100644
--- a/packages/app-aco/src/components/Table/useTableRow.tsx
+++ b/packages/app-aco/src/components/Table/useTableRow.tsx
@@ -22,16 +22,20 @@ export const TableRowProvider = ({ row, children }: TableRowProviderProps
return {children};
};
-export const useTableRow = & DefaultData>() => {
- const context = useContext>(
- TableRowContext as unknown as Context>
- );
-
- if (!context) {
- throw Error(
- `TableRowContext is missing in the component tree. Are you using "useTableRow()" hook in the right place?`
+export const createUseTableRow = ,>() => {
+ return ,>() => {
+ const context = useContext>(
+ TableRowContext as unknown as Context<
+ TableRowContextData
+ >
);
- }
- return context;
+ if (!context) {
+ throw Error(
+ `TableRowContext is missing in the component tree. Are you using "useTableRow()" hook in the right place?`
+ );
+ }
+
+ return context;
+ };
};
diff --git a/packages/app-aco/src/config/table/Column.tsx b/packages/app-aco/src/config/table/Column.tsx
index b67be4237c8..eeb5dd5d1eb 100644
--- a/packages/app-aco/src/config/table/Column.tsx
+++ b/packages/app-aco/src/config/table/Column.tsx
@@ -1,6 +1,6 @@
import React, { ReactElement } from "react";
import { Property, useIdGenerator } from "@webiny/react-properties";
-import { useTableRow } from "~/components/Table/useTableRow";
+import { createUseTableRow } from "~/components/Table/useTableRow";
import { FolderTableItem, BaseTableItem } from "~/types";
export interface ColumnConfig {
@@ -81,4 +81,4 @@ const isFolderRow = (row: BaseTableItem): row is FolderTableItem => {
return row.$type === "FOLDER";
};
-export const Column = Object.assign(BaseColumn, { isFolderRow, useTableRow });
+export const Column = Object.assign(BaseColumn, { isFolderRow, createUseTableRow });
diff --git a/packages/app-aco/src/contexts/app.tsx b/packages/app-aco/src/contexts/app.tsx
index a07ad709a59..43843ab4cb2 100644
--- a/packages/app-aco/src/contexts/app.tsx
+++ b/packages/app-aco/src/contexts/app.tsx
@@ -1,13 +1,13 @@
import React, { useEffect, useMemo, useState } from "react";
+import { ApolloClient } from "apollo-client";
+import { CircularProgress } from "@webiny/ui/Progress";
+import { DialogsProvider } from "@webiny/app-admin";
import { AcoApp, AcoAppMode, AcoError, AcoModel, AcoModelField } from "~/types";
import { createGetAppQuery, GetAppResult, GetAppVariables } from "~/graphql/app.gql";
import { FoldersProvider as FoldersContextProvider } from "./folders";
import { SearchRecordsProvider as SearchRecordsContextProvider } from "./records";
-import { DialogsProvider as DialogsContextProvider } from "../dialogs";
import { DisplayError } from "./DisplayError";
-import { ApolloClient } from "apollo-client";
import { NavigateFolderWithRouterProvider } from "~/contexts/navigateFolderWithRouter";
-import { CircularProgress } from "@webiny/ui/Progress";
import { AcoListProvider } from "~/contexts/acoList";
export interface AcoAppProviderContext {
@@ -253,7 +253,7 @@ export const AcoAppProvider = ({
createStorageKey={createNavigateFolderStorageKey}
>
- {children}
+ {children}
diff --git a/packages/app-aco/src/contexts/records.tsx b/packages/app-aco/src/contexts/records.tsx
index 7639fd292f1..6b3f3b3384d 100644
--- a/packages/app-aco/src/contexts/records.tsx
+++ b/packages/app-aco/src/contexts/records.tsx
@@ -175,7 +175,7 @@ export const SearchRecordsProvider = ({ children }: Props) => {
const { after, limit, sort: sorting, search, where } = params;
/**
- * Avoiding to fetch records in case they have already been fetched.
+ * Avoiding fetching records in case they have already been fetched.
* This happens when visiting a list with all records loaded and receives "after" param.
*/
const totalCount = meta?.totalCount || 0;
diff --git a/packages/app-aco/src/dialogs/dialogs.tsx b/packages/app-aco/src/dialogs/dialogs.tsx
deleted file mode 100644
index fae6b07aa0f..00000000000
--- a/packages/app-aco/src/dialogs/dialogs.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React, { ReactNode, useState } from "react";
-
-import { i18n } from "@webiny/app/i18n";
-import { useSnackbar } from "@webiny/app-admin";
-
-import { Dialog } from "~/dialogs/Dialog";
-import { GenericFormData } from "@webiny/form";
-
-const t = i18n.ns("app-aco/contexts/dialogs");
-
-interface ShowDialogParams {
- title: ReactNode;
- message: ReactNode;
- acceptLabel: ReactNode;
- cancelLabel: ReactNode;
- loadingLabel: ReactNode;
- onAccept?: (data: GenericFormData) => void;
- onClose?: () => void;
-}
-
-export interface DialogsContext {
- showDialog: (params: ShowDialogParams) => void;
-}
-
-interface DialogsProviderProps {
- children: ReactNode;
-}
-
-interface State {
- title: ReactNode;
- message: ReactNode;
- acceptLabel: ReactNode;
- cancelLabel: ReactNode;
- loadingLabel: ReactNode;
- onAccept?: (data: GenericFormData) => void;
- onClose?: () => void;
- open: boolean;
- loading: boolean;
-}
-
-export const initializeState = (): State => ({
- title: t`Confirmation`,
- message: undefined,
- acceptLabel: t`Confirm`,
- cancelLabel: t`Cancel`,
- loadingLabel: t`Loading`,
- onAccept: undefined,
- onClose: undefined,
- open: false,
- loading: false
-});
-
-export const Dialogs = React.createContext(undefined);
-
-export const DialogsProvider = ({ children }: DialogsProviderProps) => {
- const { showSnackbar } = useSnackbar();
-
- const [state, setState] = useState(initializeState());
-
- const showDialog = (params: ShowDialogParams) => {
- setState(state => ({
- ...state,
- ...params,
- open: true
- }));
- };
-
- const closeDialog = () => {
- if (typeof state.onClose === "function") {
- state.onClose();
- }
-
- setState(state => ({
- ...state,
- open: false
- }));
- };
-
- const onSubmit = async (data: GenericFormData) => {
- try {
- if (typeof state.onAccept === "function") {
- setState(state => ({
- ...state,
- loading: true
- }));
-
- await state.onAccept(data);
- }
- } catch (error) {
- showSnackbar(error.message);
- } finally {
- setState(state => ({
- ...state,
- loading: false
- }));
- closeDialog();
- }
- };
-
- const context = {
- showDialog,
- closeDialog
- };
-
- return (
-
- {children}
-
-
- );
-};
diff --git a/packages/app-aco/src/dialogs/index.tsx b/packages/app-aco/src/dialogs/index.tsx
index ee7eb973357..8aa15dfc2e3 100644
--- a/packages/app-aco/src/dialogs/index.tsx
+++ b/packages/app-aco/src/dialogs/index.tsx
@@ -1,8 +1,5 @@
-export * from "./Dialog";
-export * from "./dialogs";
export * from "./useCreateDialog";
export * from "./useDeleteDialog";
-export * from "./useDialogs";
export * from "./useEditDialog";
export * from "./useMoveToFolderDialog";
export * from "./useSetPermissionsDialog";
diff --git a/packages/app-aco/src/dialogs/styled.tsx b/packages/app-aco/src/dialogs/styled.tsx
index addca5002e9..0926a8fe827 100644
--- a/packages/app-aco/src/dialogs/styled.tsx
+++ b/packages/app-aco/src/dialogs/styled.tsx
@@ -1,17 +1,4 @@
import styled from "@emotion/styled";
-import { Dialog, DialogActions as DefaultDialogActions } from "@webiny/ui/Dialog";
-
-export const DialogContainer = styled(Dialog)`
- z-index: 100;
- .mdc-dialog__surface {
- width: 600px;
- min-width: 600px;
- }
-`;
-
-export const DialogActions = styled(DefaultDialogActions)`
- justify-content: space-between;
-`;
export const DialogFoldersContainer = styled("div")`
max-height: 30vh;
diff --git a/packages/app-aco/src/dialogs/useCreateDialog.tsx b/packages/app-aco/src/dialogs/useCreateDialog.tsx
index 55f597f4365..fd85d352bce 100644
--- a/packages/app-aco/src/dialogs/useCreateDialog.tsx
+++ b/packages/app-aco/src/dialogs/useCreateDialog.tsx
@@ -8,7 +8,7 @@ import { Typography } from "@webiny/ui/Typography";
import { validation } from "@webiny/validation";
import { FolderTree } from "~/components";
-import { useDialogs } from "~/dialogs/useDialogs";
+import { useDialogs } from "@webiny/app-admin";
import { DialogFoldersContainer } from "~/dialogs/styled";
import { useFolders } from "~/hooks";
import { ROOT_FOLDER } from "~/constants";
@@ -103,7 +103,7 @@ export const useCreateDialog = (): UseCreateDialogResponse => {
dialogs.showDialog({
title: "Create a new folder",
- message: ,
+ content: ,
acceptLabel: "Create folder",
cancelLabel: "Cancel",
loadingLabel: "Creating folder",
diff --git a/packages/app-aco/src/dialogs/useDeleteDialog.tsx b/packages/app-aco/src/dialogs/useDeleteDialog.tsx
index ebe07798bde..ecd6621a451 100644
--- a/packages/app-aco/src/dialogs/useDeleteDialog.tsx
+++ b/packages/app-aco/src/dialogs/useDeleteDialog.tsx
@@ -1,6 +1,6 @@
import { useSnackbar } from "@webiny/app-admin";
-import { useDialogs } from "~/dialogs/useDialogs";
+import { useDialogs } from "@webiny/app-admin";
import { useFolders } from "~/hooks";
import { FolderItem } from "~/types";
import { useCallback } from "react";
@@ -35,7 +35,7 @@ export const useDeleteDialog = (): UseDeleteDialogResponse => {
const showDialog = ({ folder }: ShowDialogParams) => {
dialogs.showDialog({
title: "Delete folder",
- message: `You are about to delete the folder "${folder.title}"! Are you sure you want to continue?`,
+ content: `You are about to delete the folder "${folder.title}"! Are you sure you want to continue?`,
acceptLabel: "Delete folder",
cancelLabel: "Cancel",
loadingLabel: "Deleting folder",
diff --git a/packages/app-aco/src/dialogs/useEditDialog.tsx b/packages/app-aco/src/dialogs/useEditDialog.tsx
index df51e5a7d2a..8439a99ad0c 100644
--- a/packages/app-aco/src/dialogs/useEditDialog.tsx
+++ b/packages/app-aco/src/dialogs/useEditDialog.tsx
@@ -8,7 +8,7 @@ import { Typography } from "@webiny/ui/Typography";
import { FolderTree } from "~/components";
import { ROOT_FOLDER } from "~/constants";
-import { useDialogs } from "~/dialogs/useDialogs";
+import { useDialogs } from "@webiny/app-admin";
import { DialogFoldersContainer } from "~/dialogs/styled";
import { useFolders } from "~/hooks";
import { FolderItem } from "~/types";
@@ -95,7 +95,7 @@ export const useEditDialog = (): UseEditDialogResponse => {
const showDialog = ({ folder }: ShowDialogParams) => {
dialog.showDialog({
title: "Edit folder",
- message: ,
+ content: ,
acceptLabel: "Edit folder",
cancelLabel: "Cancel",
loadingLabel: "Editing folder",
diff --git a/packages/app-aco/src/dialogs/useMoveToFolderDialog.tsx b/packages/app-aco/src/dialogs/useMoveToFolderDialog.tsx
index 55cb634b291..09166b89f26 100644
--- a/packages/app-aco/src/dialogs/useMoveToFolderDialog.tsx
+++ b/packages/app-aco/src/dialogs/useMoveToFolderDialog.tsx
@@ -3,7 +3,7 @@ import React, { ReactNode } from "react";
import { i18n } from "@webiny/app/i18n";
import { Bind, GenericFormData } from "@webiny/form";
import { Typography } from "@webiny/ui/Typography";
-import { useDialogs } from "~/dialogs/useDialogs";
+import { useDialogs } from "@webiny/app-admin";
import { FolderTree } from "~/components";
import { DialogFoldersContainer } from "~/dialogs/styled";
@@ -63,7 +63,7 @@ export const useMoveToFolderDialog = (): UseMoveToFolderDialogResponse => {
}: ShowDialogParams) => {
dialogs.showDialog({
title,
- message: ,
+ content: ,
acceptLabel,
cancelLabel,
loadingLabel,
diff --git a/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx b/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx
index e01094c4026..f4288073e5f 100644
--- a/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx
+++ b/packages/app-aco/src/dialogs/useSetPermissionsDialog.tsx
@@ -8,7 +8,7 @@ import { UsersTeamsMultiAutocomplete } from "./DialogSetPermissions/UsersTeamsMu
import { UsersTeamsSelection } from "./DialogSetPermissions/UsersTeamsSelection";
import { LIST_FOLDER_LEVEL_PERMISSIONS_TARGETS } from "./DialogSetPermissions/graphql";
-import { useDialogs } from "~/dialogs/useDialogs";
+import { useDialogs } from "@webiny/app-admin";
import { useFolders } from "~/hooks";
import { FolderItem, FolderLevelPermissionsTarget, FolderPermission } from "~/types";
@@ -116,7 +116,7 @@ export const useSetPermissionsDialog = (): UseSetPermissionsDialogResponse => {
const showDialog = ({ folder }: ShowDialogParams) => {
dialogs.showDialog({
title: `Manage permissions - ${folder.title}`,
- message: ,
+ content: ,
acceptLabel: "Save",
cancelLabel: "Cancel",
loadingLabel: "Updating permissions",
diff --git a/packages/app-admin/src/base/Admin.tsx b/packages/app-admin/src/base/Admin.tsx
index cf5dce7428d..dba218aaf69 100644
--- a/packages/app-admin/src/base/Admin.tsx
+++ b/packages/app-admin/src/base/Admin.tsx
@@ -1,6 +1,8 @@
import React from "react";
import { App, Provider } from "@webiny/app";
+import { ThemeProvider } from "@webiny/app-theme";
import { WcpProvider } from "@webiny/app-wcp";
+import { CircularProgress } from "@webiny/ui/Progress";
import { ApolloClientFactory, createApolloProvider } from "./providers/ApolloProvider";
import { Base } from "./Base";
import { createTelemetryProvider } from "./providers/TelemetryProvider";
@@ -8,8 +10,7 @@ import { createUiStateProvider } from "./providers/UiStateProvider";
import { SearchProvider } from "./ui/Search";
import { UserMenuProvider } from "./ui/UserMenu";
import { NavigationProvider } from "./ui/Navigation";
-import { CircularProgress } from "@webiny/ui/Progress";
-import { ThemeProvider } from "@webiny/app-theme";
+import { createDialogsProvider } from "~/components/Dialogs/DialogsContext";
export interface AdminProps {
createApolloClient: ApolloClientFactory;
@@ -20,6 +21,7 @@ export const Admin = ({ children, createApolloClient }: AdminProps) => {
const ApolloProvider = createApolloProvider(createApolloClient);
const TelemetryProvider = createTelemetryProvider();
const UiStateProvider = createUiStateProvider();
+ const DialogsProvider = createDialogsProvider();
return (
@@ -31,6 +33,7 @@ export const Admin = ({ children, createApolloClient }: AdminProps) => {
+
{children}
diff --git a/packages/app-admin/src/base/ui/Menu.tsx b/packages/app-admin/src/base/ui/Menu.tsx
index 46923326bc7..70f8cb11fe8 100644
--- a/packages/app-admin/src/base/ui/Menu.tsx
+++ b/packages/app-admin/src/base/ui/Menu.tsx
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useEffect } from "react";
-import { useNavigation } from "~/index";
+import { makeDecoratable, useNavigation } from "~/index";
export interface MenuUpdater {
(menuItem: MenuData | undefined | null): MenuData | undefined;
@@ -80,7 +80,7 @@ const mergeMenuItems = (item1: MenuData, item2: MenuData): MenuData => {
/**
* Register a new menu item into the Admin app.
*/
-export const AddMenu = ({ children, ...props }: MenuProps) => {
+export const AddMenu = makeDecoratable("AddMenu", ({ children, ...props }: MenuProps) => {
const menu = useMenu();
const navigation = useNavigation();
@@ -170,6 +170,6 @@ export const AddMenu = ({ children, ...props }: MenuProps) => {
}
return {children};
-};
+});
AddMenu.defaultProps = { tags: [] };
diff --git a/packages/app-admin/src/base/ui/Navigation.tsx b/packages/app-admin/src/base/ui/Navigation.tsx
index 3ca54c8f436..4fe93b8124e 100644
--- a/packages/app-admin/src/base/ui/Navigation.tsx
+++ b/packages/app-admin/src/base/ui/Navigation.tsx
@@ -142,13 +142,13 @@ export const NavigationProvider = (Component: React.ComponentType) => {
};
};
-export const Navigation = () => {
+export const Navigation = makeDecoratable("Navigation", () => {
return (
);
-};
+});
export const NavigationRenderer = makeDecoratable("NavigationRenderer", createVoidComponent());
diff --git a/packages/app-admin/src/components/Buttons/Buttons.styles.tsx b/packages/app-admin/src/components/Buttons/Buttons.styles.tsx
index 0b16c495d3a..204eb0f6ca8 100644
--- a/packages/app-admin/src/components/Buttons/Buttons.styles.tsx
+++ b/packages/app-admin/src/components/Buttons/Buttons.styles.tsx
@@ -2,4 +2,5 @@ import styled from "@emotion/styled";
export const ButtonContainer = styled("div")`
margin-left: 16px;
+ text-wrap: nowrap;
`;
diff --git a/packages/app-admin/src/components/Dialogs/CustomDialog.tsx b/packages/app-admin/src/components/Dialogs/CustomDialog.tsx
new file mode 100644
index 00000000000..42aa6de2e0c
--- /dev/null
+++ b/packages/app-admin/src/components/Dialogs/CustomDialog.tsx
@@ -0,0 +1,63 @@
+import React, { useContext } from "react";
+import { Form, FormOnSubmit, GenericFormData, useForm } from "@webiny/form";
+import { DialogContainer } from "./styled";
+
+interface DialogProps {
+ onSubmit: (data: GenericFormData) => void;
+ closeDialog: () => void;
+ loading: boolean;
+ open: boolean;
+ children: JSX.Element;
+}
+
+export const CustomDialog = ({ open, loading, closeDialog, onSubmit, children }: DialogProps) => {
+ const handleSubmit: FormOnSubmit = data => {
+ onSubmit(data);
+ };
+
+ return (
+
+ {open ? (
+
+ ) : null}
+
+ );
+};
+
+export interface CustomDialogContext {
+ loading: boolean;
+ closeDialog: () => void;
+ submit: () => void;
+}
+
+const CustomDialogContext = React.createContext(undefined);
+
+interface CustomDialogProviderProps {
+ loading: boolean;
+ closeDialog: () => void;
+ children: JSX.Element;
+}
+
+const CustomDialogProvider = ({ loading, closeDialog, children }: CustomDialogProviderProps) => {
+ const form = useForm();
+
+ const context: CustomDialogContext = { submit: form.submit, loading, closeDialog };
+
+ return {children};
+};
+
+export const useCustomDialog = () => {
+ const context = useContext(CustomDialogContext);
+
+ if (!context) {
+ throw new Error("useCustomDialog must be used within the CustomDialogProvider.");
+ }
+
+ return context;
+};
diff --git a/packages/app-aco/src/dialogs/Dialog.tsx b/packages/app-admin/src/components/Dialogs/Dialog.tsx
similarity index 66%
rename from packages/app-aco/src/dialogs/Dialog.tsx
rename to packages/app-admin/src/components/Dialogs/Dialog.tsx
index 92df9e45a11..9aea4cdd9b9 100644
--- a/packages/app-aco/src/dialogs/Dialog.tsx
+++ b/packages/app-admin/src/components/Dialogs/Dialog.tsx
@@ -9,10 +9,10 @@ import { DialogContainer } from "./styled";
interface DialogProps {
title: ReactNode;
- message: ReactNode;
- acceptLabel: ReactNode;
- cancelLabel: ReactNode;
- loadingLabel: ReactNode;
+ content: ReactNode;
+ acceptLabel?: ReactNode;
+ cancelLabel?: ReactNode;
+ loadingLabel?: ReactNode;
onSubmit: (data: GenericFormData) => void;
closeDialog: () => void;
loading: boolean;
@@ -23,10 +23,10 @@ export const Dialog = ({
open,
loading,
title,
- message,
+ content,
acceptLabel,
cancelLabel,
- loadingLabel,
+ loadingLabel = "Loading...",
closeDialog,
onSubmit
}: DialogProps) => {
@@ -40,12 +40,18 @@ export const Dialog = ({
+ )
+ });
- const handleRequestReview = useCallback(
- (resolve: Resolve, input: CreateContentReviewInput): ShowConfirmationOnAccept => {
- return async () => {
- const response = await client.mutate<
- CreateContentReviewMutationResponse,
- CreateApwContentReviewMutationVariables
- >({
- mutation: CREATE_CONTENT_REVIEW_MUTATION,
- variables: {
- data: {
- content: input
+ const handleRequestReview = useCallback(
+ (resolve: Resolve, input: CreateContentReviewInput): ShowConfirmationOnAccept => {
+ return async () => {
+ const response = await client.mutate<
+ CreateContentReviewMutationResponse,
+ CreateApwContentReviewMutationVariables
+ >({
+ mutation: CREATE_CONTENT_REVIEW_MUTATION,
+ variables: {
+ data: {
+ content: input
+ }
}
+ });
+
+ const error = response.data && response.data.apw.contentReview.error;
+ const contentReview = response.data && response.data.apw.contentReview.data;
+ if (error) {
+ showSnackbar(error.message);
+ resolve();
+ return;
+ } else if (contentReview) {
+ showSnackbar(`Content review requested successfully!`);
+ /**
+ * Redirect to newly created "content review".
+ */
+ resolve();
+ navigate(
+ routePaths.CONTENT_REVIEWS + "/" + encodeURIComponent(contentReview.id)
+ );
+ return;
}
- });
- const error = response.data && response.data.apw.contentReview.error;
- const contentReview = response.data && response.data.apw.contentReview.data;
- if (error) {
- showSnackbar(error.message);
+ showSnackbar(`Something went wrong!`);
resolve();
- return;
- } else if (contentReview) {
- showSnackbar(`Content review requested successfully!`);
- /**
- * Redirect to newly created "content review".
- */
- resolve();
- navigate(
- routePaths.CONTENT_REVIEWS + "/" + encodeURIComponent(contentReview.id)
- );
- return;
- }
+ };
+ },
+ []
+ );
- showSnackbar(`Something went wrong!`);
- resolve();
- };
- },
- []
- );
+ return {
+ ...hook,
+ publishRevision: async id => {
+ const inputData = {
+ id: id || entry.id,
+ type: ApwContentTypes.CMS_ENTRY,
+ settings: {
+ modelId: contentModel.modelId
+ }
+ };
+ const { data } = await client.query({
+ query: IS_REVIEW_REQUIRED_QUERY,
+ variables: {
+ data: inputData
+ }
+ });
+ const contentReviewId = dotPropImmutable.get(
+ data,
+ "apw.isReviewRequired.data.contentReviewId"
+ );
- useEffect(() => {
- return onEntryRevisionPublish(next => async params => {
- const { id, entry } = params;
- const inputData = {
- id: id || entry.id,
- type: ApwContentTypes.CMS_ENTRY,
- settings: {
- modelId: params.model.modelId
+ if (contentReviewId) {
+ showSnackbar(`A peer review for this content has been already requested.`);
+ return {
+ error: {
+ message: `A peer review for this content has been already requested.`,
+ code: "PEER_REVIEW_REQUESTED",
+ data: {}
+ }
+ };
}
- };
- const { data } = await client.query({
- query: IS_REVIEW_REQUIRED_QUERY,
- variables: {
- data: inputData
+
+ const isReviewRequired = dotPropImmutable.get(
+ data,
+ "apw.isReviewRequired.data.isReviewRequired"
+ );
+
+ if (!isReviewRequired) {
+ return hook.publishRevision(id);
}
- });
- const contentReviewId = dotPropImmutable.get(
- data,
- "apw.isReviewRequired.data.contentReviewId"
- );
- if (contentReviewId) {
- showSnackbar(`A peer review for this content has been already requested.`);
- return next({
- ...params,
- error: {
- message: `A peer review for this content has been already requested.`,
- code: "PEER_REVIEW_REQUESTED",
- data: {}
- }
- });
- }
- const isReviewRequired = dotPropImmutable.get(
- data,
- "apw.isReviewRequired.data.isReviewRequired"
- );
- if (!isReviewRequired) {
- return next(params);
- }
- /**
- * We need to show a confirmation dialog with a promise and a resolver
- * because of the async nature of the dialog confirmation action.
- *
- * If there was no promise and resolver, the next function would be triggered immediately and the dialog would not show.
- */
- return await new Promise((resolve: any) => {
- showRequestReviewConfirmation(handleRequestReview(resolve, inputData), resolve);
- }).then(() => {
- return next({
- ...params,
- error: {
- message: `A peer review is required.`,
- code: "PEER_REVIEW_REQUIRED",
- data: {}
- }
+ /**
+ * We need to show a confirmation dialog with a promise and a resolver
+ * because of the async nature of the dialog confirmation action.
+ *
+ * If there was no promise and resolver, the next function would be triggered immediately and the dialog would not show.
+ */
+ return await new Promise((resolve: any) => {
+ showRequestReviewConfirmation(handleRequestReview(resolve, inputData), resolve);
+ }).then(() => {
+ return {
+ error: {
+ message: `A peer review is required.`,
+ code: "PEER_REVIEW_REQUIRED",
+ data: {}
+ }
+ };
});
- });
- });
- }, []);
-
- return null;
-};
+ }
+ };
+ };
+});
diff --git a/packages/app-file-manager/src/components/fields/Aliases.tsx b/packages/app-file-manager/src/components/fields/Aliases.tsx
index 8f540417778..f1ffdab4ef2 100644
--- a/packages/app-file-manager/src/components/fields/Aliases.tsx
+++ b/packages/app-file-manager/src/components/fields/Aliases.tsx
@@ -39,7 +39,7 @@ const FileAliasMessage = styled("span")`
const PATHNAME_REGEX = /^\/[/.a-zA-Z0-9-]+$/;
export const Aliases = () => {
- const { value, onChange } = useBind({ name: "aliases" });
+ const { value, onChange } = useBind({ name: "aliases" });
const addAlias = () => {
const newValue = Array.isArray(value) ? [...value] : [];
diff --git a/packages/app-file-manager/src/components/fields/Tags.tsx b/packages/app-file-manager/src/components/fields/Tags.tsx
index 95fe208d6ee..307418386ff 100644
--- a/packages/app-file-manager/src/components/fields/Tags.tsx
+++ b/packages/app-file-manager/src/components/fields/Tags.tsx
@@ -9,14 +9,14 @@ export const Tags = () => {
const { canEdit } = useFileManagerApi();
const { tags } = useFileManagerView();
- const bind = useBind({
+ const bind = useBind({
name: "tags"
});
return (
!tag.startsWith("mime:"))}
+ value={(bind.value || []).filter((tag: string) => !tag.startsWith("mime:"))}
options={tags.allTags.map(tagItem => tagItem.tag)}
label={"Tags"}
description={"Type in a new tag or select an existing one."}
diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/Browser/Table/Column.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/Browser/Table/Column.tsx
index 7137d4a17aa..91c21eb81a5 100644
--- a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/Browser/Table/Column.tsx
+++ b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/Browser/Table/Column.tsx
@@ -20,6 +20,6 @@ const BaseColumn = (props: ColumnProps) => {
};
export const Column = Object.assign(BaseColumn, {
- useTableRow: Table.Column.useTableRow,
+ useTableRow: Table.Column.createUseTableRow(),
isFolderRow: Table.Column.isFolderRow
});
diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/index.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/index.tsx
index 5979f50c93b..cb7c5a31666 100644
--- a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/index.tsx
+++ b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/index.tsx
@@ -1,6 +1,7 @@
import React from "react";
import {
createDecorator,
+ DialogsProvider,
FileManagerFileItem,
FileManagerOnChange,
FileManagerRenderer as BaseFileManagerRenderer
@@ -15,7 +16,7 @@ import { FM_ACO_APP } from "~/constants";
import { FileManagerViewWithConfig } from "./FileManagerViewConfig";
import { FoldersProvider } from "@webiny/app-aco/contexts/folders";
import { NavigateFolderProvider } from "./NavigateFolderProvider";
-import { AcoWithConfig, DialogsProvider } from "@webiny/app-aco";
+import { AcoWithConfig } from "@webiny/app-aco";
import { CompositionScope } from "@webiny/react-composition";
/**
@@ -65,13 +66,13 @@ export function FileManagerProvider({
-
-
-
+
+
+
{children}
-
-
-
+
+
+
diff --git a/packages/app-headless-cms-common/src/entries.graphql.ts b/packages/app-headless-cms-common/src/entries.graphql.ts
index e95c3a2a187..46409f31c8d 100644
--- a/packages/app-headless-cms-common/src/entries.graphql.ts
+++ b/packages/app-headless-cms-common/src/entries.graphql.ts
@@ -225,6 +225,17 @@ export interface CmsEntriesListQueryVariables {
after?: string;
}
+export const createListQueryDataSelection = (
+ model: CmsEditorContentModel,
+ fields?: CmsModelField[]
+) => {
+ return `
+ ${CONTENT_ENTRY_SYSTEM_FIELDS}
+ ${fields ? createFieldsList({ model, fields }) : ""}
+ ${!fields ? getModelTitleFieldId(model) : ""}
+ `;
+};
+
export const createListQuery = (
model: CmsEditorContentModel,
fields?: CmsModelField[],
@@ -244,9 +255,7 @@ export const createListQuery = (
search: $search
) {
data {
- ${CONTENT_ENTRY_SYSTEM_FIELDS}
- ${fields ? createFieldsList({ model, fields }) : ""}
- ${!fields ? getModelTitleFieldId(model) : ""}
+ ${createListQueryDataSelection(model, fields)}
}
meta {
cursor
diff --git a/packages/app-headless-cms-common/src/types/index.ts b/packages/app-headless-cms-common/src/types/index.ts
index 210275590ab..7bf778369ec 100644
--- a/packages/app-headless-cms-common/src/types/index.ts
+++ b/packages/app-headless-cms-common/src/types/index.ts
@@ -335,6 +335,10 @@ export interface CmsDynamicZoneTemplate {
tags?: string[];
}
+export interface CmsDynamicZoneTemplateWithTypename extends CmsDynamicZoneTemplate {
+ __typename: string;
+}
+
export type CmsContentEntryStatusType = "draft" | "published" | "unpublished";
/**
@@ -344,6 +348,7 @@ export type CmsEditorContentEntry = CmsContentEntry;
export interface CmsContentEntry {
id: string;
+ entryId: string;
modelId: string;
createdOn: string;
createdBy: CmsIdentity;
diff --git a/packages/app-headless-cms/src/HeadlessCMS.tsx b/packages/app-headless-cms/src/HeadlessCMS.tsx
index 9a8da34059a..6d2d03facd0 100644
--- a/packages/app-headless-cms/src/HeadlessCMS.tsx
+++ b/packages/app-headless-cms/src/HeadlessCMS.tsx
@@ -8,7 +8,6 @@ import { CmsMenuLoader } from "~/admin/menus/CmsMenuLoader";
import apiInformation from "./admin/plugins/apiInformation";
import { ContentEntriesModule } from "~/admin/views/contentEntries/ContentEntriesModule";
import { DefaultOnEntryDelete } from "./admin/plugins/entry/DefaultOnEntryDelete";
-import { DefaultOnEntryPublish } from "~/admin/plugins/entry/DefaultOnEntryPublish";
import { DefaultOnEntryUnpublish } from "~/admin/plugins/entry/DefaultOnEntryUnpublish";
import allPlugins from "./allPlugins";
import { LexicalEditorCmsPlugin } from "~/admin/components/LexicalCmsEditor/LexicalEditorCmsPlugin";
@@ -69,7 +68,6 @@ const HeadlessCMSExtension = ({ createApolloClient }: HeadlessCMSProps) => {
-
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx
index 60bee823c9d..bcca9e2bf80 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx
@@ -9,15 +9,16 @@ import { CmsContentEntry, CmsContentFormRendererPlugin } from "~/types";
import { useContentEntryForm, UseContentEntryFormParams } from "./useContentEntryForm";
import { Fields } from "./Fields";
import { Prompt } from "@webiny/react-router";
-import { useSnackbar } from "@webiny/app-admin";
+import { makeDecoratable, useSnackbar } from "@webiny/app-admin";
import { ModelProvider, useModel } from "~/admin/components/ModelProvider";
+import { Header } from "~/admin/components/ContentEntryForm/Header";
const FormWrapper = styled("div")({
height: "calc(100vh - 260px)",
overflow: "auto"
});
-interface ContentEntryFormProps extends UseContentEntryFormParams {
+export interface ContentEntryFormProps extends UseContentEntryFormParams {
onForm?: (form: FormAPI) => void;
}
@@ -36,139 +37,143 @@ const isDifferent = (value: any, compare: any): boolean => {
return stringify(value) !== stringify(compare);
};
-export const ContentEntryForm = ({ onForm, ...props }: ContentEntryFormProps) => {
- const formElementRef = useRef(null);
- const { model } = useModel();
- const {
- loading,
- data: initialData,
- onChange,
- onSubmit,
- invalidFields
- } = useContentEntryForm(props);
-
- const [isDirty, setIsDirty] = React.useState(false);
- /**
- * Reset isDirty when the loaded data changes.
- */
- useEffect(() => {
- if (!isDirty) {
- return;
- }
- setIsDirty(false);
- }, [initialData]);
-
- const { showSnackbar } = useSnackbar();
-
- const ref = useRef(null);
-
- useEffect(() => {
- if (typeof onForm !== "function" || !ref.current) {
- return;
- }
- onForm(ref.current);
- }, []);
-
- useEffect(() => {
- if (!formElementRef.current) {
- return;
- }
-
- formElementRef.current.scrollTo(0, 0);
- }, [initialData.id, formElementRef.current]);
-
- const formRenderer = plugins
- .byType("cms-content-form-renderer")
- .find(pl => pl.modelId === model.modelId);
-
- const renderCustomLayout = useCallback(
- (formRenderProps: FormRenderPropParams) => {
- const fields = model.fields.reduce((acc, field) => {
- acc[field.fieldId] = (
-
- );
-
- return acc;
- }, {} as Record);
- if (!formRenderer) {
- return <>{`Missing form renderer for modelId "${model.modelId}".`}>;
+export const ContentEntryForm = makeDecoratable(
+ "ContentEntryForm",
+ ({ onForm, ...props }: ContentEntryFormProps) => {
+ const formElementRef = useRef(null);
+ const { model } = useModel();
+ const {
+ loading,
+ data: initialData,
+ onChange,
+ onSubmit,
+ invalidFields
+ } = useContentEntryForm(props);
+
+ const [isDirty, setIsDirty] = React.useState(false);
+ /**
+ * Reset isDirty when the loaded data changes.
+ */
+ useEffect(() => {
+ if (!isDirty) {
+ return;
}
- return formRenderer.render({
- ...formRenderProps,
- contentModel: model,
- fields,
- /**
- * TODO @ts-refactor
- * Figure out type for Bind.
- */
- // @ts-expect-error
- Bind: formRenderProps.Bind
- });
- },
- [formRenderer]
- );
-
- return (
-
- );
-};
+ );
+
+ return acc;
+ }, {} as Record);
+ if (!formRenderer) {
+ return <>{`Missing form renderer for modelId "${model.modelId}".`}>;
+ }
+ return formRenderer.render({
+ ...formRenderProps,
+ contentModel: model,
+ fields,
+ /**
+ * TODO @ts-refactor
+ * Figure out type for Bind.
+ */
+ // @ts-expect-error
+ Bind: formRenderProps.Bind
+ });
+ },
+ [formRenderer]
+ );
+
+ return (
+
+ );
+ }
+);
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormPreview.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormPreview.tsx
index 9d85fdcc6f6..bf9a0760474 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormPreview.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryFormPreview.tsx
@@ -2,6 +2,7 @@ import React, { useCallback } from "react";
import styled from "@emotion/styled";
import { Form, FormRenderPropParams } from "@webiny/form";
import { plugins } from "@webiny/plugins";
+import { makeDecoratable } from "@webiny/app-admin";
import { FieldElement } from "./FieldElement";
import { CmsContentFormRendererPlugin, CmsEditorContentModel } from "~/types";
import { Fields } from "~/admin/components/ContentEntryForm/Fields";
@@ -12,77 +13,80 @@ const FormWrapper = styled("div")({
overflow: "auto"
});
-interface ContentEntryFormPreviewProps {
+export interface ContentEntryFormPreviewProps {
contentModel: CmsEditorContentModel;
}
-export const ContentEntryFormPreview = (props: ContentEntryFormPreviewProps) => {
- const { contentModel } = props;
+export const ContentEntryFormPreview = makeDecoratable(
+ "ContentEntryFormPreview",
+ (props: ContentEntryFormPreviewProps) => {
+ const { contentModel } = props;
- const formRenderer = plugins
- .byType("cms-content-form-renderer")
- .find(pl => pl.modelId === contentModel.modelId);
+ const formRenderer = plugins
+ .byType("cms-content-form-renderer")
+ .find(pl => pl.modelId === contentModel.modelId);
- const renderCustomLayout = useCallback(
- (formRenderProps: FormRenderPropParams) => {
- const fields = contentModel.fields.reduce((acc, field) => {
- acc[field.fieldId] = (
-
- );
+ const renderCustomLayout = useCallback(
+ (formRenderProps: FormRenderPropParams) => {
+ const fields = contentModel.fields.reduce((acc, field) => {
+ acc[field.fieldId] = (
+
+ );
- return acc;
- }, {} as Record);
- if (!formRenderer) {
- return <>{`Missing form renderer for modelId "${contentModel.modelId}".`}>;
- }
- return formRenderer.render({
- ...formRenderProps,
- /**
- * TODO @ts-refactor
- * Figure out type for Bind.
- */
- // @ts-expect-error
- Bind: formRenderProps.Bind,
- contentModel,
- fields
- });
- },
- [formRenderer, contentModel.fields]
- );
+ return acc;
+ }, {} as Record);
+ if (!formRenderer) {
+ return <>{`Missing form renderer for modelId "${contentModel.modelId}".`}>;
+ }
+ return formRenderer.render({
+ ...formRenderProps,
+ /**
+ * TODO @ts-refactor
+ * Figure out type for Bind.
+ */
+ // @ts-expect-error
+ Bind: formRenderProps.Bind,
+ contentModel,
+ fields
+ });
+ },
+ [formRenderer, contentModel.fields]
+ );
- return (
-
- );
-};
+ return (
+
+ );
+ }
+);
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx
index 118a6218bd4..f852d4ce154 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx
@@ -6,27 +6,20 @@ import { CmsModelField, CmsEditorContentModel, BindComponent } from "~/types";
import Label from "./Label";
import { useBind } from "./useBind";
import { useRenderPlugins } from "./useRenderPlugins";
-import { ModelFieldProvider } from "../ModelFieldProvider";
+import { ModelFieldProvider, useModelField } from "../ModelFieldProvider";
const t = i18n.ns("app-headless-cms/admin/components/content-form");
-export interface FieldElementProps {
- field: CmsModelField;
- Bind: BindComponent;
- contentModel: CmsEditorContentModel;
-}
+type RenderFieldProps = Omit;
-export const FieldElement = makeDecoratable("FieldElement", (props: FieldElementProps) => {
+const RenderField = (props: RenderFieldProps) => {
const renderPlugins = useRenderPlugins();
- const { field, Bind, contentModel } = props;
+ const { Bind, contentModel } = props;
+ const { field } = useModelField();
const getBind = useBind({ Bind, field });
if (typeof field.renderer === "function") {
- return (
-
- {field.renderer({ field, getBind, Label, contentModel })}
-
- );
+ return <>{field.renderer({ field, getBind, Label, contentModel })}>;
}
const renderPlugin = renderPlugins.find(
@@ -39,12 +32,25 @@ export const FieldElement = makeDecoratable("FieldElement", (props: FieldElement
});
}
- return (
-
- {renderPlugin.renderer.render({ field, getBind, Label, contentModel })}
-
- );
-});
+ return <>{renderPlugin.renderer.render({ field, getBind, Label, contentModel })}>;
+};
+
+export interface FieldElementProps {
+ field: CmsModelField;
+ Bind: BindComponent;
+ contentModel: CmsEditorContentModel;
+}
+
+export const FieldElement = makeDecoratable(
+ "FieldElement",
+ ({ field, ...props }: FieldElementProps) => {
+ return (
+
+
+
+ );
+ }
+);
/**
* @deprecated Use `FieldElement` instead.
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx
index 33d9799b669..60eeb63fb56 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx
@@ -25,15 +25,19 @@ export const Fields = ({ Bind, fields, layout, contentModel, gridClassName }: Fi
{layout.map((row, rowIndex) => (
- {row.map(fieldId => (
-
-
- |
- ))}
+ {row.map(fieldId => {
+ const field = getFieldById(fields, fieldId) as CmsModelField;
+
+ return (
+
+
+ |
+ );
+ })}
))}
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/Header.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/Header.tsx
index 5502be7dd58..1205dfd1933 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/Header.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/Header.tsx
@@ -1,45 +1,37 @@
import React from "react";
-import { css } from "emotion";
import { Buttons } from "@webiny/app-admin";
-import { Grid, Cell } from "@webiny/ui/Grid";
-import classNames from "classnames";
import { useContentEntryEditorConfig } from "~/admin/config/contentEntries";
import { ContentFormOptionsMenu } from "./ContentFormOptionsMenu";
import { RevisionSelector } from "~/admin/components/ContentEntryForm/Header/RevisionSelector";
+import styled from "@emotion/styled";
-const toolbarGrid = css({
- borderBottom: "1px solid var(--mdc-theme-on-background)"
-});
+const ToolbarGrid = styled.div`
+ padding: 15px;
+ border-bottom: 1px solid var(--mdc-theme-on-background);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
-const headerActions = css({
- display: "flex",
- alignItems: "center"
-});
-
-const headerActionsLeft = css({
- justifyContent: "flex-end"
-});
-
-const headerActionsRight = css({
- justifyContent: "flex-start"
-});
+const Actions = styled.div`
+ display: flex;
+ align-items: center;
+`;
export const Header = () => {
const { buttonActions } = useContentEntryEditorConfig();
return (
-
-
-
-
- |
-
-
-
- |
-
-
+
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/RevisionSelector/RevisionSelector.styles.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/RevisionSelector/RevisionSelector.styles.tsx
index 0f8aa82b8f8..4a2bac4f97c 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/RevisionSelector/RevisionSelector.styles.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/RevisionSelector/RevisionSelector.styles.tsx
@@ -4,6 +4,7 @@ import { Menu as DefaultMenu } from "@webiny/ui/Menu";
export const Button = styled(DefaultButtonDefault)`
color: var(--mdc-theme-text-primary-on-background) !important;
+ text-wrap: nowrap;
`;
export const Menu = styled(DefaultMenu)`
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/SaveAndPublishContent/useSaveAndPublish.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/SaveAndPublishContent/useSaveAndPublish.tsx
index 3c362b87b5d..fd47b1e98b8 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/SaveAndPublishContent/useSaveAndPublish.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Header/SaveAndPublishContent/useSaveAndPublish.tsx
@@ -24,12 +24,13 @@ export const useSaveAndPublish = (): UseSaveAndPublishResponse => {
});
const showConfirmationDialog = useCallback(
- ({ ev, onAccept, onCancel }) => {
+ async ({ ev, onAccept, onCancel }) => {
+ const entry = await form.current.submit(ev);
+ if (!entry || !entry.id) {
+ return;
+ }
+
showConfirmation(async () => {
- const entry = await form.current.submit(ev);
- if (!entry || !entry.id) {
- return;
- }
await publishRevision(entry.id);
if (typeof onAccept === "function") {
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useContentEntryForm.ts b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useContentEntryForm.ts
index 63251a23834..4f158017b4d 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useContentEntryForm.ts
+++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useContentEntryForm.ts
@@ -1,7 +1,7 @@
-import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
+import { Dispatch, SetStateAction, useCallback, useMemo, useState } from "react";
import pick from "lodash/pick";
import { useRouter } from "@webiny/react-router";
-import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar";
+import { makeDecoratable, useSnackbar } from "@webiny/app-admin";
import { FormOnSubmit } from "@webiny/form";
import {
CmsEntryCreateFromMutationResponse,
@@ -15,7 +15,7 @@ import {
createUpdateMutation,
prepareFormData
} from "@webiny/app-headless-cms-common";
-import { useCms, useModel, useMutation } from "~/admin/hooks";
+import { useModel, useMutation } from "~/admin/hooks";
import { CmsContentEntry, CmsModelField, CmsModelFieldRendererPlugin } from "~/types";
import { plugins } from "@webiny/plugins";
import { getFetchPolicy } from "~/utils/getFetchPolicy";
@@ -41,7 +41,7 @@ interface InvalidFieldError {
error: string;
}
-interface UseContentEntryForm {
+export interface UseContentEntryForm {
data: Record;
loading: boolean;
setLoading: Dispatch>;
@@ -58,326 +58,302 @@ export interface UseContentEntryFormParams {
addEntryToListCache: boolean;
}
-function useEntry(entryFromProps: Partial) {
- /**
- * We need to keep track of the entry locally
- */
- const [entry, setEntry] = useState(entryFromProps);
- const { onEntryRevisionPublish } = useCms();
-
- useEffect(() => {
- setEntry(entryFromProps);
-
- if (!entryFromProps.id) {
- return;
- }
-
- return onEntryRevisionPublish(next => async params => {
- const publishRes = await next(params);
- setEntry(entry => {
- return { ...entry, meta: publishRes?.entry?.meta || {} };
- });
- return publishRes;
- });
- }, [entryFromProps, entry.id]);
-
- return entry;
-}
+export const useContentEntryForm = makeDecoratable(
+ (params: UseContentEntryFormParams): UseContentEntryForm => {
+ const { model } = useModel();
+ const { history, search: routerSearch } = useRouter();
+ const [query] = routerSearch;
+ const { currentFolderId } = useNavigateFolder();
+ const { showSnackbar } = useSnackbar();
+ const [invalidFields, setInvalidFields] = useState>({});
+ const [loading, setLoading] = useState(false);
+ const entry = params.entry;
+
+ const { addRecordToCache, updateRecordInCache } = useRecords();
+
+ const renderPlugins = useMemo(
+ () => plugins.byType("cms-editor-field-renderer"),
+ []
+ );
+
+ const goToRevision = useCallback(
+ id => {
+ const fId = query.get("folderId");
+ const folderId = fId ? `&folderId=${encodeURIComponent(fId)}` : "";
+ history.push(
+ `/cms/content-entries/${model.modelId}?id=${encodeURIComponent(id)}${folderId}`
+ );
+ },
+ [query]
+ );
+
+ const { CREATE_CONTENT, UPDATE_CONTENT, CREATE_CONTENT_FROM } = useMemo(() => {
+ return {
+ CREATE_CONTENT: createCreateMutation(model),
+ UPDATE_CONTENT: createUpdateMutation(model),
+ CREATE_CONTENT_FROM: createCreateFromMutation(model)
+ };
+ }, [model.modelId]);
+
+ const [createMutation] = useMutation<
+ CmsEntryCreateMutationResponse,
+ CmsEntryCreateMutationVariables
+ >(CREATE_CONTENT);
+ const [updateMutation] = useMutation<
+ CmsEntryUpdateMutationResponse,
+ CmsEntryUpdateMutationVariables
+ >(UPDATE_CONTENT);
+ const [createFromMutation] = useMutation<
+ CmsEntryCreateFromMutationResponse,
+ CmsEntryCreateFromMutationVariables
+ >(CREATE_CONTENT_FROM);
-export function useContentEntryForm(params: UseContentEntryFormParams): UseContentEntryForm {
- const { model } = useModel();
- const { history, search: routerSearch } = useRouter();
- const [query] = routerSearch;
- const { currentFolderId } = useNavigateFolder();
- const { showSnackbar } = useSnackbar();
- const [invalidFields, setInvalidFields] = useState>({});
- const [loading, setLoading] = useState(false);
- const entry = useEntry(params.entry);
-
- const { addRecordToCache, updateRecordInCache } = useRecords();
-
- const renderPlugins = useMemo(
- () => plugins.byType("cms-editor-field-renderer"),
- []
- );
-
- const goToRevision = useCallback(
- id => {
- const fId = query.get("folderId");
- const folderId = fId ? `&folderId=${encodeURIComponent(fId)}` : "";
- history.push(
- `/cms/content-entries/${model.modelId}?id=${encodeURIComponent(id)}${folderId}`
- );
- },
- [query]
- );
-
- const { CREATE_CONTENT, UPDATE_CONTENT, CREATE_CONTENT_FROM } = useMemo(() => {
- return {
- CREATE_CONTENT: createCreateMutation(model),
- UPDATE_CONTENT: createUpdateMutation(model),
- CREATE_CONTENT_FROM: createCreateFromMutation(model)
+ /**
+ * Note that when passing `error.data` variable we cast as InvalidFieldError[] because we know it is so.
+ */
+ const setInvalidFieldValues = (errors?: InvalidFieldError[]): void => {
+ if (Array.isArray(errors) === false || !errors) {
+ return;
+ }
+ const values = (errors || []).reduce((acc, er) => {
+ acc[er.fieldId] = er.error;
+ return acc;
+ }, {} as Record);
+ setInvalidFields(() => values);
};
- }, [model.modelId]);
-
- const [createMutation] = useMutation<
- CmsEntryCreateMutationResponse,
- CmsEntryCreateMutationVariables
- >(CREATE_CONTENT);
- const [updateMutation] = useMutation<
- CmsEntryUpdateMutationResponse,
- CmsEntryUpdateMutationVariables
- >(UPDATE_CONTENT);
- const [createFromMutation] = useMutation<
- CmsEntryCreateFromMutationResponse,
- CmsEntryCreateFromMutationVariables
- >(CREATE_CONTENT_FROM);
-
- /**
- * Note that when passing `error.data` variable we cast as InvalidFieldError[] because we know it is so.
- */
- const setInvalidFieldValues = (errors?: InvalidFieldError[]): void => {
- if (Array.isArray(errors) === false || !errors) {
- return;
- }
- const values = (errors || []).reduce((acc, er) => {
- acc[er.fieldId] = er.error;
- return acc;
- }, {} as Record);
- setInvalidFields(() => values);
- };
-
- const resetInvalidFieldValues = (): void => {
- setInvalidFields(() => ({}));
- };
-
- const createContent: FormOnSubmit = useCallback(
- async (formData, form) => {
- setLoading(true);
- const response = await createMutation({
- variables: {
- data: {
- ...formData,
- /**
- * Added for the ACO to work.
- * TODO: introduce hook like onEntryPublish, or similar.
- */
- wbyAco_location: {
- folderId: currentFolderId || ROOT_FOLDER
+
+ const resetInvalidFieldValues = (): void => {
+ setInvalidFields(() => ({}));
+ };
+
+ const createContent: FormOnSubmit = useCallback(
+ async (formData, form) => {
+ setLoading(true);
+ const response = await createMutation({
+ variables: {
+ data: {
+ ...formData,
+ /**
+ * Added for the ACO to work.
+ * TODO: introduce hook like onEntryPublish, or similar.
+ */
+ wbyAco_location: {
+ folderId: currentFolderId || ROOT_FOLDER
+ }
+ },
+ options: {
+ skipValidators: form.options.skipValidators
}
},
- options: {
- skipValidators: form.options.skipValidators
- }
- },
- fetchPolicy: getFetchPolicy(model)
- });
+ fetchPolicy: getFetchPolicy(model)
+ });
- setLoading(false);
+ setLoading(false);
- /**
- * Finalize "create" process
- */
- if (!response.data) {
- showSnackbar("Missing response data in Create Entry.");
- return;
- }
- const { data: entry, error } = response.data.content || {};
- if (error) {
- showSnackbar(error.message);
- setInvalidFieldValues(error.data as InvalidFieldError[]);
- return;
- } else if (!entry) {
- showSnackbar("Missing entry data in Create Entry Response.");
- return;
- }
- resetInvalidFieldValues();
+ /**
+ * Finalize "create" process
+ */
+ if (!response.data) {
+ showSnackbar("Missing response data in Create Entry.");
+ return;
+ }
+ const { data: entry, error } = response.data.content || {};
+ if (error) {
+ showSnackbar(error.message);
+ setInvalidFieldValues(error.data as InvalidFieldError[]);
+ return;
+ } else if (!entry) {
+ showSnackbar("Missing entry data in Create Entry Response.");
+ return;
+ }
+ resetInvalidFieldValues();
- if (params.addEntryToListCache) {
- addRecordToCache(entry);
- }
+ if (params.addEntryToListCache) {
+ addRecordToCache(entry);
+ }
- showSnackbar(`${model.name} entry created successfully!`);
- if (typeof params.onSubmit === "function") {
- params.onSubmit(entry, form);
+ showSnackbar(`${model.name} entry created successfully!`);
+ if (typeof params.onSubmit === "function") {
+ params.onSubmit(entry, form);
+ return entry;
+ }
+ goToRevision(entry.id);
return entry;
- }
- goToRevision(entry.id);
- return entry;
- },
- [model.modelId, params.onSubmit, params.addEntryToListCache, query, currentFolderId]
- );
-
- const updateContent = useCallback(
- async (revision, data, form) => {
- setLoading(true);
- const response = await updateMutation({
- variables: {
- revision,
- data,
- options: {
- skipValidators: form.options.skipValidators
- }
- },
- fetchPolicy: getFetchPolicy(model)
- });
- setLoading(false);
- if (!response.data) {
- showSnackbar("Missing response data on Update Entry Response.");
- return;
- }
+ },
+ [model.modelId, params.onSubmit, params.addEntryToListCache, query, currentFolderId]
+ );
+
+ const updateContent = useCallback(
+ async (revision, data, form) => {
+ setLoading(true);
+ const response = await updateMutation({
+ variables: {
+ revision,
+ data,
+ options: {
+ skipValidators: form.options.skipValidators
+ }
+ },
+ fetchPolicy: getFetchPolicy(model)
+ });
+ setLoading(false);
+ if (!response.data) {
+ showSnackbar("Missing response data on Update Entry Response.");
+ return;
+ }
- const { error } = response.data.content;
- if (error) {
- showSnackbar(error.message);
- setInvalidFieldValues(error.data as InvalidFieldError[]);
- return null;
- }
+ const { error } = response.data.content;
+ if (error) {
+ showSnackbar(error.message);
+ setInvalidFieldValues(error.data as InvalidFieldError[]);
+ return null;
+ }
- resetInvalidFieldValues();
- showSnackbar("Content saved successfully.");
- const { data: entry } = response.data.content;
-
- updateRecordInCache(entry);
-
- return entry;
- },
- [model.modelId]
- );
-
- const createContentFrom = useCallback(
- async (revision: string, formData: Record, form) => {
- setLoading(true);
- const response = await createFromMutation({
- variables: {
- revision,
- data: formData,
- options: {
- skipValidators: form.options.skipValidators
- }
- },
- fetchPolicy: getFetchPolicy(model)
- });
+ resetInvalidFieldValues();
+ showSnackbar("Content saved successfully.");
+ const { data: entry } = response.data.content;
- if (!response.data) {
- showSnackbar("Missing data in update callback on Create From Entry Response.");
- return;
- }
- const { data: newRevision, error } = response.data.content;
- if (error) {
- showSnackbar(error.message);
- setInvalidFieldValues(error.data as InvalidFieldError[]);
- return;
- } else if (!newRevision) {
- showSnackbar("Missing entry data in update callback on Create From Entry.");
- return;
- }
- resetInvalidFieldValues();
+ updateRecordInCache(entry);
- updateRecordInCache(newRevision);
+ return entry;
+ },
+ [model.modelId]
+ );
+
+ const createContentFrom = useCallback(
+ async (revision: string, formData: Record, form) => {
+ setLoading(true);
+ const response = await createFromMutation({
+ variables: {
+ revision,
+ data: formData,
+ options: {
+ skipValidators: form.options.skipValidators
+ }
+ },
+ fetchPolicy: getFetchPolicy(model)
+ });
- showSnackbar("A new revision was created!");
- goToRevision(newRevision.id);
+ if (!response.data) {
+ showSnackbar("Missing data in update callback on Create From Entry Response.");
+ return;
+ }
+ const { data: newRevision, error } = response.data.content;
+ if (error) {
+ showSnackbar(error.message);
+ setInvalidFieldValues(error.data as InvalidFieldError[]);
+ return;
+ } else if (!newRevision) {
+ showSnackbar("Missing entry data in update callback on Create From Entry.");
+ return;
+ }
+ resetInvalidFieldValues();
- setLoading(false);
+ updateRecordInCache(newRevision);
- return newRevision;
- },
- [model.modelId]
- );
+ showSnackbar("A new revision was created!");
+ goToRevision(newRevision.id);
- const onChange: FormOnSubmit = (data, form) => {
- if (!params.onChange) {
- return;
- }
- return params.onChange(data, form);
- };
+ setLoading(false);
- const onSubmit: FormOnSubmit = async (data, form) => {
- const fieldsIds = model.fields.map(item => item.fieldId);
- const formData = pick(data, [...fieldsIds]);
+ return newRevision;
+ },
+ [model.modelId]
+ );
- const gqlData = prepareFormData(formData, model.fields);
+ const onChange: FormOnSubmit = (data, form) => {
+ if (!params.onChange) {
+ return;
+ }
+ return params.onChange(data, form);
+ };
- if (!entry.id) {
- return createContent(gqlData as CmsContentEntry, form);
- }
+ const onSubmit: FormOnSubmit = async (data, form) => {
+ const fieldsIds = model.fields.map(item => item.fieldId);
+ const formData = pick(data, [...fieldsIds]);
- const { meta } = entry;
- const { locked: isLocked } = meta || {};
+ const gqlData = prepareFormData(formData, model.fields);
- if (!isLocked) {
- return updateContent(entry.id, gqlData, form);
- }
+ if (!entry.id) {
+ return createContent(gqlData as CmsContentEntry, form);
+ }
- return createContentFrom(entry.id, gqlData, form);
- };
+ const { meta } = entry;
+ const { locked: isLocked } = meta || {};
- const defaultValues = useMemo((): Record => {
- const values: Record = {};
- /**
- * Assign the default values:
- * * check the settings.defaultValue
- * * check the predefinedValues for selected value
- */
- for (const field of model.fields) {
+ if (!isLocked) {
+ return updateContent(entry.id, gqlData, form);
+ }
+
+ return createContentFrom(entry.id, gqlData, form);
+ };
+
+ const defaultValues = useMemo((): Record => {
+ const values: Record = {};
/**
- * When checking if defaultValue is set in settings, we do the undefined check because it can be null, 0, empty string, false, etc...
+ * Assign the default values:
+ * * check the settings.defaultValue
+ * * check the predefinedValues for selected value
*/
- const { settings, multipleValues = false } = field;
- if (settings && settings.defaultValue !== undefined) {
+ for (const field of model.fields) {
/**
- * Special type of field is the boolean one.
- * We MUST set true/false for default value.
+ * When checking if defaultValue is set in settings, we do the undefined check because it can be null, 0, empty string, false, etc...
*/
- values[field.fieldId] = convertDefaultValue(field, settings.defaultValue);
- continue;
- }
- /**
- * No point in going further if predefined values are not enabled.
- */
- const { predefinedValues } = field;
- if (
- !predefinedValues ||
- !predefinedValues.enabled ||
- Array.isArray(predefinedValues.values) === false
- ) {
- continue;
- }
- /**
- * When field is not a multiple values one, we find the first possible default selected value and set it as field value.
- */
- if (!multipleValues) {
- const selectedValue = predefinedValues.values.find(({ selected }) => {
- return !!selected;
- });
- if (selectedValue) {
- values[field.fieldId] = convertDefaultValue(field, selectedValue.value);
+ const { settings, multipleValues = false } = field;
+ if (settings && settings.defaultValue !== undefined) {
+ /**
+ * Special type of field is the boolean one.
+ * We MUST set true/false for default value.
+ */
+ values[field.fieldId] = convertDefaultValue(field, settings.defaultValue);
+ continue;
+ }
+ /**
+ * No point in going further if predefined values are not enabled.
+ */
+ const { predefinedValues } = field;
+ if (
+ !predefinedValues ||
+ !predefinedValues.enabled ||
+ Array.isArray(predefinedValues.values) === false
+ ) {
+ continue;
}
- continue;
+ /**
+ * When field is not a multiple values one, we find the first possible default selected value and set it as field value.
+ */
+ if (!multipleValues) {
+ const selectedValue = predefinedValues.values.find(({ selected }) => {
+ return !!selected;
+ });
+ if (selectedValue) {
+ values[field.fieldId] = convertDefaultValue(field, selectedValue.value);
+ }
+ continue;
+ }
+ /**
+ *
+ */
+ values[field.fieldId] = predefinedValues.values
+ .filter(({ selected }) => !!selected)
+ .map(({ value }) => {
+ return convertDefaultValue(field, value);
+ });
}
+ return values;
+ }, [model.modelId]);
+
+ return {
/**
- *
+ * If entry is not set or `entry.id` does not exist, it means it's a new entry, so fetch default values.
*/
- values[field.fieldId] = predefinedValues.values
- .filter(({ selected }) => !!selected)
- .map(({ value }) => {
- return convertDefaultValue(field, value);
- });
- }
- return values;
- }, [model.modelId]);
-
- return {
- /**
- * If entry is not set or `entry.id` does not exist, it means it's a new entry, so fetch default values.
- */
- data: entry && entry.id ? entry : defaultValues,
- loading,
- setLoading,
- onChange,
- onSubmit,
- invalidFields,
- renderPlugins
- };
-}
+ data: entry && entry.id ? entry : defaultValues,
+ loading,
+ setLoading,
+ onChange,
+ onSubmit,
+ invalidFields,
+ renderPlugins
+ };
+ }
+);
diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx
similarity index 89%
rename from packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx
rename to packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx
index 0d672965c58..fe3e271d3dd 100644
--- a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx
@@ -1,4 +1,5 @@
import React, { useState } from "react";
+import { CompositionScope, makeDecoratable } from "@webiny/app-admin";
import { Prompt } from "@webiny/react-router";
import styled from "@emotion/styled";
import { css } from "emotion";
@@ -64,7 +65,7 @@ interface OnChangeParams {
layout: CmsEditorFieldsLayout;
}
-export const Editor = () => {
+export const ContentModelEditor = makeDecoratable("ContentModelEditor", () => {
const { data, setData, isPristine } = useModelEditor();
const [activeTabIndex, setActiveTabIndex] = useState(0);
@@ -112,9 +113,11 @@ export const Editor = () => {
-
-
-
+
+
+
+
+
@@ -123,4 +126,4 @@ export const Editor = () => {
);
-};
+});
diff --git a/packages/app-headless-cms/src/admin/components/ModelFieldProvider/ModelFieldContext.tsx b/packages/app-headless-cms/src/admin/components/ModelFieldProvider/ModelFieldContext.tsx
index 1bda00a4ccc..a523849add6 100644
--- a/packages/app-headless-cms/src/admin/components/ModelFieldProvider/ModelFieldContext.tsx
+++ b/packages/app-headless-cms/src/admin/components/ModelFieldProvider/ModelFieldContext.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { CmsModelField } from "~/types";
+import { createGenericContext } from "@webiny/app-admin";
export type ModelFieldContext = CmsModelField;
@@ -13,3 +14,16 @@ export interface ModelFieldProviderProps {
export const ModelFieldProvider = ({ field, children }: ModelFieldProviderProps) => {
return {children};
};
+
+const { Provider, useHook } = createGenericContext<{ index: number }>("FieldIndex");
+
+export const ParentValueIndexProvider = Provider;
+
+export const useParentValueIndex = () => {
+ try {
+ const context = useHook();
+ return context.index;
+ } catch {
+ return -1;
+ }
+};
diff --git a/packages/app-headless-cms/src/admin/components/ModelFieldProvider/useModelField.ts b/packages/app-headless-cms/src/admin/components/ModelFieldProvider/useModelField.ts
index ac05732d8f2..74fa5e61bfa 100644
--- a/packages/app-headless-cms/src/admin/components/ModelFieldProvider/useModelField.ts
+++ b/packages/app-headless-cms/src/admin/components/ModelFieldProvider/useModelField.ts
@@ -1,7 +1,8 @@
import { useContext } from "react";
-import { ModelFieldContext } from "./ModelFieldContext";
import { plugins } from "@webiny/plugins";
-import { CmsModelFieldTypePlugin } from "~/types";
+import { makeDecoratable } from "@webiny/react-composition";
+import { ModelFieldContext, useParentValueIndex } from "./ModelFieldContext";
+import { CmsModelField, CmsModelFieldTypePlugin } from "~/types";
interface GetFieldPlugin {
(type: string): CmsModelFieldTypePlugin;
@@ -19,11 +20,19 @@ const getFieldPlugin: GetFieldPlugin = type => {
return plugin;
};
+export interface UseModelField {
+ field: CmsModelField;
+ parentValueIndex: number;
+ fieldPlugin: CmsModelFieldTypePlugin;
+}
+
/**
* Get model field from the current context.
*/
-export function useModelField() {
+export const useModelField = makeDecoratable((): UseModelField => {
const field = useContext(ModelFieldContext);
+ const parentValueIndex = useParentValueIndex();
+
if (!field) {
throw Error(
`Missing "ModelFieldProvider" in the component tree. Are you using the "useModelField()" hook in the right place?`
@@ -32,5 +41,5 @@ export function useModelField() {
const fieldPlugin = getFieldPlugin(field.type);
- return { field, fieldPlugin };
-}
+ return { field, fieldPlugin, parentValueIndex };
+});
diff --git a/packages/app-headless-cms/src/admin/config/IsApplicableToCurrentModel.tsx b/packages/app-headless-cms/src/admin/config/IsApplicableToCurrentModel.tsx
new file mode 100644
index 00000000000..b19ae78196b
--- /dev/null
+++ b/packages/app-headless-cms/src/admin/config/IsApplicableToCurrentModel.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import { useModel } from "~/admin/components/ModelProvider";
+
+export interface IsApplicableToCurrentModelProps {
+ modelIds: string[];
+ children: React.ReactNode;
+}
+
+export const IsApplicableToCurrentModel = ({
+ modelIds,
+ children
+}: IsApplicableToCurrentModelProps) => {
+ const { model } = useModel();
+
+ if (modelIds.length > 0 && !modelIds.includes(model.modelId)) {
+ return null;
+ }
+
+ return <>{children}>;
+};
diff --git a/packages/app-headless-cms/src/admin/config/contentEntries/list/Browser/Table/Column.tsx b/packages/app-headless-cms/src/admin/config/contentEntries/list/Browser/Table/Column.tsx
index cb973150432..0ff2c5f2f9f 100644
--- a/packages/app-headless-cms/src/admin/config/contentEntries/list/Browser/Table/Column.tsx
+++ b/packages/app-headless-cms/src/admin/config/contentEntries/list/Browser/Table/Column.tsx
@@ -1,8 +1,8 @@
import React from "react";
import { CompositionScope } from "@webiny/react-composition";
import { AcoConfig, TableColumnConfig as ColumnConfig } from "@webiny/app-aco";
-import { useModel } from "~/admin/components/ModelProvider";
import { TableItem } from "~/types";
+import { IsApplicableToCurrentModel } from "~/admin/config/IsApplicableToCurrentModel";
const { Table } = AcoConfig;
@@ -13,22 +13,18 @@ export interface ColumnProps extends React.ComponentProps {
- const { model } = useModel();
-
- if (modelIds.length > 0 && !modelIds.includes(model.modelId)) {
- return null;
- }
-
return (
-
+
+
+
);
};
export const Column = Object.assign(BaseColumn, {
- useTableRow: Table.Column.useTableRow,
+ useTableRow: Table.Column.createUseTableRow(),
isFolderRow: Table.Column.isFolderRow
});
diff --git a/packages/app-headless-cms/src/admin/constants.ts b/packages/app-headless-cms/src/admin/constants.ts
index c63b391745e..d96296f4a1c 100644
--- a/packages/app-headless-cms/src/admin/constants.ts
+++ b/packages/app-headless-cms/src/admin/constants.ts
@@ -1,19 +1,11 @@
-import { i18n } from "@webiny/app/i18n";
-
-const t = i18n.ns("app-headless-cms/admin/content-entries/status");
-
export { ROOT_FOLDER } from "@webiny/app-aco/constants";
export const CMS_ENTRY_LIST_LINK = "/cms/content-entries";
export const LOCAL_STORAGE_LATEST_VISITED_FOLDER = "webiny_cms_entry_latest_visited_folder";
-export interface Statuses {
- draft: `Draft`;
- published: `Published`;
- unpublished: `Unpublished`;
-}
+export type Statuses = typeof statuses;
-export const statuses: Statuses = {
- draft: t`Draft`,
- published: t`Published`,
- unpublished: t`Unpublished`
+export const statuses = {
+ draft: "Draft",
+ published: "Published",
+ unpublished: "Unpublished"
};
diff --git a/packages/app-headless-cms/src/admin/contexts/Cms/index.tsx b/packages/app-headless-cms/src/admin/contexts/Cms/index.tsx
index a8403d206f2..11c771aec20 100644
--- a/packages/app-headless-cms/src/admin/contexts/Cms/index.tsx
+++ b/packages/app-headless-cms/src/admin/contexts/Cms/index.tsx
@@ -1,17 +1,22 @@
-import React, { useRef } from "react";
+import React, { useCallback, useRef } from "react";
import ApolloClient from "apollo-client";
import { useI18N } from "@webiny/app-i18n/hooks/useI18N";
import { CircularProgress } from "@webiny/ui/Progress";
import { config as appConfig } from "@webiny/app/config";
-import { CmsContentEntry, CmsModel } from "~/types";
+import { CmsContentEntry, CmsErrorResponse, CmsModel } from "~/types";
import { MutationHookOptions } from "@apollo/react-hooks";
import { AsyncProcessor, composeAsync } from "@webiny/utils";
+import { DocumentNode } from "graphql";
+import {
+ CmsEntryPublishMutationResponse,
+ CmsEntryPublishMutationVariables,
+ createPublishMutation
+} from "@webiny/app-headless-cms-common";
interface MutationEntryOptions {
mutationOptions?: MutationHookOptions;
}
-type PublishEntryOptions = MutationEntryOptions;
type UnpublishEntryOptions = MutationEntryOptions;
type DeleteEntryOptions = MutationEntryOptions;
@@ -21,20 +26,15 @@ interface EntryError {
data?: Record;
}
-export interface OnEntryPublishRequest {
- model: CmsModel;
- entry: CmsContentEntry;
- id: string;
- options: PublishEntryOptions;
- // TODO: Maybe a different input and output type for compose.
- error?: EntryError | null;
- locale: string;
- client: ApolloClient;
-}
-
-export interface OnEntryPublishResponse extends Omit {
- entry: CmsContentEntry | undefined;
-}
+export type OnEntryPublishResponse =
+ | {
+ entry: CmsContentEntry;
+ error?: never;
+ }
+ | {
+ entry?: never;
+ error: EntryError;
+ };
export interface OnEntryDeleteRequest {
model: CmsModel;
@@ -66,10 +66,6 @@ export interface OnEntryUnpublishResponse extends Omit;
type OnEntryDeleteSubscriber = AsyncProcessor;
type OnEntryRevisionUnpublishSubscriber = AsyncProcessor<
OnEntryUnpublishRequest,
@@ -79,7 +75,6 @@ type OnEntryRevisionUnpublishSubscriber = AsyncProcessor<
interface PublishEntryRevisionParams {
model: CmsModel;
entry: CmsContentEntry;
- options?: PublishEntryOptions;
id: string;
}
interface DeleteEntryParams {
@@ -101,7 +96,6 @@ export interface CmsContext {
createApolloClient: CmsProviderProps["createApolloClient"];
apolloClient: ApolloClient;
publishEntryRevision: (params: PublishEntryRevisionParams) => Promise;
- onEntryRevisionPublish: (fn: OnEntryRevisionPublishSubscriber) => () => void;
unpublishEntryRevision: (
params: UnpublishEntryRevisionParams
) => Promise;
@@ -127,6 +121,20 @@ interface ApolloClientsCache {
[locale: string]: ApolloClient;
}
+interface Mutations {
+ [key: string]: DocumentNode;
+}
+
+interface CreateMutationKeyParams {
+ model: CmsModel;
+ locale: string;
+}
+
+const createMutationKey = (params: CreateMutationKeyParams): string => {
+ const { model, locale } = params;
+ return `${model.modelId}_${locale}_${model.savedOn}`;
+};
+
const apolloClientsCache: ApolloClientsCache = {};
export interface CmsProviderProps {
@@ -137,11 +145,21 @@ export interface CmsProviderProps {
export const CmsProvider = (props: CmsProviderProps) => {
const apiUrl = appConfig.getKey("API_URL", process.env.REACT_APP_API_URL);
const { getCurrentLocale } = useI18N();
-
- const onEntryRevisionPublish = useRef([]);
+ const mutations = useRef({});
const onEntryRevisionUnpublish = useRef([]);
const onEntryDelete = useRef([]);
+ const getMutation = useCallback(
+ (model: CmsModel, locale: string): DocumentNode => {
+ const key = createMutationKey({ model, locale });
+ if (!mutations.current[key]) {
+ mutations.current[key] = createPublishMutation(model);
+ }
+ return mutations.current[key];
+ },
+ [mutations.current]
+ );
+
const currentLocale = getCurrentLocale("content");
if (currentLocale && !apolloClientsCache[currentLocale]) {
@@ -168,18 +186,34 @@ export const CmsProvider = (props: CmsProviderProps) => {
createApolloClient: props.createApolloClient,
apolloClient: getApolloClient(currentLocale),
publishEntryRevision: async params => {
- return await composeAsync([...onEntryRevisionPublish.current].reverse())({
- locale: currentLocale,
- ...params,
- client: getApolloClient(currentLocale),
- options: params.options || {}
+ const mutation = getMutation(params.model, currentLocale);
+ const response = await value.apolloClient.mutate<
+ CmsEntryPublishMutationResponse,
+ CmsEntryPublishMutationVariables
+ >({
+ mutation,
+ variables: {
+ revision: params.id
+ }
});
- },
- onEntryRevisionPublish: fn => {
- onEntryRevisionPublish.current.push(fn);
- return () => {
- const index = onEntryRevisionPublish.current.length;
- onEntryRevisionPublish.current.splice(index, 1);
+
+ if (!response.data) {
+ const error: CmsErrorResponse = {
+ message: "Missing response data on Publish Entry Mutation.",
+ code: "MISSING_RESPONSE_DATA",
+ data: {}
+ };
+ return { error };
+ }
+
+ const { data, error } = response.data.content;
+
+ if (error) {
+ return { error };
+ }
+
+ return {
+ entry: data as CmsContentEntry
};
},
unpublishEntryRevision: async params => {
diff --git a/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryPublish.ts b/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryPublish.ts
deleted file mode 100644
index 988d6ef63cf..00000000000
--- a/packages/app-headless-cms/src/admin/plugins/entry/DefaultOnEntryPublish.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { useCallback, useEffect, useRef } from "react";
-import { DocumentNode } from "graphql";
-import {
- CmsEntryPublishMutationResponse,
- CmsEntryPublishMutationVariables,
- createPublishMutation
-} from "@webiny/app-headless-cms-common";
-import { CmsErrorResponse, CmsModel } from "~/types";
-import { useCms } from "~/admin/hooks";
-import { OnEntryPublishResponse } from "~/admin/contexts/Cms";
-
-interface Mutations {
- [key: string]: DocumentNode;
-}
-
-interface CreateMutationKeyParams {
- model: CmsModel;
- locale: string;
-}
-
-const createMutationKey = (params: CreateMutationKeyParams): string => {
- const { model, locale } = params;
- return `${model.modelId}_${locale}_${model.savedOn}`;
-};
-
-const OnEntryPublish = () => {
- const { onEntryRevisionPublish } = useCms();
-
- const mutations = useRef({});
-
- const getMutation = useCallback(
- (model: CmsModel, locale: string): DocumentNode => {
- const key = createMutationKey({ model, locale });
- if (!mutations.current[key]) {
- mutations.current[key] = createPublishMutation(model);
- }
- return mutations.current[key];
- },
- [mutations.current]
- );
-
- const handleOnPublish = async ({ model, id, client, locale }: OnEntryPublishResponse) => {
- const mutation = getMutation(model, locale);
-
- const response = await client.mutate<
- CmsEntryPublishMutationResponse,
- CmsEntryPublishMutationVariables
- >({
- mutation,
- variables: {
- revision: id
- }
- });
-
- if (!response.data) {
- const error: CmsErrorResponse = {
- message: "Missing response data on Publish Entry Mutation.",
- code: "MISSING_RESPONSE_DATA",
- data: {}
- };
- return {
- error
- };
- }
- const { data, error } = response.data.content;
- if (error) {
- return {
- error
- };
- }
-
- return {
- entry: data,
- error: null
- };
- };
-
- useEffect(() => {
- return onEntryRevisionPublish(next => async params => {
- const result = await next(params);
-
- if (result.error) {
- return result;
- }
-
- const response = await handleOnPublish(params);
-
- return {
- ...result,
- ...response
- };
- });
- }, []);
-
- return null;
-};
-
-export const DefaultOnEntryPublish = React.memo(OnEntryPublish);
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx
index 48fb54b965c..987a54f1af4 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx
@@ -8,6 +8,7 @@ import { FormElementMessage } from "@webiny/ui/FormElementMessage";
import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18px.svg";
import { GetBindCallable } from "~/admin/components/ContentEntryForm/useBind";
import { ParentFieldProvider } from "~/admin/hooks";
+import { ParentValueIndexProvider } from "~/admin/components/ModelFieldProvider";
const t = i18n.ns("app-headless-cms/admin/fields/text");
@@ -74,10 +75,7 @@ const DynamicSection = ({
{bindProps => (
-
+
{/* We bind it to index "0", so when you start typing, that index in parent array will be populated */}
{children({
Bind: FirstFieldBind,
@@ -90,7 +88,7 @@ const DynamicSection = ({
},
index: 0 // Binds to "items.0" in the
+
)}
@@ -104,10 +102,7 @@ const DynamicSection = ({
{bindProps => (
-
+
{children({
Bind: BindField,
field,
@@ -117,7 +112,7 @@ const DynamicSection = ({
},
index: realIndex
})}
-
+
)}
|
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/AddTemplate.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/AddTemplate.tsx
index 455bccde1af..a3eee887b7e 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/AddTemplate.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/AddTemplate.tsx
@@ -3,9 +3,10 @@ import styled from "@emotion/styled";
import { ReactComponent as InfoIcon } from "@material-design-icons/svg/outlined/info.svg";
import { ReactComponent as AddIcon } from "@material-design-icons/svg/round/add_circle_outline.svg";
import { Typography } from "@webiny/ui/Typography";
-import { CmsDynamicZoneTemplate } from "~/types";
+import { CmsDynamicZoneTemplate, CmsDynamicZoneTemplateWithTypename } from "~/types";
import { TemplateGallery } from "./TemplateGallery";
import { IconButton, ButtonSecondary } from "@webiny/ui/Button";
+import { useTemplateTypename } from "~/admin/plugins/fieldRenderers/dynamicZone/useTemplateTypename";
const AddIconContainer = styled.div`
text-align: center;
@@ -34,18 +35,19 @@ const Info = styled.div`
`;
interface UseAddTemplateParams {
- onTemplate: (template: CmsDynamicZoneTemplate) => void;
+ onTemplate: (template: CmsDynamicZoneTemplateWithTypename) => void;
}
function useAddTemplate(params: UseAddTemplateParams) {
const [showGallery, setShowGallery] = useState(false);
+ const { getFullTypename } = useTemplateTypename();
const browseTemplates = () => {
setShowGallery(true);
};
const onTemplate = (template: CmsDynamicZoneTemplate) => {
- params.onTemplate(template);
+ params.onTemplate({ ...template, __typename: getFullTypename(template) });
onGalleryClose();
};
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/MultiValueDynamicZone.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/MultiValueDynamicZone.tsx
index 82a45ad71ce..066f39785f5 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/MultiValueDynamicZone.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/MultiValueDynamicZone.tsx
@@ -16,10 +16,12 @@ import {
CmsDynamicZoneTemplate,
CmsModelFieldRendererProps,
CmsModel,
- CmsModelField
+ CmsModelField,
+ CmsDynamicZoneTemplateWithTypename
} from "~/types";
import { makeDecoratable } from "@webiny/react-composition";
import { TemplateProvider } from "~/admin/plugins/fieldRenderers/dynamicZone/TemplateProvider";
+import { ParentValueIndexProvider } from "~/admin/components/ModelFieldProvider";
const BottomMargin = styled.div`
margin-bottom: 20px;
@@ -180,8 +182,8 @@ interface MultiValueDynamicZoneProps {
export const MultiValueDynamicZone = (props: MultiValueDynamicZoneProps) => {
const { bind, getBind, contentModel } = props;
- const onTemplate = (template: CmsDynamicZoneTemplate) => {
- bind.appendValue({ _templateId: template.id });
+ const onTemplate = (template: CmsDynamicZoneTemplateWithTypename) => {
+ bind.appendValue({ _templateId: template.id, __typename: template.__typename });
};
const cloneValue = (index: number) => {
@@ -203,11 +205,7 @@ export const MultiValueDynamicZone = (props: MultiValueDynamicZoneProps) => {
const Bind = getBind(index);
return (
-
+
{
onDelete={() => bind.removeValue(index)}
onClone={() => cloneValue(index)}
/>
-
+
);
})}
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/SingleValueDynamicZone.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/SingleValueDynamicZone.tsx
index 3bd7c4c1d66..aa7450755f8 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/SingleValueDynamicZone.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/SingleValueDynamicZone.tsx
@@ -9,10 +9,15 @@ import {
CmsDynamicZoneTemplate,
CmsModelFieldRendererProps,
CmsModel,
- CmsModelField
+ CmsModelField,
+ CmsDynamicZoneTemplateWithTypename
} from "~/types";
import { Fields } from "~/admin/components/ContentEntryForm/Fields";
import { ParentFieldProvider } from "~/admin/components/ContentEntryForm/ParentValue";
+import {
+ ParentValueIndexProvider,
+ ModelFieldProvider
+} from "~/admin/components/ModelFieldProvider";
type GetBind = CmsModelFieldRendererProps["getBind"];
@@ -29,8 +34,8 @@ export const SingleValueDynamicZone = ({
contentModel,
getBind
}: SingleValueDynamicZoneProps) => {
- const onTemplate = (template: CmsDynamicZoneTemplate) => {
- bind.onChange({ _templateId: template.id });
+ const onTemplate = (template: CmsDynamicZoneTemplateWithTypename) => {
+ bind.onChange({ _templateId: template.id, __typename: template.__typename });
};
const templates = field.settings?.templates || [];
@@ -49,32 +54,36 @@ export const SingleValueDynamicZone = ({
<>
{template ? (
-
- }
- open={true}
- interactive={false}
- actions={
-
- }
- onClick={unsetValue}
- />
-
- }
- >
-
-
-
-
-
+
+
+
+ }
+ open={true}
+ interactive={false}
+ actions={
+
+ }
+ onClick={unsetValue}
+ />
+
+ }
+ >
+
+
+
+
+
+
+
) : null}
{bind.value ? null : }
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/useTemplateTypename.ts b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/useTemplateTypename.ts
new file mode 100644
index 00000000000..af5c505742b
--- /dev/null
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/useTemplateTypename.ts
@@ -0,0 +1,35 @@
+import upperFirst from "lodash/upperFirst";
+import camelCase from "lodash/camelCase";
+import { CmsDynamicZoneTemplate } from "@webiny/app-headless-cms-common/types";
+import { useParentField } from "~/admin/components/ContentEntryForm/ParentValue";
+import { useModelField } from "~/admin/components/ModelFieldProvider";
+import { useModel } from "~/admin/components/ModelProvider";
+
+const createTypeName = (modelId: string): string => {
+ return upperFirst(camelCase(modelId));
+};
+
+export const useTemplateTypename = () => {
+ const { model } = useModel();
+ const { field } = useModelField();
+ const parent = useParentField();
+
+ const getFullTypename = (template: CmsDynamicZoneTemplate) => {
+ let currentParent = parent;
+ const parents: string[] = [];
+ while (currentParent) {
+ parents.push(createTypeName(currentParent.field.fieldId));
+
+ currentParent = currentParent.getParentField(0);
+ }
+
+ return [
+ model.singularApiName,
+ ...parents.reverse(),
+ createTypeName(field.fieldId),
+ template.gqlTypeName
+ ].join("_");
+ };
+
+ return { getFullTypename };
+};
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/hidden.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/hidden.tsx
new file mode 100644
index 00000000000..e1add07565b
--- /dev/null
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/hidden.tsx
@@ -0,0 +1,17 @@
+import { CmsModelFieldRendererPlugin } from "~/types";
+
+export const hiddenFieldRenderer: CmsModelFieldRendererPlugin = {
+ type: "cms-editor-field-renderer",
+ name: "cms-editor-field-renderer-hidden",
+ renderer: {
+ rendererName: "hidden",
+ name: "Hidden Field",
+ description: `Hides the component from the UI.`,
+ canUse() {
+ return true;
+ },
+ render() {
+ return null;
+ }
+ }
+};
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx
index 0331abef5e2..95eb60be5e0 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjectsAccordion.tsx
@@ -23,7 +23,6 @@ import {
} from "./StyledComponents";
import { generateAlphaNumericLowerCaseId } from "@webiny/utils";
import { FieldSettings } from "./FieldSettings";
-import { ParentFieldProvider } from "~/admin/components/ContentEntryForm/ParentValue";
const t = i18n.ns("app-headless-cms/admin/fields/text");
@@ -112,18 +111,13 @@ const ObjectsRenderer = (props: CmsModelFieldRendererProps) => {
defaultValue={index === 0}
>
-
-
-
+
|
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectAccordion.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectAccordion.tsx
index 2ea0d54b18b..1d8e2893d4c 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectAccordion.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectAccordion.tsx
@@ -5,6 +5,7 @@ import { CmsModelFieldRendererPlugin } from "~/types";
import { Fields } from "~/admin/components/ContentEntryForm/Fields";
import { FieldSettings } from "./FieldSettings";
import { ParentFieldProvider } from "~/admin/hooks";
+import { ParentValueIndexProvider } from "~/admin/components/ModelFieldProvider";
const t = i18n.ns("app-headless-cms/admin/fields/text");
@@ -34,16 +35,18 @@ const plugin: CmsModelFieldRendererPlugin = {
{bindProps => (
-
-
-
-
-
+
+
+
+
+
+
+
)}
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectInline.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectInline.tsx
index 182c9e66700..8dca21f1625 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectInline.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/singleObjectInline.tsx
@@ -8,6 +8,7 @@ import { FormElementMessage } from "@webiny/ui/FormElementMessage";
import { fieldsWrapperStyle } from "./StyledComponents";
import { FieldSettings } from "./FieldSettings";
import { ParentFieldProvider } from "~/admin/components/ContentEntryForm/ParentValue";
+import { ParentValueIndexProvider } from "~/admin/components/ModelFieldProvider";
const t = i18n.ns("app-headless-cms/admin/fields/text");
@@ -37,22 +38,26 @@ const plugin: CmsModelFieldRendererPlugin = {
{bindProps => (
-
-
-
- {field.helpText && (
- {field.helpText}
- )}
- |
-
-
- |
-
+
+
+
+
+ {field.helpText && (
+
+ {field.helpText}
+
+ )}
+ |
+
+
+ |
+
+
)}
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/simple/components/SimpleSingleRenderer.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/simple/components/SimpleSingleRenderer.tsx
index 4c655111460..7ed019616b9 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/simple/components/SimpleSingleRenderer.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/ref/simple/components/SimpleSingleRenderer.tsx
@@ -8,6 +8,7 @@ import { AddItemParams, SimpleItems } from "./SimpleItems";
interface SimpleSingleRendererProps {
bind: BindComponentRenderProp;
field: CmsModelField;
+ loadingElement?: JSX.Element;
}
export const SimpleSingleRenderer = (props: SimpleSingleRendererProps) => {
@@ -35,6 +36,10 @@ export const SimpleSingleRenderer = (props: SimpleSingleRendererProps) => {
bind.onChange(null);
}, [bind, value]);
+ if (references.loading && props.loadingElement) {
+ return props.loadingElement;
+ }
+
return (
name={"settings.templates"}>
+
{({ value: templates }) => {
return (
<>
- {templates.map((template, index) => {
+ {(templates as CmsDynamicZoneTemplate[]).map((template, index) => {
const icon = template.icon
? (template.icon.split("/") as FontAwesomeIconProps["icon"])
: undefined;
diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntries.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntries.tsx
index 5adf6d1ece3..caacf542b5a 100644
--- a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntries.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntries.tsx
@@ -1,4 +1,6 @@
import React from "react";
+import { DialogsProvider } from "@webiny/app-admin";
+import { AcoWithConfig } from "@webiny/app-aco";
import { Table as CmsAcoTable } from "./Table";
import { useModel } from "~/admin/components/ModelProvider";
import {
@@ -6,7 +8,6 @@ import {
ContentEntryListWithConfig
} from "~/admin/config/contentEntries";
import { ContentEntriesProvider } from "~/admin/views/contentEntries/ContentEntriesContext";
-import { AcoWithConfig } from "@webiny/app-aco";
export const ContentEntries = () => {
const { model } = useModel();
@@ -16,7 +17,9 @@ export const ContentEntries = () => {
-
+
+
+
diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry.tsx
index cf79270c292..31068ba1b5c 100644
--- a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry.tsx
@@ -10,7 +10,6 @@ import { Elevation } from "@webiny/ui/Elevation";
import { CircularProgress } from "@webiny/ui/Progress";
import RevisionsList from "./ContentEntry/RevisionsList";
import { useContentEntry } from "./hooks/useContentEntry";
-import { Header } from "~/admin/components/ContentEntryForm/Header";
import { ContentEntryForm } from "~/admin/components/ContentEntryForm/ContentEntryForm";
const t = i18n.namespace("app-headless-cms/admin/content-model-entries/details");
@@ -90,7 +89,6 @@ export const ContentEntry = () => {
{loading && }
-
setFormRef(form)}
diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/ContentEntryContext.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/ContentEntryContext.tsx
index 79966276f38..4add26842cd 100644
--- a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/ContentEntryContext.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/ContentEntryContext.tsx
@@ -8,7 +8,6 @@ import React, {
useRef,
useState
} from "react";
-import isEmpty from "lodash/isEmpty";
import get from "lodash/get";
import { useRouter } from "@webiny/react-router";
import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar";
@@ -38,6 +37,7 @@ type ContentEntryContextFormRef = MutableRefObject;
export interface ContentEntryContext extends ContentEntriesContext {
createEntry: () => void;
entry: CmsContentEntry;
+ setEntry: (entry: CmsContentEntry) => void;
form: ContentEntryContextFormRef;
setFormRef: (form: Pick) => void;
loading: boolean;
@@ -84,6 +84,7 @@ export const ContentEntryProvider = ({
getContentId
}: ContentEntryContextProviderProps) => {
const [activeTab, setActiveTab] = useState(0);
+ const [entry, setEntry] = useState();
const { contentModel, canCreate } = useContentEntries();
const { search } = useRouter();
@@ -116,6 +117,12 @@ export const ContentEntryProvider = ({
version = result.version;
}
+ useEffect(() => {
+ if (!revisionId && entry) {
+ setEntry(undefined);
+ }
+ }, [revisionId]);
+
const { READ_CONTENT } = useMemo(() => {
return {
READ_CONTENT: createReadQuery(contentModel)
@@ -162,13 +169,14 @@ export const ContentEntryProvider = ({
variables,
skip: !revisionId,
fetchPolicy: getFetchPolicy(contentModel),
- onCompleted: data => {
- if (!data) {
+ onCompleted: response => {
+ if (!response) {
return;
}
- const { error } = data.content;
+ const { data, error } = response.content;
if (!error) {
+ setEntry(data);
return;
}
history.push(`/cms/content-entries/${contentModel.modelId}`);
@@ -196,13 +204,13 @@ export const ContentEntryProvider = ({
}, [revisionId, getRevisions]);
const loading = isLoading || getEntry.loading || getRevisions.loading;
- const entry = (get(getEntry, "data.content.data") as unknown as CmsContentEntry) || {};
const value: ContentEntryContext = {
canCreate,
contentModel,
createEntry,
- entry,
+ entry: (entry || {}) as CmsContentEntry,
+ setEntry,
form: formRef,
loading,
revisions: get(getRevisions, "data.revisions.data") || [],
@@ -211,7 +219,7 @@ export const ContentEntryProvider = ({
setLoading,
setActiveTab,
activeTab,
- showEmptyView: !newEntry && !loading && isEmpty(entry)
+ showEmptyView: !newEntry && !loading && !revisionId
};
return {children};
diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/RevisionListItem.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/RevisionListItem.tsx
index dff359cda42..bb0578aceca 100644
--- a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/RevisionListItem.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/RevisionListItem.tsx
@@ -24,7 +24,7 @@ import { ReactComponent as DeleteIcon } from "~/admin/icons/delete.svg";
import { CmsContentEntryRevision } from "~/types";
import { i18n } from "@webiny/app/i18n";
import { useRevision } from "./useRevision";
-import usePermission from "~/admin/hooks/usePermission";
+import { usePermission } from "~/admin/hooks/usePermission";
import { useContentEntry } from "~/admin/views/contentEntries/hooks/useContentEntry";
import { PublishEntryRevisionListItem } from "./PublishEntryRevisionListItem";
import { useConfirmationDialog } from "@webiny/app-admin";
diff --git a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/useRevision.tsx b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/useRevision.tsx
index 1a65bd21646..5f837b39189 100644
--- a/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/useRevision.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentEntries/ContentEntry/useRevision.tsx
@@ -2,6 +2,8 @@ import React, { useMemo } from "react";
import { useRouter } from "@webiny/react-router";
import { useHandlers } from "@webiny/app/hooks/useHandlers";
import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar";
+import { makeDecoratable } from "@webiny/app-admin";
+import { useRecords } from "@webiny/app-aco";
import { CmsContentEntry } from "~/types";
import {
CmsEntryCreateFromMutationResponse,
@@ -11,23 +13,23 @@ import {
import { useApolloClient, useCms } from "~/admin/hooks";
import { useContentEntry } from "~/admin/views/contentEntries/hooks/useContentEntry";
import { getFetchPolicy } from "~/utils/getFetchPolicy";
-import { useRecords } from "@webiny/app-aco";
+import { OnEntryPublishResponse } from "~/admin/contexts/Cms";
-interface CreateRevisionHandler {
+export interface CreateRevisionHandler {
(id?: string): Promise;
}
-interface EditRevisionHandler {
+export interface EditRevisionHandler {
(id?: string): void;
}
-interface DeleteRevisionHandler {
+export interface DeleteRevisionHandler {
(id?: string): Promise;
}
-interface PublishRevisionHandler {
- (id?: string): Promise;
+export interface PublishRevisionHandler {
+ (id?: string): Promise;
}
-interface UnpublishRevisionHandler {
+export interface UnpublishRevisionHandler {
(id?: string): Promise;
}
interface UseRevisionHandlers {
@@ -44,9 +46,9 @@ export interface UseRevisionProps {
};
}
-export const useRevision = ({ revision }: UseRevisionProps) => {
+export const useRevision = makeDecoratable(({ revision }: UseRevisionProps) => {
const { publishEntryRevision, unpublishEntryRevision, deleteEntry } = useCms();
- const { contentModel, entry, setLoading } = useContentEntry();
+ const { contentModel, entry, setEntry, setLoading } = useContentEntry();
const { history } = useRouter();
const { showSnackbar } = useSnackbar();
@@ -149,20 +151,22 @@ export const useRevision = ({ revision }: UseRevisionProps) => {
setLoading(false);
- const { error, entry: entryResult } = response;
- if (error) {
- showSnackbar(error.message);
- return;
+ if (response.error) {
+ showSnackbar(response.error.message);
+ return response;
}
- updateRecordInCache(entryResult);
+ setEntry(response.entry);
+ updateRecordInCache(response.entry);
showSnackbar(
Successfully published revision{" "}
- #{response.entry!.meta.version}!
+ #{response.entry.meta.version}!
);
+
+ return response;
},
unpublishRevision:
({ entry }): UnpublishRevisionHandler =>
@@ -203,4 +207,4 @@ export const useRevision = ({ revision }: UseRevisionProps) => {
publishRevision,
unpublishRevision
};
-};
+});
diff --git a/packages/app-headless-cms/src/admin/views/contentModels/ContentModelEditor.tsx b/packages/app-headless-cms/src/admin/views/contentModels/ContentModelEditor.tsx
index c9ea60e4f2a..91e40b2ba15 100644
--- a/packages/app-headless-cms/src/admin/views/contentModels/ContentModelEditor.tsx
+++ b/packages/app-headless-cms/src/admin/views/contentModels/ContentModelEditor.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
-import { Editor } from "~/admin/components/ContentModelEditor/Editor";
+import { ContentModelEditor } from "~/admin/components/ContentModelEditor/ContentModelEditor";
import { useRouter } from "@webiny/react-router";
import { useCms } from "~/admin/hooks";
import { ContentModelEditorProvider } from "~/admin/components/ContentModelEditor";
@@ -20,7 +20,7 @@ const ContentModelEditorView = () => {
return (
-
+
);
diff --git a/packages/app-headless-cms/src/allPlugins.ts b/packages/app-headless-cms/src/allPlugins.ts
index 8d50a3fbb75..bf3fefafe68 100644
--- a/packages/app-headless-cms/src/allPlugins.ts
+++ b/packages/app-headless-cms/src/allPlugins.ts
@@ -20,6 +20,7 @@ import selectFieldRenderer from "~/admin/plugins/fieldRenderers/select";
import checkboxesFieldRenderer from "~/admin/plugins/fieldRenderers/checkboxes";
import refFieldRenderer from "~/admin/plugins/fieldRenderers/ref";
import objectFieldRenderer from "~/admin/plugins/fieldRenderers/object";
+import { hiddenFieldRenderer } from "~/admin/plugins/fieldRenderers/hidden";
import editorGteFieldValidator from "~/admin/plugins/fieldValidators/gte";
import editorDateGteFieldValidator from "~/admin/plugins/fieldValidators/dateGte";
@@ -83,5 +84,6 @@ export default [
objectField,
objectFieldRenderer,
dynamicZoneField,
- dynamicZoneFieldRenderer
+ dynamicZoneFieldRenderer,
+ hiddenFieldRenderer
];
diff --git a/packages/app-headless-cms/src/components.ts b/packages/app-headless-cms/src/components.ts
index 3e5675c9f6b..8fa6454a7d2 100644
--- a/packages/app-headless-cms/src/components.ts
+++ b/packages/app-headless-cms/src/components.ts
@@ -6,8 +6,21 @@ import {
TemplateGallery,
useTemplate
} from "~/admin/plugins/fieldRenderers/dynamicZone";
+import { ContentEntryForm as BaseContentEntryForm } from "./admin/components/ContentEntryForm/ContentEntryForm";
+import { ContentEntryFormPreview } from "./admin/components/ContentEntryForm/ContentEntryFormPreview";
+import { useContentEntryForm } from "./admin/components/ContentEntryForm/useContentEntryForm";
+import { useRevision } from "./admin/views/contentEntries/ContentEntry/useRevision";
+import { useContentEntry } from "~/admin/views/contentEntries/hooks";
export const Components = {
+ ContentEntry: {
+ ContentEntryForm: Object.assign(BaseContentEntryForm, {
+ useContentEntryForm,
+ useContentEntry
+ }),
+ ContentEntryFormPreview,
+ useRevision
+ },
FieldRenderers: {
DynamicZone: {
Template: {
diff --git a/packages/app-page-builder/src/admin/config/pages/list/Browser/Table/Column.tsx b/packages/app-page-builder/src/admin/config/pages/list/Browser/Table/Column.tsx
index b1b18fec746..d43d6e36b8c 100644
--- a/packages/app-page-builder/src/admin/config/pages/list/Browser/Table/Column.tsx
+++ b/packages/app-page-builder/src/admin/config/pages/list/Browser/Table/Column.tsx
@@ -20,6 +20,6 @@ const BaseColumn = (props: ColumnProps) => {
};
export const Column = Object.assign(BaseColumn, {
- useTableRow: Table.Column.useTableRow,
+ useTableRow: Table.Column.createUseTableRow(),
isFolderRow: Table.Column.isFolderRow
});
diff --git a/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx
index 327d2c0b877..14462c1d997 100644
--- a/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx
+++ b/packages/app-trash-bin/src/Presentation/configs/list/Browser/Table/Column.tsx
@@ -20,6 +20,6 @@ const BaseColumn = (props: ColumnProps) => {
};
export const Column = Object.assign(BaseColumn, {
- useTableRow: Table.Column.useTableRow,
+ useTableRow: Table.Column.createUseTableRow(),
isFolderRow: Table.Column.isFolderRow
});
diff --git a/packages/app/src/core/AddRoute.tsx b/packages/app/src/core/AddRoute.tsx
index 429327b5e29..7eefa9c5f78 100644
--- a/packages/app/src/core/AddRoute.tsx
+++ b/packages/app/src/core/AddRoute.tsx
@@ -1,8 +1,9 @@
import React, { useEffect } from "react";
-import { useApp } from "~/App";
import { Route, RouteProps } from "@webiny/react-router";
+import { useApp } from "~/App";
+import { makeDecoratable } from "@webiny/react-composition";
-export const AddRoute = (props: RouteProps) => {
+export const AddRoute = makeDecoratable("AddRoute", (props: RouteProps) => {
const { addRoute } = useApp();
useEffect(() => {
@@ -10,4 +11,4 @@ export const AddRoute = (props: RouteProps) => {
}, []);
return null;
-};
+});