From 764c93106ca125636ff08f91930da52922791ff5 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Tue, 19 Mar 2024 20:05:13 +0100 Subject: [PATCH 01/24] fix(app-headless-cms): add parent field context (#4048) --- apps/admin/src/App.tsx | 2 +- packages/api-headless-cms/src/types.ts | 1 + .../BatchEditorDialog/FieldRenderer.tsx | 8 +- .../FileDetails/FieldDecorator.tsx | 6 +- .../src/types/index.ts | 3 +- .../ContentEntryForm/ContentEntryForm.tsx | 4 +- .../ContentEntryFormPreview.tsx | 43 +++--- .../ContentEntryForm/FieldElement.tsx | 57 ++++++++ .../components/ContentEntryForm/Fields.tsx | 4 +- .../ContentEntryForm/ParentValue.tsx | 87 ++++++++++++ .../ContentEntryForm/RenderFieldElement.tsx | 50 ------- .../components/ContentEntryForm/useBind.tsx | 21 +-- .../components/ContentModelEditor/Editor.tsx | 5 +- .../editor/ContentEntryEditorConfig.tsx | 3 +- .../contentEntries/editor/FieldElement.tsx | 20 +++ .../app-headless-cms/src/admin/hooks/index.ts | 1 + .../plugins/fieldRenderers/DynamicSection.tsx | 126 ++++++++++-------- .../dynamicZone/AddTemplate.tsx | 2 +- .../dynamicZone/MultiValueDynamicZone.tsx | 47 ++++--- .../dynamicZone/SingleValueDynamicZone.tsx | 48 ++++--- .../dynamicZone/TemplateGallery.tsx | 82 ++++++++++-- .../{TemplateCard.tsx => TemplateItem.tsx} | 45 ++++--- .../fieldRenderers/dynamicZone/index.tsx | 1 + .../fieldRenderers/object/multipleObjects.tsx | 1 - .../object/multipleObjectsAccordion.tsx | 21 +-- .../object/singleObjectAccordion.tsx | 29 ++-- .../object/singleObjectInline.tsx | 39 +++--- .../fields/dynamicZone/TemplateDialog.tsx | 6 + packages/app-headless-cms/src/components.ts | 6 +- packages/app-headless-cms/src/index.tsx | 20 ++- packages/react-composition/src/decorators.tsx | 30 ++--- 31 files changed, 532 insertions(+), 286 deletions(-) create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx create mode 100644 packages/app-headless-cms/src/admin/components/ContentEntryForm/ParentValue.tsx delete mode 100644 packages/app-headless-cms/src/admin/components/ContentEntryForm/RenderFieldElement.tsx create mode 100644 packages/app-headless-cms/src/admin/config/contentEntries/editor/FieldElement.tsx rename packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/{TemplateCard.tsx => TemplateItem.tsx} (58%) diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 2773f643e51..0015d2d546b 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -3,7 +3,7 @@ import { Admin } from "@webiny/app-serverless-cms"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; -export const App = () => { +export const App: React.FC = () => { return ( diff --git a/packages/api-headless-cms/src/types.ts b/packages/api-headless-cms/src/types.ts index d02eed2c1ea..ad290d7ecda 100644 --- a/packages/api-headless-cms/src/types.ts +++ b/packages/api-headless-cms/src/types.ts @@ -280,6 +280,7 @@ export interface CmsDynamicZoneTemplate { fields: CmsModelField[]; layout: string[][]; validation: CmsModelFieldValidation[]; + tags?: string[]; } /** diff --git a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx index 74919c0cec1..308ac960258 100644 --- a/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx +++ b/packages/app-file-manager/src/components/BulkActions/ActionEdit/BatchEditorDialog/FieldRenderer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { RenderFieldElement, ModelProvider } from "@webiny/app-headless-cms"; +import { FieldElement, ModelProvider } from "@webiny/app-headless-cms"; import { Bind, BindPrefix } from "@webiny/form"; import { Cell } from "@webiny/ui/Grid"; import { FieldDTO, OperatorType } from "~/components/BulkActions/ActionEdit/domain"; @@ -28,11 +28,7 @@ export const FieldRenderer = (props: FieldRendererProps) => { {customFieldRenderer.element} ) : ( - + ); diff --git a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/FileDetails/FieldDecorator.tsx b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/FileDetails/FieldDecorator.tsx index e7a8b014817..511b552726c 100644 --- a/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/FileDetails/FieldDecorator.tsx +++ b/packages/app-file-manager/src/modules/FileManagerRenderer/FileManagerView/configComponents/FileDetails/FieldDecorator.tsx @@ -6,9 +6,9 @@ import { GenericComponent, Decorator } from "@webiny/app-admin"; -import { RenderFieldElement } from "@webiny/app-headless-cms"; +import { FieldElement } from "@webiny/app-headless-cms"; -export type FieldProps = React.ComponentProps; +export type FieldProps = React.ComponentProps; const shouldDecorate = (decoratorProps: FieldDecoratorProps, componentProps: FieldProps) => { const { id } = decoratorProps; @@ -36,7 +36,7 @@ export const createScopedFieldDecorator = return ( - + ); }; diff --git a/packages/app-headless-cms-common/src/types/index.ts b/packages/app-headless-cms-common/src/types/index.ts index efe0f004767..89c3a8399c8 100644 --- a/packages/app-headless-cms-common/src/types/index.ts +++ b/packages/app-headless-cms-common/src/types/index.ts @@ -332,6 +332,7 @@ export interface CmsDynamicZoneTemplate { fields: CmsModelField[]; layout: string[][]; validation: CmsModelFieldValidator[]; + tags?: string[]; } export type CmsContentEntryStatusType = "draft" | "published" | "unpublished"; @@ -562,7 +563,7 @@ interface BindComponentProps } export type BindComponent = React.ComponentType> & { - parentName?: string; + parentName: string; }; /** 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 bf2afc91d28..60bee823c9d 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/ContentEntryForm.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef } from "react"; -import { RenderFieldElement } from "./RenderFieldElement"; +import { FieldElement } from "./FieldElement"; import styled from "@emotion/styled"; import { Form } from "@webiny/form"; import { FormAPI, FormRenderPropParams } from "@webiny/form/types"; @@ -85,7 +85,7 @@ export const ContentEntryForm = ({ onForm, ...props }: ContentEntryFormProps) => (formRenderProps: FormRenderPropParams) => { const fields = model.fields.reduce((acc, field) => { acc[field.fieldId] = ( - (formRenderProps: FormRenderPropParams) => { const fields = contentModel.fields.reduce((acc, field) => { acc[field.fieldId] = ( - return (
{formProps => ( - - {formRenderer ? ( - renderCustomLayout(formProps) - ) : ( - - )} - + + + {formRenderer ? ( + renderCustomLayout(formProps) + ) : ( + + )} + + )}
); diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx new file mode 100644 index 00000000000..118a6218bd4 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/FieldElement.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import get from "lodash/get"; +import { makeDecoratable } from "@webiny/app-admin"; +import { i18n } from "@webiny/app/i18n"; +import { CmsModelField, CmsEditorContentModel, BindComponent } from "~/types"; +import Label from "./Label"; +import { useBind } from "./useBind"; +import { useRenderPlugins } from "./useRenderPlugins"; +import { ModelFieldProvider } from "../ModelFieldProvider"; + +const t = i18n.ns("app-headless-cms/admin/components/content-form"); + +export interface FieldElementProps { + field: CmsModelField; + Bind: BindComponent; + contentModel: CmsEditorContentModel; +} + +export const FieldElement = makeDecoratable("FieldElement", (props: FieldElementProps) => { + const renderPlugins = useRenderPlugins(); + const { field, Bind, contentModel } = props; + const getBind = useBind({ Bind, field }); + + if (typeof field.renderer === "function") { + return ( + + {field.renderer({ field, getBind, Label, contentModel })} + + ); + } + + const renderPlugin = renderPlugins.find( + plugin => plugin.renderer.rendererName === get(field, "renderer.name") + ); + + if (!renderPlugin) { + return t`Cannot render "{fieldName}" field - field renderer missing.`({ + fieldName: {field.fieldId} + }); + } + + return ( + + {renderPlugin.renderer.render({ field, getBind, Label, contentModel })} + + ); +}); + +/** + * @deprecated Use `FieldElement` instead. + */ +export const RenderFieldElement = FieldElement; + +/** + * @deprecated Use `FieldElementProps` instead. + */ +export type RenderFieldElementProps = FieldElementProps; 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 73ac588e0da..33d9799b669 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/Fields.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Cell, Grid } from "@webiny/ui/Grid"; -import { RenderFieldElement } from "./RenderFieldElement"; +import { FieldElement } from "./FieldElement"; import { CmsEditorContentModel, CmsModelField, @@ -27,7 +27,7 @@ export const Fields = ({ Bind, fields, layout, contentModel, gridClassName }: Fi {row.map(fieldId => ( - ; + } + } +} + +interface ParentField { + value: any; + setValue: (fieldId: string, cb: (prevValue: any) => any) => void; + field: CmsModelField; + getParentField(level: number): ParentField | undefined; + path: string; +} + +const ParentField = createContext(undefined); + +export function useParentField(level = 0): ParentField | undefined { + const parent = useContext(ParentField); + + if (!parent) { + return undefined; + } + + return level === 0 ? parent : parent.getParentField(level); +} + +interface ParentFieldProviderProps { + value: any; + path: string; + children: React.ReactNode; +} + +export const ParentFieldProvider = ({ path, value, children }: ParentFieldProviderProps) => { + const parent = useContext(ParentField); + const form = useForm(); + const formRef = useRef(); + + let field: CmsModelField | undefined; + try { + const fieldContext = useModelField(); + field = fieldContext.field; + } catch { + field = undefined; + } + + const getParentField = (level = 0) => { + return parent ? (level === 0 ? parent : parent.getParentField(level - 1)) : undefined; + }; + + useEffect(() => { + formRef.current = form; + }, [form.data]); + + const setValue = useCallback((fieldId, cb) => { + const fieldPath = `${path}.${fieldId}`; + if (!path || !formRef.current) { + return; + } + + formRef.current.setValue(fieldPath, cb(get(formRef.current.data, fieldPath))); + }, []); + + const context: ParentField | undefined = field + ? { + value, + field, + getParentField, + path, + setValue + } + : undefined; + + return ( + + {children} + + ); +}; diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/RenderFieldElement.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/RenderFieldElement.tsx deleted file mode 100644 index 82243a2c09e..00000000000 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/RenderFieldElement.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import get from "lodash/get"; -import { makeDecoratable } from "@webiny/app-admin"; -import { i18n } from "@webiny/app/i18n"; -import { CmsModelField, CmsEditorContentModel, BindComponent } from "~/types"; -import Label from "./Label"; -import { useBind } from "./useBind"; -import { useRenderPlugins } from "./useRenderPlugins"; -import { ModelFieldProvider } from "../ModelFieldProvider"; - -const t = i18n.ns("app-headless-cms/admin/components/content-form"); - -export interface RenderFieldElementProps { - field: CmsModelField; - Bind: BindComponent; - contentModel: CmsEditorContentModel; -} - -export const RenderFieldElement = makeDecoratable( - "RenderFieldElement", - (props: RenderFieldElementProps) => { - const renderPlugins = useRenderPlugins(); - const { field, Bind, contentModel } = props; - const getBind = useBind({ Bind, field }); - - if (typeof field.renderer === "function") { - return ( - - {field.renderer({ field, getBind, Label, contentModel })} - - ); - } - - const renderPlugin = renderPlugins.find( - plugin => plugin.renderer.rendererName === get(field, "renderer.name") - ); - - if (!renderPlugin) { - return t`Cannot render "{fieldName}" field - field renderer missing.`({ - fieldName: {field.fieldId} - }); - } - - return ( - - {renderPlugin.renderer.render({ field, getBind, Label, contentModel })} - - ); - } -); diff --git a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx index 5232cb8c69e..8ff8a866549 100644 --- a/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentEntryForm/useBind.tsx @@ -18,12 +18,12 @@ export interface GetBindCallable { (index?: number): BindComponent; } -export function useBind({ Bind: ParentBind, field }: UseBindProps) { +export function useBind({ Bind, field }: UseBindProps) { const memoizedBindComponents = useRef>({}); return useCallback( (index = -1) => { - const { parentName } = ParentBind; + const { parentName } = Bind; // If there's a parent name assigned to the given Bind component, we need to include it in the new field "name". // This allows us to have nested fields (like "object" field with nested properties) const name = [parentName, field.fieldId, index >= 0 ? index : undefined] @@ -44,7 +44,7 @@ export function useBind({ Bind: ParentBind, field }: UseBindProps) { const { name: childName, validators: childValidators, children } = params; return ( - + ); - }; + } as BindComponent; // We need to keep track of current field name, to support nested fields. memoizedBindComponents.current[name].parentName = name; - memoizedBindComponents.current[name].displayName = `ParentBind<${name}>`; + memoizedBindComponents.current[name].displayName = `Bind<${name}>`; return memoizedBindComponents.current[name]; }, diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx index adf1b138bb8..0d672965c58 100644 --- a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/Editor.tsx @@ -16,6 +16,7 @@ import Header from "./Header"; import DragPreview from "../DragPreview"; import { useModelEditor } from "./useModelEditor"; import { CmsModelField, CmsEditorFieldsLayout } from "~/types"; +import { ContentEntryEditorWithConfig } from "~/admin/config/contentEntries"; const t = i18n.ns("app-headless-cms/admin/editor"); @@ -111,7 +112,9 @@ export const Editor = () => { - + + + diff --git a/packages/app-headless-cms/src/admin/config/contentEntries/editor/ContentEntryEditorConfig.tsx b/packages/app-headless-cms/src/admin/config/contentEntries/editor/ContentEntryEditorConfig.tsx index 753451f7197..5ec1c9cbf86 100644 --- a/packages/app-headless-cms/src/admin/config/contentEntries/editor/ContentEntryEditorConfig.tsx +++ b/packages/app-headless-cms/src/admin/config/contentEntries/editor/ContentEntryEditorConfig.tsx @@ -1,10 +1,11 @@ import { useMemo } from "react"; import { createConfigurableComponent } from "@webiny/react-properties"; import { Actions, ActionsConfig } from "./Actions"; +import { FieldElement } from "./FieldElement"; const base = createConfigurableComponent("ContentEntryEditorConfig"); -export const ContentEntryEditorConfig = Object.assign(base.Config, { Actions }); +export const ContentEntryEditorConfig = Object.assign(base.Config, { Actions, FieldElement }); export const ContentEntryEditorWithConfig = base.WithConfig; diff --git a/packages/app-headless-cms/src/admin/config/contentEntries/editor/FieldElement.tsx b/packages/app-headless-cms/src/admin/config/contentEntries/editor/FieldElement.tsx new file mode 100644 index 00000000000..f32e94255e7 --- /dev/null +++ b/packages/app-headless-cms/src/admin/config/contentEntries/editor/FieldElement.tsx @@ -0,0 +1,20 @@ +import { createDecoratorFactory } from "@webiny/react-composition"; +import { FieldElement as FieldElementComponent } from "~/admin/components/ContentEntryForm/FieldElement"; +import { useModel } from "~/admin/components/ModelProvider"; + +const createDecorator = createDecoratorFactory<{ modelIds?: string[] }>()( + FieldElementComponent, + decoratorProps => { + const { model } = useModel(); + + if (decoratorProps?.modelIds?.length && !decoratorProps.modelIds.includes(model.modelId)) { + return false; + } + + return true; + } +); + +export const FieldElement = { + createDecorator +}; diff --git a/packages/app-headless-cms/src/admin/hooks/index.ts b/packages/app-headless-cms/src/admin/hooks/index.ts index f460bfc1fed..3c09b3ac606 100644 --- a/packages/app-headless-cms/src/admin/hooks/index.ts +++ b/packages/app-headless-cms/src/admin/hooks/index.ts @@ -7,6 +7,7 @@ export { default as useApolloClient } from "./useApolloClient"; export { default as usePermission } from "./usePermission"; export { useModel } from "../components/ModelProvider"; export { useModelEditor } from "../components/ContentModelEditor"; +export { useParentField, ParentFieldProvider } from "../components/ContentEntryForm/ParentValue"; export { useModelField } from "../components/ModelFieldProvider"; export { useModelFieldEditor } from "../components/FieldEditor"; export { useChangeEntryStatus } from "~/admin/hooks/useChangeEntryStatus"; 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 485caef6f5a..48fb54b965c 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/DynamicSection.tsx @@ -7,6 +7,7 @@ import { BindComponent, BindComponentRenderProp, CmsModelField } from "~/types"; 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"; const t = i18n.ns("app-headless-cms/admin/fields/text"); @@ -64,70 +65,83 @@ const DynamicSection = ({ const bindFieldValue: string[] = value || []; return ( - - {typeof renderTitle === "function" && renderTitle(bindFieldValue)} - - {/* We always render the first item, for better UX */} - {showLabel && field.label && } - - {bindProps => - /* We bind it to index "0", so when you start typing, that index in parent array will be populated */ - children({ - Bind: FirstFieldBind, - field, - // "index" contains Bind props for this particular item in the array - // "field" contains Bind props for the main (parent) field. - bind: { - index: bindProps, - field: bindField - }, - index: 0 // Binds to "items.0" in the
. - }) - } - - + + + {typeof renderTitle === "function" && renderTitle(bindFieldValue)} + + {/* We always render the first item, for better UX */} + {showLabel && field.label && } - {/* Now we skip the first item, because we already rendered it above, and proceed with all other items. */} - {bindFieldValue.slice(1).map((_, index) => { - /* We simply increase index, and as you type, the appropriate indexes in the parent array will be updated. */ - const realIndex = index + 1; - const BindField = getBind(realIndex); - return ( - - - {bindProps => - children({ - Bind: BindField, + + {bindProps => ( + + {/* We bind it to index "0", so when you start typing, that index in parent array will be populated */} + {children({ + Bind: FirstFieldBind, field, + // "index" contains Bind props for this particular item in the array + // "field" contains Bind props for the main (parent) field. bind: { index: bindProps, field: bindField }, - index: realIndex - }) - } - - - ); - })} + index: 0 // Binds to "items.0" in the . + })} + + )} + + - {bindField.validation.isValid === false && ( - - - {bindField.validation.message} - + {/* Now we skip the first item, because we already rendered it above, and proceed with all other items. */} + {bindFieldValue.slice(1).map((_, index) => { + /* We simply increase index, and as you type, the appropriate indexes in the parent array will be updated. */ + const realIndex = index + 1; + const BindField = getBind(realIndex); + return ( + + + {bindProps => ( + + {children({ + Bind: BindField, + field, + bind: { + index: bindProps, + field: bindField + }, + index: realIndex + })} + + )} + + + ); + })} + + {bindField.validation.isValid === false && ( + + + {bindField.validation.message} + + + )} + + appendValue(emptyValue)} + > + } /> + {t`Add value`} + - )} - - appendValue(emptyValue)} - > - } /> - {t`Add value`} - - - + + ); }} 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 6bb89add9bf..455bccde1af 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 @@ -81,7 +81,7 @@ export const AddTemplateButton = (props: AddTemplateProps) => { - Click here to learn how templates and dynamic zones work + Click here to learn how templates and dynamic zones work. 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 b5e1d655a05..697b0278dd8 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 @@ -8,7 +8,7 @@ import { ReactComponent as ArrowUpIcon } from "@material-design-icons/svg/round/ import { ReactComponent as ArrowDownIcon } from "@material-design-icons/svg/round/expand_more.svg"; import { AddTemplateButton, AddTemplateIcon } from "./AddTemplate"; import { TemplateIcon } from "./TemplateIcon"; -import { useModelField } from "~/admin/hooks"; +import { ParentFieldProvider, useModelField } from "~/admin/hooks"; import { Fields } from "~/admin/components/ContentEntryForm/Fields"; import { BindComponent, @@ -188,25 +188,38 @@ export const MultiValueDynamicZone = (props: MultiValueDynamicZoneProps) => { const values: TemplateValue[] = bind.value || []; const hasValues = values.length > 0; + const Bind = getBind(); + return ( <> {hasValues ? ( - - {values.map((value, index) => ( - bind.moveValueUp(index)} - onMoveDown={() => bind.moveValueDown(index)} - onDelete={() => bind.removeValue(index)} - onClone={() => cloneValue(index)} - /> - ))} - + + + {values.map((value, index) => { + const Bind = getBind(index); + + return ( + + bind.moveValueUp(index)} + onMoveDown={() => bind.moveValueDown(index)} + onDelete={() => bind.removeValue(index)} + onClone={() => cloneValue(index)} + /> + + ); + })} + + ) : null} {hasValues ? ( 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 42160360a48..a91400b3607 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 @@ -11,6 +11,7 @@ import { CmsModel, CmsModelField } from "~/types"; +import { ParentFieldProvider } from "~/admin/components/ContentEntryForm/ParentValue"; type GetBind = CmsModelFieldRendererProps["getBind"]; @@ -46,27 +47,32 @@ export const SingleValueDynamicZone = ({ return ( <> {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/TemplateGallery.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGallery.tsx index c37eb7fd471..6de5e4fa6a2 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGallery.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGallery.tsx @@ -1,37 +1,95 @@ import React from "react"; import styled from "@emotion/styled"; import { ReactComponent as CloseIcon } from "@material-design-icons/svg/outlined/highlight_off.svg"; +import { createDecoratorFactory, DecoratableComponent, makeDecoratable } from "@webiny/app-admin"; import { IconButton } from "@webiny/ui/Button"; import { CmsDynamicZoneTemplate } from "~/types"; -import { useModelField } from "~/admin/hooks"; -import { TemplateCard } from "./TemplateCard"; +import { useModel, useModelField } from "~/admin/hooks"; +import { TemplateItem } from "./TemplateItem"; -const GalleryContainer = styled.div``; +export interface TemplateGalleryContainerProps { + children: React.ReactNode; +} + +const GalleryContainer = makeDecoratable( + "TemplateGalleryContainer", + (props: TemplateGalleryContainerProps) => { + return <>{props.children}; + } +); -const GalleryCards = styled.div` +const DefaultGalleryList = styled.div` display: flex; flex-wrap: wrap; justify-content: center; padding: 10px; `; -interface TemplateGalleryProps { +export interface TemplateGalleryListProps { + children: React.ReactNode; +} + +const GalleryList = makeDecoratable("TemplateGalleryList", (props: TemplateGalleryListProps) => { + return {props.children}; +}); + +export interface CloseGalleryProps { + onClose: () => void; +} + +const CloseGallery = makeDecoratable("TemplateGalleryClose", (props: CloseGalleryProps) => { + return } />; +}); + +export interface TemplateGalleryProps { onTemplate: (template: CmsDynamicZoneTemplate) => void; onClose: () => void; + templates?: CmsDynamicZoneTemplate[]; } -export const TemplateGallery = ({ onTemplate, onClose }: TemplateGalleryProps) => { +const Gallery = makeDecoratable("TemplateGalley", (props: TemplateGalleryProps) => { const { field } = useModelField(); - const templates = field.settings?.templates || []; + const templates = props.templates || field.settings?.templates || []; return ( - + {templates.map(template => ( - + ))} - - } /> + + ); -}; +}); + +function withDecoratorFactory(Component: T) { + return Object.assign(Component, { + createDecorator: createDecoratorFactory<{ modelIds?: string[] }>()( + Component, + decoratorProps => { + const { model } = useModel(); + + if ( + decoratorProps?.modelIds?.length && + !decoratorProps.modelIds.includes(model.modelId) + ) { + return false; + } + + return true; + } + ) + }); +} + +export const TemplateGallery = Object.assign(withDecoratorFactory(Gallery), { + Container: withDecoratorFactory(GalleryContainer), + List: withDecoratorFactory(GalleryList), + Item: withDecoratorFactory(TemplateItem), + Close: withDecoratorFactory(CloseGallery) +}); diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateCard.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateItem.tsx similarity index 58% rename from packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateCard.tsx rename to packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateItem.tsx index 95a18ced7a2..b1ea8863c06 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateCard.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateItem.tsx @@ -1,9 +1,9 @@ import React from "react"; import styled from "@emotion/styled"; - +import { makeDecoratable } from "@webiny/app-admin"; import { Typography } from "@webiny/ui/Typography"; -import { CmsDynamicZoneTemplate } from "~/types"; import { ButtonSecondary } from "@webiny/ui/Button"; +import { CmsDynamicZoneTemplate } from "~/types"; import { TemplateIcon } from "~/admin/plugins/fieldRenderers/dynamicZone/TemplateIcon"; const CardContainer = styled.div` @@ -47,26 +47,29 @@ const CardButton = styled(ButtonSecondary)` margin-top: auto; `; -interface TemplateCardProps { +export interface TemplateCardProps { template: CmsDynamicZoneTemplate; onTemplate: (template: CmsDynamicZoneTemplate) => void; } -export const TemplateCard = ({ template, onTemplate }: TemplateCardProps) => { - return ( - - - - - - - {template.name} - - - {template.description} - - onTemplate(template)}>+ Insert Template - - - ); -}; +export const TemplateItem = makeDecoratable( + "TemplateItem", + ({ template, onTemplate }: TemplateCardProps) => { + return ( + + + + + + + {template.name} + + + {template.description} + + onTemplate(template)}>+ Insert Template + + + ); + } +); diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/index.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/index.tsx index 49d08782ef8..81e749f8e3a 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/index.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/index.tsx @@ -1,4 +1,5 @@ export { DynamicZoneContainerProps, DynamicZoneContainer } from "./dynamicZoneRenderer"; +export { TemplateGallery } from "./TemplateGallery"; export { MultiValueItemContainer, MultiValueContainer, diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx index ea4ea85fcd7..3b3d0e36ddd 100644 --- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/object/multipleObjects.tsx @@ -119,7 +119,6 @@ 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 5024115f1e6..2ea0d54b18b 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 @@ -1,9 +1,10 @@ import React from "react"; import { i18n } from "@webiny/app/i18n"; +import { Accordion, AccordionItem } from "@webiny/ui/Accordion"; import { CmsModelFieldRendererPlugin } from "~/types"; import { Fields } from "~/admin/components/ContentEntryForm/Fields"; -import { Accordion, AccordionItem } from "@webiny/ui/Accordion"; import { FieldSettings } from "./FieldSettings"; +import { ParentFieldProvider } from "~/admin/hooks"; const t = i18n.ns("app-headless-cms/admin/fields/text"); @@ -30,16 +31,22 @@ const plugin: CmsModelFieldRendererPlugin = { const settings = fieldSettings.getSettings(); return ( - - - - - + + {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 423dcaae79e..182c9e66700 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 @@ -7,6 +7,7 @@ import { Grid, Cell } from "@webiny/ui/Grid"; import { FormElementMessage } from "@webiny/ui/FormElementMessage"; import { fieldsWrapperStyle } from "./StyledComponents"; import { FieldSettings } from "./FieldSettings"; +import { ParentFieldProvider } from "~/admin/components/ContentEntryForm/ParentValue"; const t = i18n.ns("app-headless-cms/admin/fields/text"); @@ -33,22 +34,28 @@ const plugin: CmsModelFieldRendererPlugin = { const settings = fieldSettings.getSettings(); return ( - - - - {field.helpText && ( - {field.helpText} - )} - - - - - + + {bindProps => ( + + + + + {field.helpText && ( + {field.helpText} + )} + + + + + + + )} + ); } } diff --git a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/TemplateDialog.tsx b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/TemplateDialog.tsx index 17bcb85d9da..3f4555dd033 100644 --- a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/TemplateDialog.tsx +++ b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/TemplateDialog.tsx @@ -9,6 +9,7 @@ import { generateAlphaNumericLowerCaseId } from "@webiny/utils"; import { IconPicker } from "~/admin/components/IconPicker"; import { Dialog, DialogTitle, DialogContent, DialogActions } from "~/admin/components/Dialog"; import { Alert } from "@webiny/ui/Alert"; +import { Tags } from "@webiny/ui/Tags"; const typeNameValidator = (value: string) => { const regex = new RegExp("^[A-Z]+[_0-9A-Za-z]+$"); @@ -153,6 +154,11 @@ export const TemplateDialog = (props: TemplateDialogProps) => { + + + + + diff --git a/packages/app-headless-cms/src/components.ts b/packages/app-headless-cms/src/components.ts index 6a242cef50d..7254243209a 100644 --- a/packages/app-headless-cms/src/components.ts +++ b/packages/app-headless-cms/src/components.ts @@ -2,7 +2,8 @@ import { DynamicZoneContainer, MultiValueContainer, MultiValueItemContainer, - MultiValueItem + MultiValueItem, + TemplateGallery } from "~/admin/plugins/fieldRenderers/dynamicZone"; export const Components = { @@ -18,7 +19,8 @@ export const Components = { Container: MultiValueContainer, ItemContainer: MultiValueItemContainer, Item: MultiValueItem - } + }, + TemplateGallery } } }; diff --git a/packages/app-headless-cms/src/index.tsx b/packages/app-headless-cms/src/index.tsx index 5caab966494..c2c17419c8f 100644 --- a/packages/app-headless-cms/src/index.tsx +++ b/packages/app-headless-cms/src/index.tsx @@ -1,12 +1,15 @@ import React from "react"; -import { ContentEntryEditorConfig, ContentEntryListConfig } from "./admin/config/contentEntries"; +import { + ContentEntryEditorConfig as BaseContentEntryEditorConfig, + ContentEntryListConfig +} from "./admin/config/contentEntries"; export * from "./HeadlessCMS"; export * from "./admin/hooks"; export { LexicalEditorConfig } from "~/admin/lexicalConfig/LexicalEditorConfig"; -export { RenderFieldElement } from "~/admin/components/ContentEntryForm/RenderFieldElement"; +export * from "~/admin/components/ContentEntryForm/FieldElement"; export { ModelProvider } from "~/admin/components/ModelProvider"; -export { ContentEntryEditorConfig, ContentEntryListConfig }; +export { ContentEntryListConfig }; interface LegacyContentEntriesViewConfigProps { children: React.ReactNode; @@ -28,11 +31,18 @@ const LegacyContentEntriesViewConfig = ({ children }: LegacyContentEntriesViewCo const LegacySorter = (props: any) => null; /** - * @deprecated Use ContentEntryListConfig instead + * @deprecated Use ContentEntryListConfig instead. */ export const ContentEntriesViewConfig = Object.assign(LegacyContentEntriesViewConfig, { Filter: ContentEntryListConfig.Browser.Filter, Sorter: LegacySorter }); -export * from "./components"; +import { Components as AllComponents } from "./components"; + +/** + * @deprecated Use `ContentEntryEditorConfig` namespace instead. + */ +export const Components = AllComponents; + +export const ContentEntryEditorConfig = Object.assign(BaseContentEntryEditorConfig, AllComponents); diff --git a/packages/react-composition/src/decorators.tsx b/packages/react-composition/src/decorators.tsx index d5cabb0aeb5..0c0c0b1c5ed 100644 --- a/packages/react-composition/src/decorators.tsx +++ b/packages/react-composition/src/decorators.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Compose } from "~/Compose"; import { GetDecoratee, GetDecorateeParams } from "~/createDecorator"; -import { DecoratableComponent, GenericComponent, GenericHook, Decorator } from "~/types"; +import { DecoratableComponent, GenericComponent, Decorator } from "~/types"; interface ShouldDecorate { (decoratorProps: TDecorator, componentProps: TComponent): boolean; @@ -13,11 +13,12 @@ export function createConditionalDecorator( decoratorProps: unknown ): Decorator { return (Original => { + const DecoratedComponent = decorator(Original); + return function ShouldDecorate(props: unknown) { if (shouldDecorate(decoratorProps, props)) { - const Component = decorator(Original); // @ts-expect-error - return ; + return ; } // @ts-expect-error @@ -26,6 +27,8 @@ export function createConditionalDecorator( }) as Decorator; } +const defaultShouldDecorate = () => true; + export function createDecoratorFactory() { return function from( decoratable: TDecoratable, @@ -33,22 +36,13 @@ export function createDecoratorFactory() { ) { return function createDecorator(decorator: Decorator>) { return function DecoratorPlugin(props: TDecorator) { - if (shouldDecorate) { - const componentDecorator = createConditionalDecorator( - shouldDecorate, - decorator as unknown as Decorator, - props - ); - - return ; - } - - return ( - } - /> + const componentDecorator = createConditionalDecorator( + shouldDecorate || defaultShouldDecorate, + decorator as unknown as Decorator, + props ); + + return ; }; }; }; From 2a32c4d3b4a2e77d51beaf4697a73af3071acedb Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 22 Mar 2024 12:16:31 +0100 Subject: [PATCH 02/24] fix: add support for federated OIDC identity providers (#4052) --- packages/app-admin-cognito/package.json | 4 + .../src/components/Divider.tsx | 5 + .../src/components/FederatedProviders.tsx | 25 +++ .../{views => components}/StyledComponents.ts | 0 .../app-admin-cognito/src/components/View.tsx | 71 ++++++ .../src/federatedIdentityProviders.ts | 18 ++ packages/app-admin-cognito/src/index.tsx | 40 ++-- .../src/views/FederatedLogin.tsx | 52 +++-- .../src/views/ForgotPassword.tsx | 196 ++++++++--------- .../app-admin-cognito/src/views/LoggingIn.tsx | 4 +- .../src/views/RequireNewPassword.tsx | 100 ++++----- .../src/views/SetNewPassword.tsx | 195 +++++++---------- .../app-admin-cognito/src/views/SignIn.tsx | 204 ++++++++++-------- .../app-admin-cognito/src/views/SignedIn.tsx | 4 +- .../src/views/StateContainer.tsx | 18 -- .../src/views/webiny-orange-logo.svg | 23 -- .../app-admin-cognito/tsconfig.build.json | 1 + packages/app-admin-cognito/tsconfig.json | 3 + .../app-admin-users-cognito/src/Cognito.tsx | 38 +--- .../src/CognitoLogin.tsx | 40 ++++ .../createAuthentication.tsx | 4 +- packages/app-admin-users-cognito/src/index.ts | 3 + .../src/plugins/userMenu/accountDetails.tsx | 14 +- .../plugins/userMenu/useIsDefaultTenant.ts | 19 ++ .../src/plugins/userMenu/userInfo.tsx | 130 ++++++----- .../app-cognito-authenticator/src/index.ts | 8 + .../dynamicZone/TemplateGallery.tsx | 42 ++-- .../cognitoIdentityProviders/configure.ts | 3 +- .../cognitoIdentityProviders/getIdpConfig.ts | 4 +- .../core/cognitoIdentityProviders/oidc.ts | 25 +++ .../__tests__/composition.test.tsx | 5 +- .../__tests__/decorators.test.tsx | 69 ++++++ .../react-composition/src/createDecorator.tsx | 18 +- packages/react-composition/src/decorators.tsx | 50 ++++- .../react-composition/src/makeDecoratable.tsx | 37 ++-- yarn.lock | 1 + 36 files changed, 874 insertions(+), 599 deletions(-) create mode 100644 packages/app-admin-cognito/src/components/Divider.tsx create mode 100644 packages/app-admin-cognito/src/components/FederatedProviders.tsx rename packages/app-admin-cognito/src/{views => components}/StyledComponents.ts (100%) create mode 100644 packages/app-admin-cognito/src/components/View.tsx create mode 100644 packages/app-admin-cognito/src/federatedIdentityProviders.ts delete mode 100644 packages/app-admin-cognito/src/views/StateContainer.tsx delete mode 100644 packages/app-admin-cognito/src/views/webiny-orange-logo.svg create mode 100644 packages/app-admin-users-cognito/src/CognitoLogin.tsx create mode 100644 packages/app-admin-users-cognito/src/plugins/userMenu/useIsDefaultTenant.ts create mode 100644 packages/app-cognito-authenticator/src/index.ts create mode 100644 packages/pulumi-aws/src/apps/core/cognitoIdentityProviders/oidc.ts create mode 100644 packages/react-composition/__tests__/decorators.test.tsx diff --git a/packages/app-admin-cognito/package.json b/packages/app-admin-cognito/package.json index 5e1b719c5b0..5007c076e85 100644 --- a/packages/app-admin-cognito/package.json +++ b/packages/app-admin-cognito/package.json @@ -21,6 +21,7 @@ "@webiny/app-security": "0.0.0", "@webiny/form": "0.0.0", "@webiny/plugins": "0.0.0", + "@webiny/react-composition": "0.0.0", "@webiny/ui": "0.0.0", "@webiny/validation": "0.0.0", "apollo-client": "^2.6.10", @@ -54,6 +55,9 @@ }, "adio": { "ignore": { + "dependencies": [ + "@webiny/react-composition" + ], "peerDependencies": [ "react-dom" ] diff --git a/packages/app-admin-cognito/src/components/Divider.tsx b/packages/app-admin-cognito/src/components/Divider.tsx new file mode 100644 index 00000000000..bbeed85530a --- /dev/null +++ b/packages/app-admin-cognito/src/components/Divider.tsx @@ -0,0 +1,5 @@ +import styled from "@emotion/styled"; + +export const Divider = styled.div` + border-top: 1px solid #ececec; +`; diff --git a/packages/app-admin-cognito/src/components/FederatedProviders.tsx b/packages/app-admin-cognito/src/components/FederatedProviders.tsx new file mode 100644 index 00000000000..e40d732bf9e --- /dev/null +++ b/packages/app-admin-cognito/src/components/FederatedProviders.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { makeDecoratable } from "@webiny/app-admin"; + +const FlexContainer = styled.div` + display: flex; + column-gap: 10px; + padding-top: 20px; + justify-content: center; +`; + +export interface ContainerProps { + children: React.ReactNode; +} + +export const Container = makeDecoratable( + "FederatedProvidersContainer", + ({ children }: ContainerProps) => { + return {children}; + } +); + +export const FederatedProviders = { + Container +}; diff --git a/packages/app-admin-cognito/src/views/StyledComponents.ts b/packages/app-admin-cognito/src/components/StyledComponents.ts similarity index 100% rename from packages/app-admin-cognito/src/views/StyledComponents.ts rename to packages/app-admin-cognito/src/components/StyledComponents.ts diff --git a/packages/app-admin-cognito/src/components/View.tsx b/packages/app-admin-cognito/src/components/View.tsx new file mode 100644 index 00000000000..2e8d9c1c6da --- /dev/null +++ b/packages/app-admin-cognito/src/components/View.tsx @@ -0,0 +1,71 @@ +import * as React from "react"; +import { Logo, makeDecoratable } from "@webiny/app-admin"; +import * as Styled from "./StyledComponents"; +import { Elevation } from "@webiny/ui/Elevation"; +import { Cell, Grid } from "@webiny/ui/Grid"; +import { Typography } from "@webiny/ui/Typography"; + +export interface ContainerProps { + children: React.ReactNode; +} + +const Container = makeDecoratable("ViewContainer", ({ children }: ContainerProps) => ( + + + + + {children} + +)); + +export interface ContentProps { + children: React.ReactNode; +} + +const Content = makeDecoratable("ViewContent", ({ children }: ContentProps) => ( + + {children} + +)); + +export interface FooterProps { + children: React.ReactNode; +} + +const Footer = makeDecoratable("ViewFooter", ({ children }: FooterProps) => { + return ( + + + {children} + + + ); +}); + +export interface TitleProps { + title: string; + description?: React.ReactNode; +} + +const Title = makeDecoratable("ViewTitle", ({ title, description }: TitleProps) => { + return ( + +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +}); + +export const View = { + Container, + Logo, + Content, + Title, + Footer +}; diff --git a/packages/app-admin-cognito/src/federatedIdentityProviders.ts b/packages/app-admin-cognito/src/federatedIdentityProviders.ts new file mode 100644 index 00000000000..04f2e2eaa8f --- /dev/null +++ b/packages/app-admin-cognito/src/federatedIdentityProviders.ts @@ -0,0 +1,18 @@ +import React from "react"; + +export const federatedIdentityProviders: Record = { + Cognito: "COGNITO", + Google: "Google", + Facebook: "Facebook", + Amazon: "LoginWithAmazon", + Apple: "SignInWithApple" +}; + +export interface SignInProps { + signIn: () => void; +} + +export type FederatedIdentityProvider = { + name: string; + component?: React.FunctionComponent; +}; diff --git a/packages/app-admin-cognito/src/index.tsx b/packages/app-admin-cognito/src/index.tsx index 943b83b4cc1..d92ad083b48 100644 --- a/packages/app-admin-cognito/src/index.tsx +++ b/packages/app-admin-cognito/src/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { Auth } from "@aws-amplify/auth"; -import { AuthOptions, CognitoHostedUIIdentityProvider } from "@aws-amplify/auth/lib-esm/types"; +import { AuthOptions } from "@aws-amplify/auth/lib-esm/types"; import ApolloClient from "apollo-client"; import { useApolloClient } from "@apollo/react-hooks"; import { setContext } from "apollo-link-context"; @@ -11,12 +11,21 @@ import { CognitoIdToken } from "@webiny/app-cognito-authenticator/types"; import { Authenticator } from "@webiny/app-cognito-authenticator/Authenticator"; import { useSecurity } from "@webiny/app-security"; import { config as appConfig } from "@webiny/app/config"; -import SignIn from "~/views/SignIn"; -import RequireNewPassword from "~/views/RequireNewPassword"; -import ForgotPassword from "~/views/ForgotPassword"; -import SetNewPassword from "~/views/SetNewPassword"; -import SignedIn from "~/views/SignedIn"; -import LoggingIn from "~/views/LoggingIn"; +import { SignIn } from "~/views/SignIn"; +import { RequireNewPassword } from "~/views/RequireNewPassword"; +import { ForgotPassword } from "~/views/ForgotPassword"; +import { SetNewPassword } from "~/views/SetNewPassword"; +import { SignedIn } from "~/views/SignedIn"; +import { LoggingIn } from "~/views/LoggingIn"; +import { FederatedIdentityProvider } from "~/federatedIdentityProviders"; +import { FederatedProviders } from "~/components/FederatedProviders"; +import { View } from "~/components/View"; + +export const Components = { + View, + FederatedProviders, + SignIn +}; const createApolloLinkPlugin = (): ApolloLinkPlugin => { return new ApolloLinkPlugin(() => { @@ -63,15 +72,14 @@ export interface AuthenticationProps { children: React.ReactNode; } -export type CognitoFederatedProvider = keyof typeof CognitoHostedUIIdentityProvider; - export interface AuthenticationFactoryConfig extends AuthOptions { - federatedProviders?: CognitoFederatedProvider[]; - onError?(error: Error): void; - getIdentityData(params: { + allowSignInWithCredentials?: boolean; + federatedProviders?: FederatedIdentityProvider[]; + onError?: (error: Error) => void; + getIdentityData: (params: { client: ApolloClient; payload: { [key: string]: any }; - }): Promise<{ [key: string]: any }>; + }) => Promise<{ [key: string]: any }>; } interface AuthenticationFactory { @@ -79,6 +87,7 @@ interface AuthenticationFactory { } export const createAuthentication: AuthenticationFactory = ({ + allowSignInWithCredentials = true, getIdentityData, onError, ...config @@ -142,7 +151,10 @@ export const createAuthentication: AuthenticationFactory = ({ {loadingIdentity ? ( ) : ( - + )} diff --git a/packages/app-admin-cognito/src/views/FederatedLogin.tsx b/packages/app-admin-cognito/src/views/FederatedLogin.tsx index 72f44a8c72e..e68ef9b1a5f 100644 --- a/packages/app-admin-cognito/src/views/FederatedLogin.tsx +++ b/packages/app-admin-cognito/src/views/FederatedLogin.tsx @@ -1,48 +1,54 @@ import React from "react"; -import styled from "@emotion/styled"; -import { Auth, CognitoHostedUIIdentityProvider } from "@aws-amplify/auth"; +import { Auth } from "@aws-amplify/auth"; +import { CognitoHostedUIIdentityProvider } from "@aws-amplify/auth/lib-esm/types/Auth"; import { FacebookLoginButton, GoogleLoginButton, AppleLoginButton, AmazonLoginButton } from "react-social-login-buttons"; -import { CognitoFederatedProvider } from "~/index"; +import { + federatedIdentityProviders, + FederatedIdentityProvider +} from "~/federatedIdentityProviders"; +import { FederatedProviders } from "~/components/FederatedProviders"; -const federatedButtons = { +const federatedButtons: Record = { Facebook: FacebookLoginButton, Google: GoogleLoginButton, Amazon: AmazonLoginButton, - Apple: AppleLoginButton, - Cognito: () => null + Apple: AppleLoginButton }; -const FederatedContainer = styled.div` - border-top: 1px solid #ececec; - padding-top: 20px; -`; - interface FederatedLoginProps { - providers: CognitoFederatedProvider[]; + providers: FederatedIdentityProvider[]; } export const FederatedLogin = ({ providers }: FederatedLoginProps) => { return ( - - {providers.map(provider => { - const Button = federatedButtons[provider]; + + {providers.map(({ name, component }) => { + const Component = component ?? federatedButtons[name] ?? (() => null); + const cognitoProviderName = federatedIdentityProviders[name] ?? name; + const isCustomProvider = !(name in federatedIdentityProviders); return ( -