@cosmicdrift/kumiko-document-designer 0.388.0 → 0.390.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/feature.js +11 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list-definition.d.ts +3 -1
- package/dist/list-definition.js +10 -3
- package/dist/web/client-plugin.js +6 -2
- package/dist/web/constants.d.ts +1 -0
- package/dist/web/constants.js +1 -0
- package/dist/web/i18n.js +1 -0
- package/dist/web/layout-editor-body.d.ts +3 -14
- package/dist/web/layout-editor-body.js +25 -172
- package/dist/web/layout-editor-center.d.ts +7 -0
- package/dist/web/layout-editor-center.js +23 -0
- package/dist/web/layout-editor-logic.d.ts +97 -0
- package/dist/web/layout-editor-logic.js +143 -0
- package/dist/web/layout-editor-paper.d.ts +4 -0
- package/dist/web/layout-editor-paper.js +24 -0
- package/dist/web/layout-editor.d.ts +3 -1
- package/dist/web/layout-editor.js +7 -3
- package/dist/web/mail-editor-body.d.ts +4 -16
- package/dist/web/mail-editor-body.js +27 -204
- package/dist/web/mail-editor-logic.d.ts +70 -0
- package/dist/web/mail-editor-logic.js +81 -0
- package/dist/web/mail-editor-model.d.ts +105 -0
- package/dist/web/mail-editor-model.js +114 -0
- package/dist/web/mail-editor-render.d.ts +11 -0
- package/dist/web/mail-editor-render.js +51 -0
- package/dist/web/mail-preview-render.d.ts +5 -0
- package/dist/web/mail-preview-render.js +22 -0
- package/dist/web/pdf-editor-actions.d.ts +27 -0
- package/dist/web/pdf-editor-actions.js +146 -0
- package/dist/web/pdf-editor-chrome.d.ts +6 -0
- package/dist/web/pdf-editor-chrome.js +60 -0
- package/dist/web/pdf-editor-context.d.ts +22 -0
- package/dist/web/pdf-editor-context.js +1 -0
- package/dist/web/pdf-editor-draft.d.ts +23 -0
- package/dist/web/pdf-editor-draft.js +33 -0
- package/dist/web/pdf-editor-panes.d.ts +6 -0
- package/dist/web/pdf-editor-panes.js +40 -0
- package/dist/web/pdf-editor-view.d.ts +52 -0
- package/dist/web/pdf-editor-view.js +89 -0
- package/dist/web/pdf-template-editor.d.ts +1 -1
- package/dist/web/pdf-template-editor.js +40 -342
- package/dist/web/use-layout-editor-state.d.ts +62 -0
- package/dist/web/use-layout-editor-state.js +155 -0
- package/dist/web/use-mail-editor-parts.d.ts +42 -0
- package/dist/web/use-mail-editor-parts.js +74 -0
- package/dist/web/use-mail-editor-state.d.ts +89 -0
- package/dist/web/use-mail-editor-state.js +55 -0
- package/dist/web/use-pdf-editor-state.d.ts +77 -0
- package/dist/web/use-pdf-editor-state.js +161 -0
- package/package.json +6 -6
- package/src/changes.json +30 -0
package/dist/feature.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { TEMPLATE_FACETS, templateColumns, templateRowActions } from "./list-definition.js";
|
|
3
|
-
import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
|
|
3
|
+
import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
|
|
4
4
|
import { DOCUMENT_DESIGNER_I18N } from "./web/i18n.js";
|
|
5
5
|
const OVERVIEW_QUERY = "template-resolver:query:overview";
|
|
6
6
|
export function createDocumentDesignerFeature(options = {}) {
|
|
@@ -20,7 +20,7 @@ export function createDocumentDesignerFeature(options = {}) {
|
|
|
20
20
|
query: OVERVIEW_QUERY,
|
|
21
21
|
columns: templateColumns({ platform: false }),
|
|
22
22
|
facets: [...TEMPLATE_FACETS],
|
|
23
|
-
rowActions: templateRowActions(),
|
|
23
|
+
rowActions: templateRowActions({ platform: false }),
|
|
24
24
|
searchable: true,
|
|
25
25
|
searchPlaceholder: i18nKey("documentDesigner.search"),
|
|
26
26
|
pagination: "pages",
|
|
@@ -41,7 +41,7 @@ export function createDocumentDesignerFeature(options = {}) {
|
|
|
41
41
|
query: OVERVIEW_QUERY,
|
|
42
42
|
columns: templateColumns({ platform: true }),
|
|
43
43
|
facets: [...TEMPLATE_FACETS],
|
|
44
|
-
rowActions: templateRowActions(),
|
|
44
|
+
rowActions: templateRowActions({ platform: true }),
|
|
45
45
|
searchable: true,
|
|
46
46
|
searchPlaceholder: i18nKey("documentDesigner.search"),
|
|
47
47
|
pagination: "pages",
|
|
@@ -64,6 +64,14 @@ export function createDocumentDesignerFeature(options = {}) {
|
|
|
64
64
|
dormant: true,
|
|
65
65
|
access: { roles: access.roles(...editorRoles) },
|
|
66
66
|
});
|
|
67
|
+
r.screen({
|
|
68
|
+
id: PLATFORM_LAYOUT_EDITOR_SCREEN_ID,
|
|
69
|
+
type: "custom",
|
|
70
|
+
renderer: { react: { __component: PLATFORM_LAYOUT_EDITOR_SCREEN_ID } },
|
|
71
|
+
description: "documentDesigner.screen.layoutEditor.description",
|
|
72
|
+
dormant: true,
|
|
73
|
+
access: { roles: access.systemAdmin },
|
|
74
|
+
});
|
|
67
75
|
r.screen({
|
|
68
76
|
id: PDF_TEMPLATE_EDITOR_SCREEN_ID,
|
|
69
77
|
type: "custom",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createDocumentDesignerFeature, type DocumentDesignerOptions } from "./feature.ts";
|
|
2
|
-
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./web/constants.ts";
|
|
2
|
+
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./web/constants.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createDocumentDesignerFeature } from "./feature.js";
|
|
2
|
-
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
|
|
2
|
+
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
|
|
@@ -3,4 +3,6 @@ export declare function templateColumns(options: {
|
|
|
3
3
|
readonly platform: boolean;
|
|
4
4
|
}): ListColumnSpec[];
|
|
5
5
|
export declare const TEMPLATE_FACETS: readonly ListFacetSpec[];
|
|
6
|
-
export declare function templateRowActions(
|
|
6
|
+
export declare function templateRowActions(options: {
|
|
7
|
+
readonly platform: boolean;
|
|
8
|
+
}): RowAction[];
|
package/dist/list-definition.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
2
2
|
import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { DocumentDesignerCells, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
|
|
3
|
+
import { DocumentDesignerCells, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
|
|
4
4
|
function cell(component) {
|
|
5
5
|
return { react: { __component: component } };
|
|
6
6
|
}
|
|
@@ -85,6 +85,9 @@ export const TEMPLATE_FACETS = [
|
|
|
85
85
|
],
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
|
+
function isLayoutKind(kind) {
|
|
89
|
+
return kind === "mail-layout" || kind === "pdf-layout";
|
|
90
|
+
}
|
|
88
91
|
const EDITABLE_KINDS = [
|
|
89
92
|
"mail",
|
|
90
93
|
"document-pdf",
|
|
@@ -101,9 +104,13 @@ const RESET_LOCKED_OVERRIDE_ACTION = {
|
|
|
101
104
|
style: "danger",
|
|
102
105
|
visible: { field: "canResetLockedOverride", eq: true },
|
|
103
106
|
};
|
|
104
|
-
|
|
107
|
+
// The screen the row is opened from fixes the layout scope: the platform overview edits the system
|
|
108
|
+
// layout, the tenant overview always the tenant's own, even for a SystemAdmin on a system-origin row.
|
|
109
|
+
export function templateRowActions(options) {
|
|
105
110
|
const editActions = EDITABLE_KINDS.flatMap((kind) => {
|
|
106
|
-
const screen =
|
|
111
|
+
const screen = options.platform && isLayoutKind(kind)
|
|
112
|
+
? PLATFORM_LAYOUT_EDITOR_SCREEN_ID
|
|
113
|
+
: TEMPLATE_EDITOR_SCREENS[kind];
|
|
107
114
|
if (screen === undefined)
|
|
108
115
|
return [];
|
|
109
116
|
return [
|
|
@@ -4,14 +4,17 @@ import { mergeTranslations, translationsByLocaleFromKeys, useNav, } from "@cosmi
|
|
|
4
4
|
import { LoadingState } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
5
|
import { lazy, Suspense } from "react";
|
|
6
6
|
import { createLocalesCell, KindCell, ModifiedCell, OriginCell, OverriddenByCell, StatusCell, TitleCell, UsedByCell, } from "./cells.js";
|
|
7
|
-
import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, } from "./constants.js";
|
|
7
|
+
import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, } from "./constants.js";
|
|
8
8
|
import { documentDesignerTranslations } from "./default-translations.js";
|
|
9
9
|
import { DOCUMENT_DESIGNER_I18N } from "./i18n.js";
|
|
10
10
|
// The editors pull tiptap, MJML preview and the page canvas; load them on first mount.
|
|
11
11
|
const LazyLayoutEditor = lazy(() => import("./layout-editor.js").then((module) => ({ default: module.LayoutEditor })));
|
|
12
12
|
const LazyPdfTemplateEditor = lazy(() => import("./pdf-template-editor.js").then((module) => ({ default: module.PdfTemplateEditor })));
|
|
13
13
|
function LayoutEditorScreen() {
|
|
14
|
-
return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, {}) }));
|
|
14
|
+
return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "tenant" }) }));
|
|
15
|
+
}
|
|
16
|
+
function PlatformLayoutEditorScreen() {
|
|
17
|
+
return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "system" }) }));
|
|
15
18
|
}
|
|
16
19
|
function createLazyMailEditor(appLocales) {
|
|
17
20
|
const LazyMailEditor = lazy(() => import("./mail-editor.js").then((module) => ({
|
|
@@ -47,6 +50,7 @@ export function documentDesignerClient(options = {}) {
|
|
|
47
50
|
},
|
|
48
51
|
components: {
|
|
49
52
|
[LAYOUT_EDITOR_SCREEN_ID]: LayoutEditorScreen,
|
|
53
|
+
[PLATFORM_LAYOUT_EDITOR_SCREEN_ID]: PlatformLayoutEditorScreen,
|
|
50
54
|
[PDF_TEMPLATE_EDITOR_SCREEN_ID]: PdfTemplateEditorScreen,
|
|
51
55
|
[MAIL_EDITOR_SCREEN_ID]: createLazyMailEditor(options.appLocales ?? DEFAULT_TEMPLATE_LOCALES),
|
|
52
56
|
},
|
package/dist/web/constants.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const DOCUMENT_DESIGNER_FEATURE: "document-designer";
|
|
|
2
2
|
export declare const TEMPLATES_SCREEN_ID: "templates";
|
|
3
3
|
export declare const PLATFORM_TEMPLATES_SCREEN_ID: "platform-templates";
|
|
4
4
|
export declare const LAYOUT_EDITOR_SCREEN_ID: "layout-editor";
|
|
5
|
+
export declare const PLATFORM_LAYOUT_EDITOR_SCREEN_ID: "platform-layout-editor";
|
|
5
6
|
export declare const PDF_TEMPLATE_EDITOR_SCREEN_ID: "pdf-template-editor";
|
|
6
7
|
export declare const MAIL_EDITOR_SCREEN_ID: "mail-editor";
|
|
7
8
|
export declare const TEMPLATES_NAV_ID: "templates";
|
package/dist/web/constants.js
CHANGED
|
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_FEATURE = "document-designer";
|
|
|
3
3
|
export const TEMPLATES_SCREEN_ID = "templates";
|
|
4
4
|
export const PLATFORM_TEMPLATES_SCREEN_ID = "platform-templates";
|
|
5
5
|
export const LAYOUT_EDITOR_SCREEN_ID = "layout-editor";
|
|
6
|
+
export const PLATFORM_LAYOUT_EDITOR_SCREEN_ID = "platform-layout-editor";
|
|
6
7
|
export const PDF_TEMPLATE_EDITOR_SCREEN_ID = "pdf-template-editor";
|
|
7
8
|
export const MAIL_EDITOR_SCREEN_ID = "mail-editor";
|
|
8
9
|
export const TEMPLATES_NAV_ID = "templates";
|
package/dist/web/i18n.js
CHANGED
|
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_I18N = {
|
|
|
3
3
|
"screen:templates.title": { en: "Templates", de: "Vorlagen" },
|
|
4
4
|
"screen:platform-templates.title": { en: "System templates", de: "System-Vorlagen" },
|
|
5
5
|
"screen:layout-editor.title": { en: "Layout", de: "Layout" },
|
|
6
|
+
"screen:platform-layout-editor.title": { en: "System layout", de: "System-Layout" },
|
|
6
7
|
"screen:pdf-template-editor.title": { en: "PDF template", de: "PDF-Vorlage" },
|
|
7
8
|
"document-designer:nav.templates": { en: "Templates", de: "Vorlagen" },
|
|
8
9
|
"document-designer:nav.platformTemplates": { en: "System templates", de: "System-Vorlagen" },
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
export declare function LayoutEditorBody({ kind, slug, isSystemAdmin, source, overviewRows, usedByCount, overrides, onKindChange, onReload, }: {
|
|
5
|
-
readonly kind: LayoutKind;
|
|
6
|
-
readonly slug: string;
|
|
7
|
-
readonly isSystemAdmin: boolean;
|
|
8
|
-
readonly source: LayoutSourceView;
|
|
9
|
-
readonly overviewRows: readonly OverviewRowView[];
|
|
10
|
-
readonly usedByCount: number;
|
|
11
|
-
readonly overrides: TenantOverrideCounts | null;
|
|
12
|
-
readonly onKindChange: (kind: LayoutKind) => void;
|
|
13
|
-
readonly onReload: () => Promise<void>;
|
|
14
|
-
}): ReactNode;
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type LayoutEditorBodyProps } from "./use-layout-editor-state.ts";
|
|
3
|
+
export declare function LayoutEditorBody(props: LayoutEditorBodyProps): ReactNode;
|
|
@@ -1,187 +1,40 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { pluralKey } from "@cosmicdrift/kumiko-renderer/headless";
|
|
5
|
-
import { cn, ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer/web";
|
|
6
|
-
import { useState } from "react";
|
|
7
|
-
import { composeLayoutHtml } from "./compose-layout.js";
|
|
8
|
-
import { BRANDING_QUERY_QN } from "./constants.js";
|
|
2
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { ModeSwitch } from "@cosmicdrift/kumiko-renderer/web";
|
|
9
4
|
import { EditorHeader } from "./editor-header.js";
|
|
10
5
|
import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
|
|
11
|
-
import {
|
|
12
|
-
import { appearanceOf, isHexColor, metaToSave, } from "./layout-meta.js";
|
|
6
|
+
import { renderCenter } from "./layout-editor-center.js";
|
|
13
7
|
import { AppearanceRail, BrandingRail, MjmlSourceRail, PlaceholderRail, RendererRail, UsedByRail, } from "./layout-rails.js";
|
|
14
|
-
import { systemLabelMap } from "./mail-draft.js";
|
|
15
8
|
import { OriginBar } from "./origin-bar.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const REGION_EDITOR_IDS = {
|
|
21
|
-
header: "document-designer-region-header",
|
|
22
|
-
signature: "document-designer-region-signature",
|
|
23
|
-
footer: "document-designer-region-footer",
|
|
24
|
-
};
|
|
25
|
-
const PAPER_WIDTH_CLASS = {
|
|
26
|
-
560: "max-w-[560px]",
|
|
27
|
-
600: "max-w-[600px]",
|
|
28
|
-
640: "max-w-[640px]",
|
|
29
|
-
};
|
|
30
|
-
function issueText(t, issue) {
|
|
31
|
-
const translated = t(issue.i18nKey, issue.params);
|
|
32
|
-
return translated === issue.i18nKey ? issue.code : translated;
|
|
9
|
+
import { useLayoutEditorState, } from "./use-layout-editor-state.js";
|
|
10
|
+
function renderLeftRail(props, m) {
|
|
11
|
+
const { branding } = m;
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx(BrandingRail, { data: branding.data, unavailable: branding.error !== null || (branding.data === null && !branding.loading) }), _jsx(PlaceholderRail, { validation: m.checked }), _jsx(RendererRail, { kind: props.kind })] }));
|
|
33
13
|
}
|
|
34
|
-
function
|
|
35
|
-
return (
|
|
36
|
-
a.font === b.font &&
|
|
37
|
-
a.outerBackground === b.outerBackground &&
|
|
38
|
-
a.accentLine === b.accentLine &&
|
|
39
|
-
a.buttonStyle === b.buttonStyle);
|
|
14
|
+
function renderRightRail(props, m) {
|
|
15
|
+
return (_jsxs(_Fragment, { children: [m.isMail && (_jsx(AppearanceRail, { appearance: m.appearance, onChange: m.setAppearance, fromMjml: m.appearanceFromMjml })), m.isMail && !m.mjmlMode && _jsx(MjmlSourceRail, { mjmlSource: props.source.mjmlSource }), _jsx(UsedByRail, { layoutSlug: props.slug, kind: props.kind, rows: props.overviewRows })] }));
|
|
40
16
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
function renderKindSwitch(t, props, m) {
|
|
18
|
+
return (_jsx(ModeSwitch, { value: props.kind, onChange: m.requestKind, ariaLabel: t("documentDesigner.editor.kindSwitch"), testId: "document-designer-kind-switch", options: [
|
|
19
|
+
{ value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
|
|
20
|
+
{ value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
|
|
21
|
+
] }));
|
|
44
22
|
}
|
|
45
|
-
export function LayoutEditorBody(
|
|
23
|
+
export function LayoutEditorBody(props) {
|
|
24
|
+
const { kind, isSystemAdmin, source, overrides, onKindChange, onReload } = props;
|
|
46
25
|
const t = useTranslation();
|
|
47
26
|
const { Banner, Dialog } = usePrimitives();
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const [tab, setTab] = useState(showMjmlTab && source.effectiveFormat === "mjml" ? "mjml" : "draft");
|
|
51
|
-
const [header, setHeader] = useState(source.regions.header);
|
|
52
|
-
const [signature, setSignature] = useState(source.regions.signature);
|
|
53
|
-
const [footer, setFooter] = useState(source.regions.footer);
|
|
54
|
-
const [mjml, setMjml] = useState(source.mjmlSource ?? "");
|
|
55
|
-
const storedAppearance = appearanceOf(source.meta);
|
|
56
|
-
const [appearance, setAppearance] = useState(storedAppearance);
|
|
57
|
-
const [activeRegion, setActiveRegion] = useState("header");
|
|
58
|
-
const [toolbarHost, setToolbarHost] = useState(null);
|
|
59
|
-
const [pendingKind, setPendingKind] = useState(null);
|
|
60
|
-
const [saveError, setSaveError] = useState(null);
|
|
61
|
-
const upsertTenant = useMutation(TemplateResolverHandlers.upsertTenant);
|
|
62
|
-
const upsertSystem = useMutation(TemplateResolverHandlers.upsertSystem);
|
|
63
|
-
const publishSystem = useMutation(TemplateResolverHandlers.publishSystem);
|
|
64
|
-
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
65
|
-
const mjmlMode = tab === "mjml";
|
|
66
|
-
// An mjml layout brings its own look; the appearance fields only drive structured (html) layouts.
|
|
67
|
-
const appearanceFromMjml = source.effectiveFormat === "mjml" || mjmlMode;
|
|
68
|
-
const appearanceEditable = isMail && !appearanceFromMjml;
|
|
69
|
-
const appearanceDirty = appearanceEditable && !sameAppearance(appearance, storedAppearance);
|
|
70
|
-
const appearanceValid = !appearanceEditable || isHexColor(appearance.outerBackground);
|
|
71
|
-
const htmlDirty = header !== source.regions.header ||
|
|
72
|
-
signature !== source.regions.signature ||
|
|
73
|
-
footer !== source.regions.footer ||
|
|
74
|
-
appearanceDirty;
|
|
75
|
-
const dirty = mjmlMode ? mjml !== (source.mjmlSource ?? "") : htmlDirty;
|
|
76
|
-
const content = mjmlMode
|
|
77
|
-
? mjml
|
|
78
|
-
: composeLayoutHtml({ header, signature: isMail ? signature : "", footer });
|
|
79
|
-
const debouncedContent = useDebouncedValue(content, VALIDATE_DEBOUNCE_MS);
|
|
80
|
-
const validation = useQuery(TemplateResolverQueries.validate, {
|
|
81
|
-
slug,
|
|
82
|
-
kind,
|
|
83
|
-
contentFormat: mjmlMode ? "mjml" : "html",
|
|
84
|
-
content: debouncedContent,
|
|
85
|
-
locale: source.locale,
|
|
86
|
-
scope: isSystemAdmin ? "system" : "tenant",
|
|
87
|
-
});
|
|
88
|
-
const checked = validation.data;
|
|
89
|
-
const shownRow = isSystemAdmin ? source.system : (source.tenant ?? source.system);
|
|
90
|
-
const userCount = usedByCount;
|
|
91
|
-
const placeholders = checked?.systemPlaceholders ?? [];
|
|
92
|
-
const placeholderLabels = systemLabelMap(t, placeholders);
|
|
93
|
-
const requestKind = (next) => {
|
|
94
|
-
if (next === kind)
|
|
95
|
-
return;
|
|
96
|
-
if (dirty)
|
|
97
|
-
setPendingKind(next);
|
|
98
|
-
else
|
|
99
|
-
onKindChange(next);
|
|
100
|
-
};
|
|
101
|
-
const save = async (publish) => {
|
|
102
|
-
setSaveError(null);
|
|
103
|
-
const base = {
|
|
104
|
-
slug,
|
|
105
|
-
kind,
|
|
106
|
-
locale: source.locale,
|
|
107
|
-
content,
|
|
108
|
-
contentFormat: mjmlMode ? "mjml" : "html",
|
|
109
|
-
meta: appearanceEditable ? metaToSave(appearance, source.meta) : isMail ? source.meta : {},
|
|
110
|
-
};
|
|
111
|
-
const saved = isSystemAdmin
|
|
112
|
-
? await upsertSystem.mutate(base)
|
|
113
|
-
: await upsertTenant.mutate({ ...base, status: publish ? "active" : "draft" });
|
|
114
|
-
if (!saved.isSuccess) {
|
|
115
|
-
setSaveError(dispatcherErrorText(saved.error, t));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (isSystemAdmin && publish) {
|
|
119
|
-
const published = await publishSystem.mutate({ id: saved.data.id });
|
|
120
|
-
if (!published.isSuccess) {
|
|
121
|
-
setSaveError(dispatcherErrorText(published.error, t));
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
await onReload();
|
|
126
|
-
};
|
|
127
|
-
const busy = upsertTenant.pending || upsertSystem.pending || publishSystem.pending;
|
|
128
|
-
const shownStatus = shownRow === null ? null : asRowStatus(shownRow.status);
|
|
129
|
-
const canSave = dirty && checked?.canSave === true && appearanceValid && !busy;
|
|
130
|
-
const canPublish = checked?.canPublish === true && appearanceValid && (dirty || shownStatus === "draft") && !busy;
|
|
131
|
-
const blockingIssue = checked === null ? undefined : [...checked.issues, ...checked.publishIssues][0];
|
|
132
|
-
const footerMessage = checked === null
|
|
133
|
-
? null
|
|
134
|
-
: !appearanceValid
|
|
135
|
-
? { tone: "bad", text: t("documentDesigner.appearance.backgroundInvalid") }
|
|
136
|
-
: blockingIssue !== undefined
|
|
137
|
-
? { tone: "bad", text: issueText(t, blockingIssue) }
|
|
138
|
-
: {
|
|
139
|
-
tone: "ok",
|
|
140
|
-
text: t(pluralKey(isMail
|
|
141
|
-
? "documentDesigner.editor.footer.okMail"
|
|
142
|
-
: "documentDesigner.editor.footer.okPdf", userCount), { count: userCount }),
|
|
143
|
-
};
|
|
144
|
-
const accentColor = branding.data?.accentColor ?? "";
|
|
145
|
-
const showAccentLine = isMail && appearanceEditable && appearance.accentLine === "branding" && isHexColor(accentColor);
|
|
146
|
-
// A layout region has no per-mail variables, so a steps block there could never show a stage and an images block no photos.
|
|
147
|
-
const blocks = (isMail ? MAIL_DOCUMENT_BLOCKS : PDF_DOCUMENT_BLOCKS).filter((block) => block !== "steps" && block !== "images");
|
|
148
|
-
// The layout's "logo" resource is the branding logo, so the editor shows it instead of a chip.
|
|
149
|
-
const logoFile = branding.data?.logoFile ?? "";
|
|
150
|
-
const resourcePreviews = logoFile === ""
|
|
151
|
-
? undefined
|
|
152
|
-
: {
|
|
153
|
-
logo: {
|
|
154
|
-
src: fileUrl(logoFile),
|
|
155
|
-
caption: isMail ? t("documentDesigner.editor.logoCaption") : "",
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
const regionEditor = (region, value, onChange) => (_jsx(DocumentContentEditor, { id: REGION_EDITOR_IDS[region], value: value, onChange: onChange, variables: placeholders, variableLabels: placeholderLabels, systemVariables: placeholders, readOnly: false, blocks: blocks, ...(resourcePreviews && { resourcePreviews }), chrome: "canvas", toolbarHost: activeRegion === region ? toolbarHost : null }));
|
|
27
|
+
const m = useLayoutEditorState(props, t);
|
|
28
|
+
const { pendingKind, setPendingKind } = m;
|
|
159
29
|
return (_jsxs(EditorFrame, { testId: "document-designer-layout-editor", children: [_jsx(EditorHeader, { title: t(kind === "mail-layout"
|
|
160
30
|
? "documentDesigner.editor.title.mail"
|
|
161
|
-
: "documentDesigner.editor.title.pdf"), stored: shownRow, actions: {
|
|
162
|
-
busy,
|
|
163
|
-
canSave,
|
|
164
|
-
canPublish,
|
|
165
|
-
onSave: () => void save(false),
|
|
166
|
-
onPublish: () => void save(true),
|
|
167
|
-
}, children: _jsx(
|
|
168
|
-
{ value: "mail-layout", label: t("documentDesigner.editor.kind.mail") },
|
|
169
|
-
{ value: "pdf-layout", label: t("documentDesigner.editor.kind.pdf") },
|
|
170
|
-
] }) }), _jsx(OriginBar, { isSystemAdmin: isSystemAdmin, source: source, kind: kind, userCount: userCount, overrides: overrides, onReset: () => void onReload(), onResetError: setSaveError }), _jsx(EditorBody, { left: _jsxs(_Fragment, { children: [_jsx(BrandingRail, { data: branding.data, unavailable: branding.error !== null || (branding.data === null && !branding.loading) }), _jsx(PlaceholderRail, { validation: checked }), _jsx(RendererRail, { kind: kind })] }), center: _jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center border-b bg-background", children: [_jsx("div", { ref: setToolbarHost, className: "min-w-0 flex-1" }), showMjmlTab && (_jsx("div", { className: "shrink-0 px-3 py-1", children: _jsx(ModeSwitch, { value: tab, onChange: setTab, ariaLabel: t("documentDesigner.editor.tabSwitch"), testId: "layout-mjml-tab", options: [
|
|
171
|
-
{ value: "draft", label: t("documentDesigner.editor.tab.draft") },
|
|
172
|
-
{ value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
|
|
173
|
-
] }) }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto px-8 py-6", children: mjmlMode ? (_jsx(TextareaField, { id: "document-designer-mjml-source", name: "mjml", label: t("documentDesigner.editor.mjml.label"), value: mjml, onChange: setMjml, rows: 24, testId: "document-designer-mjml-source" })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [source.effectiveFormat === "mjml" && (_jsx(Banner, { variant: "info", testId: "document-designer-mjml-notice", children: t(isSystemAdmin
|
|
174
|
-
? "documentDesigner.editor.mjmlNotice.platform"
|
|
175
|
-
: "documentDesigner.editor.mjmlNotice.tenant") })), _jsxs("div", { className: cn("mx-auto flex w-full flex-col gap-5 bg-card px-8 py-6 shadow-md", isMail ? PAPER_WIDTH_CLASS[appearance.width] : "min-h-[842px] max-w-[595px]", showAccentLine && "border-t-4 border-t-(--layout-accent)"), ref: (element) => element?.style.setProperty("--layout-accent", accentColor), "data-testid": "document-designer-paper", children: [_jsx(PaperRegion, { label: t("documentDesigner.editor.region.header"), testId: "document-designer-region-header", onFocus: () => setActiveRegion("header"), children: regionEditor("header", header, setHeader) }), _jsxs("div", { className: "rounded-sm border-2 border-dashed border-primary bg-primary/10 px-3 py-9 text-center font-medium text-primary", "data-testid": "document-designer-region-content", children: ["\u229E ", t("documentDesigner.editor.region.content"), _jsx("small", { className: "mt-1 block text-xs font-normal opacity-85", children: t(isMail
|
|
176
|
-
? "documentDesigner.editor.slot.mail"
|
|
177
|
-
: "documentDesigner.editor.slot.pdf") })] }), isMail && (_jsx(PaperRegion, { label: t("documentDesigner.editor.region.signature"), testId: "document-designer-region-signature", onFocus: () => setActiveRegion("signature"), children: regionEditor("signature", signature, setSignature) })), _jsx("div", { className: cn(isMail && "-mx-8 -mb-6 bg-muted px-8 py-4"), children: _jsx(PaperRegion, { label: t("documentDesigner.editor.region.footer"), testId: "document-designer-region-footer", onFocus: () => setActiveRegion("footer"), children: regionEditor("footer", footer, setFooter) }) })] })] })) })] }), right: _jsxs(_Fragment, { children: [isMail && (_jsx(AppearanceRail, { appearance: appearance, onChange: setAppearance, fromMjml: appearanceFromMjml })), isMail && !mjmlMode && _jsx(MjmlSourceRail, { mjmlSource: source.mjmlSource }), _jsx(UsedByRail, { layoutSlug: slug, kind: kind, rows: overviewRows })] }) }), _jsx(EditorFooter, { testIdPrefix: "document-designer", message: footerMessage, saveError: saveError, modifiedAt: null, dirty: dirty, busy: busy, canSave: canSave, canPublish: canPublish, onDiscard: () => {
|
|
178
|
-
setHeader(source.regions.header);
|
|
179
|
-
setSignature(source.regions.signature);
|
|
180
|
-
setFooter(source.regions.footer);
|
|
181
|
-
setMjml(source.mjmlSource ?? "");
|
|
182
|
-
setAppearance(storedAppearance);
|
|
183
|
-
setSaveError(null);
|
|
184
|
-
}, onSave: () => void save(false), onPublish: () => void save(true) }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
|
|
31
|
+
: "documentDesigner.editor.title.pdf"), stored: m.shownRow, actions: {
|
|
32
|
+
busy: m.busy,
|
|
33
|
+
canSave: m.canSave,
|
|
34
|
+
canPublish: m.canPublish,
|
|
35
|
+
onSave: () => void m.save(false),
|
|
36
|
+
onPublish: () => void m.save(true),
|
|
37
|
+
}, children: renderKindSwitch(t, props, m) }), _jsx(OriginBar, { isSystemAdmin: isSystemAdmin, source: source, kind: kind, userCount: m.userCount, overrides: overrides, onReset: () => void onReload(), onResetError: m.setSaveError }), _jsx(EditorBody, { left: renderLeftRail(props, m), center: renderCenter(Banner, t, props, m), right: renderRightRail(props, m) }), _jsx(EditorFooter, { testIdPrefix: "document-designer", message: m.footerMessage, saveError: m.saveError, modifiedAt: null, dirty: m.dirty, busy: m.busy, canSave: m.canSave, canPublish: m.canPublish, onDiscard: m.discard, onSave: () => void m.save(false), onPublish: () => void m.save(true) }), _jsx(Dialog, { open: pendingKind !== null, onOpenChange: (open) => {
|
|
185
38
|
if (!open)
|
|
186
39
|
setPendingKind(null);
|
|
187
40
|
}, title: t("documentDesigner.editor.dirtySwitch.title"), description: t("documentDesigner.editor.dirtySwitch.description"), confirmLabel: t("documentDesigner.editor.dirtySwitch.confirm"), variant: "danger", onConfirm: () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { Translate } from "./layout-editor-logic.ts";
|
|
4
|
+
import type { LayoutEditorBodyProps, LayoutEditorModel } from "./use-layout-editor-state.ts";
|
|
5
|
+
type Banner = ReturnType<typeof usePrimitives>["Banner"];
|
|
6
|
+
export declare function renderCenter(Banner: Banner, t: Translate, props: LayoutEditorBodyProps, m: LayoutEditorModel): ReactNode;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ModeSwitch, TextareaField } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
|
+
import { renderPaper } from "./layout-editor-paper.js";
|
|
4
|
+
function renderToolbarRow(t, m) {
|
|
5
|
+
return (_jsxs("div", { className: "flex items-center border-b bg-background", children: [_jsx("div", { ref: m.setToolbarHost, className: "min-w-0 flex-1" }), m.showMjmlTab && (_jsx("div", { className: "shrink-0 px-3 py-1", children: _jsx(ModeSwitch, { value: m.tab, onChange: m.setTab, ariaLabel: t("documentDesigner.editor.tabSwitch"), testId: "layout-mjml-tab", options: [
|
|
6
|
+
{ value: "draft", label: t("documentDesigner.editor.tab.draft") },
|
|
7
|
+
{ value: "mjml", label: t("documentDesigner.editor.tab.mjml") },
|
|
8
|
+
] }) }))] }));
|
|
9
|
+
}
|
|
10
|
+
function renderMjmlNotice(Banner, t, props) {
|
|
11
|
+
return (_jsx(Banner, { variant: "info", testId: "document-designer-mjml-notice", children: t(props.isSystemAdmin
|
|
12
|
+
? "documentDesigner.editor.mjmlNotice.platform"
|
|
13
|
+
: "documentDesigner.editor.mjmlNotice.tenant") }));
|
|
14
|
+
}
|
|
15
|
+
function renderDraftCanvas(Banner, t, props, m) {
|
|
16
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [props.source.effectiveFormat === "mjml" && renderMjmlNotice(Banner, t, props), renderPaper(t, m)] }));
|
|
17
|
+
}
|
|
18
|
+
function renderMjmlSource(t, m) {
|
|
19
|
+
return (_jsx(TextareaField, { id: "document-designer-mjml-source", name: "mjml", label: t("documentDesigner.editor.mjml.label"), value: m.mjml, onChange: m.setMjml, rows: 24, testId: "document-designer-mjml-source" }));
|
|
20
|
+
}
|
|
21
|
+
export function renderCenter(Banner, t, props, m) {
|
|
22
|
+
return (_jsxs(_Fragment, { children: [renderToolbarRow(t, m), _jsx("div", { className: "min-h-0 flex-1 overflow-auto px-8 py-6", children: m.mjmlMode ? renderMjmlSource(t, m) : renderDraftCanvas(Banner, t, props, m) })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type useMutation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { FooterTone } from "./editor-shell.tsx";
|
|
3
|
+
import { type LayoutAppearance, type LayoutWidth } from "./layout-meta.ts";
|
|
4
|
+
import type { BrandingView, LayoutKind, LayoutSourceView, LayoutValidation, UpsertResponse, ValidationIssue } from "./layout-types.ts";
|
|
5
|
+
export type EditorTab = "draft" | "mjml";
|
|
6
|
+
export type Region = "header" | "signature" | "footer";
|
|
7
|
+
export type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
8
|
+
export declare const REGION_EDITOR_IDS: Readonly<Record<Region, string>>;
|
|
9
|
+
export declare const PAPER_WIDTH_CLASS: Readonly<Record<LayoutWidth, string>>;
|
|
10
|
+
export type FooterMessage = {
|
|
11
|
+
readonly tone: FooterTone;
|
|
12
|
+
readonly text: string;
|
|
13
|
+
} | null;
|
|
14
|
+
export type ResourcePreviews = Readonly<Record<string, {
|
|
15
|
+
readonly src: string;
|
|
16
|
+
readonly caption: string;
|
|
17
|
+
}>>;
|
|
18
|
+
export declare function issueText(t: Translate, issue: ValidationIssue): string;
|
|
19
|
+
export declare function sameAppearance(a: LayoutAppearance, b: LayoutAppearance): boolean;
|
|
20
|
+
export declare function initialTab(showMjmlTab: boolean, source: LayoutSourceView): EditorTab;
|
|
21
|
+
export declare function layoutDirty(input: {
|
|
22
|
+
readonly mjmlMode: boolean;
|
|
23
|
+
readonly mjml: string;
|
|
24
|
+
readonly header: string;
|
|
25
|
+
readonly signature: string;
|
|
26
|
+
readonly footer: string;
|
|
27
|
+
readonly appearance: LayoutAppearance;
|
|
28
|
+
readonly storedAppearance: LayoutAppearance;
|
|
29
|
+
readonly appearanceEditable: boolean;
|
|
30
|
+
readonly source: LayoutSourceView;
|
|
31
|
+
}): boolean;
|
|
32
|
+
export declare function layoutContent(input: {
|
|
33
|
+
readonly mjmlMode: boolean;
|
|
34
|
+
readonly mjml: string;
|
|
35
|
+
readonly header: string;
|
|
36
|
+
readonly signature: string;
|
|
37
|
+
readonly footer: string;
|
|
38
|
+
readonly isMail: boolean;
|
|
39
|
+
}): string;
|
|
40
|
+
export declare function footerMessageOf(input: {
|
|
41
|
+
readonly t: Translate;
|
|
42
|
+
readonly checked: LayoutValidation | null;
|
|
43
|
+
readonly appearanceValid: boolean;
|
|
44
|
+
readonly isMail: boolean;
|
|
45
|
+
readonly userCount: number;
|
|
46
|
+
}): FooterMessage;
|
|
47
|
+
export declare function layoutBlocks(isMail: boolean): ("align" | "button" | "divider" | "image" | "pageBreak" | "repeat" | "table")[];
|
|
48
|
+
export declare function resourcePreviewsOf(branding: BrandingView | null, isMail: boolean, t: Translate): ResourcePreviews | undefined;
|
|
49
|
+
type UpsertMutation = ReturnType<typeof useMutation<UpsertResponse>>;
|
|
50
|
+
type PublishMutation = ReturnType<typeof useMutation>;
|
|
51
|
+
export type SaveLayoutInput = {
|
|
52
|
+
readonly publish: boolean;
|
|
53
|
+
readonly kind: LayoutKind;
|
|
54
|
+
readonly slug: string;
|
|
55
|
+
readonly source: LayoutSourceView;
|
|
56
|
+
readonly content: string;
|
|
57
|
+
readonly mjmlMode: boolean;
|
|
58
|
+
readonly isMail: boolean;
|
|
59
|
+
readonly isSystemAdmin: boolean;
|
|
60
|
+
readonly appearanceEditable: boolean;
|
|
61
|
+
readonly appearance: LayoutAppearance;
|
|
62
|
+
readonly t: Translate;
|
|
63
|
+
readonly upsertTenant: UpsertMutation;
|
|
64
|
+
readonly upsertSystem: UpsertMutation;
|
|
65
|
+
readonly publishSystem: PublishMutation;
|
|
66
|
+
readonly setSaveError: (message: string | null) => void;
|
|
67
|
+
readonly onReload: () => Promise<void>;
|
|
68
|
+
};
|
|
69
|
+
export declare function saveLayout(input: SaveLayoutInput): Promise<void>;
|
|
70
|
+
export declare function accentLineShown(input: {
|
|
71
|
+
readonly isMail: boolean;
|
|
72
|
+
readonly appearanceEditable: boolean;
|
|
73
|
+
readonly appearance: LayoutAppearance;
|
|
74
|
+
readonly accentColor: string;
|
|
75
|
+
}): boolean;
|
|
76
|
+
export declare function appearanceFlags(input: {
|
|
77
|
+
readonly isMail: boolean;
|
|
78
|
+
readonly mjmlMode: boolean;
|
|
79
|
+
readonly source: LayoutSourceView;
|
|
80
|
+
readonly appearance: LayoutAppearance;
|
|
81
|
+
}): {
|
|
82
|
+
appearanceFromMjml: boolean;
|
|
83
|
+
appearanceEditable: boolean;
|
|
84
|
+
appearanceValid: boolean;
|
|
85
|
+
};
|
|
86
|
+
export declare function shownRowOf(isSystemAdmin: boolean, source: LayoutSourceView): import("./layout-types.ts").LayoutSourceRow | null;
|
|
87
|
+
export declare function layoutSaveFlags(input: {
|
|
88
|
+
readonly dirty: boolean;
|
|
89
|
+
readonly checked: LayoutValidation | null;
|
|
90
|
+
readonly appearanceValid: boolean;
|
|
91
|
+
readonly busy: boolean;
|
|
92
|
+
readonly shownRow: ReturnType<typeof shownRowOf>;
|
|
93
|
+
}): {
|
|
94
|
+
canSave: boolean;
|
|
95
|
+
canPublish: boolean;
|
|
96
|
+
};
|
|
97
|
+
export {};
|