canopycms 0.0.67 → 0.0.68-int.101
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/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
2
3
|
import { Paper, Stack, Text } from '@mantine/core';
|
|
3
4
|
import { formatCanopyPath } from '../canopy-path.js';
|
|
5
|
+
import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
|
|
4
6
|
export const InlineGroupField = ({ label, description, fields, value, onChange, renderField, path, }) => {
|
|
5
|
-
|
|
7
|
+
const descriptionBaseId = useId();
|
|
8
|
+
return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", shadow: "xs", ...groupDescriptionProps(descriptionBaseId, description), children: _jsxs(Stack, { gap: "sm", children: [(label || description) && (_jsxs(Stack, { gap: 2, children: [label && (_jsx(Text, { size: "xs", fw: 700, c: "dimmed", children: label })), _jsx(FieldDescription, { baseId: descriptionBaseId, description: description })] })), _jsx(Stack, { gap: "sm", children: fields.map((field) => {
|
|
6
9
|
// Nested inline groups are also transparent — pass the same parent value/onChange
|
|
7
10
|
if (field.type === 'group') {
|
|
8
11
|
const childGroup = field;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import React, { Suspense, useId, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { Text, Textarea } from '@mantine/core';
|
|
3
|
+
import React, { Suspense, useId, useRef, useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { Alert, Button, Group, Text, Textarea } from '@mantine/core';
|
|
5
5
|
import { InsertEntryLink } from './entry-link/index.js';
|
|
6
6
|
import { MdxImageDialog } from './MdxImageDialog.js';
|
|
7
|
-
import { useApiClient } from '../context/index.js';
|
|
7
|
+
import { useApiClient, useAssetContext } from '../context/index.js';
|
|
8
|
+
import { editorImageSrc } from '../media/editor-image-src.js';
|
|
8
9
|
import { uploadAsset } from '../media/upload-asset.js';
|
|
10
|
+
import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
|
|
9
11
|
const MDXEditorLazy = React.lazy(async () => {
|
|
10
|
-
const [{ MDXEditor
|
|
11
|
-
|
|
12
|
+
const [{ MDXEditor, headingsPlugin, listsPlugin, quotePlugin, thematicBreakPlugin, markdownShortcutPlugin, linkPlugin, linkDialogPlugin, imagePlugin, tablePlugin, toolbarPlugin, codeBlockPlugin, codeMirrorPlugin, BoldItalicUnderlineToggles, BlockTypeSelect, ListsToggle, CreateLink, InsertImage, InsertTable, InsertThematicBreak, CodeToggle, InsertCodeBlock, UndoRedo, Separator, insertMarkdown$, usePublisher, useCellValues, saveImage$, closeImageDialog$, imageDialogState$, activeEditor$, $isImageNode,
|
|
13
|
+
// MDXEditor's own lexical instance: lexical keeps the active editor state per module, so
|
|
14
|
+
// a separately resolved copy would throw inside this editor's `read()`.
|
|
15
|
+
lexical: { $getNodeByKey }, }, { mdxJsxPlugins },] = await Promise.all([import('@mdxeditor/editor'), import('./mdx-jsx-support.js')]);
|
|
16
|
+
const EntryLinkToolbarButton = ({ onInsert }) => {
|
|
12
17
|
const insertMarkdown = usePublisher(insertMarkdown$);
|
|
13
|
-
return _jsx(InsertEntryLink, { onInsert: insertMarkdown });
|
|
18
|
+
return (_jsx(InsertEntryLink, { onInsert: (markdown) => onInsert(() => insertMarkdown(markdown), markdown) }));
|
|
14
19
|
};
|
|
15
20
|
/**
|
|
16
21
|
* Bridges mdxeditor's realm cells (reachable only once this lazy chunk has
|
|
@@ -19,13 +24,27 @@ const MDXEditorLazy = React.lazy(async () => {
|
|
|
19
24
|
* `EntryLinkToolbarButton` above.
|
|
20
25
|
*/
|
|
21
26
|
const MdxImageDialogBridge = () => {
|
|
22
|
-
const [state] = useCellValues(imageDialogState$);
|
|
27
|
+
const [state, editor] = useCellValues(imageDialogState$, activeEditor$);
|
|
23
28
|
const saveImage = usePublisher(saveImage$);
|
|
24
29
|
const closeImageDialog = usePublisher(closeImageDialog$);
|
|
25
|
-
|
|
30
|
+
// With an `imagePreviewHandler`, MDXEditor seeds an edit dialog with the src the node was
|
|
31
|
+
// first rendered with, not its current one, so saving after a src change would revert it.
|
|
32
|
+
// Read the node's live src from the same editor `saveImage$` writes through.
|
|
33
|
+
const liveState = React.useMemo(() => {
|
|
34
|
+
if (state.type !== 'editing' || !editor)
|
|
35
|
+
return state;
|
|
36
|
+
const src = editor.getEditorState().read(() => {
|
|
37
|
+
const node = $getNodeByKey(state.nodeKey);
|
|
38
|
+
return $isImageNode(node) ? node.getSrc() : undefined;
|
|
39
|
+
});
|
|
40
|
+
return src === undefined
|
|
41
|
+
? state
|
|
42
|
+
: { ...state, initialValues: { ...state.initialValues, src } };
|
|
43
|
+
}, [state, editor]);
|
|
44
|
+
return _jsx(MdxImageDialog, { state: liveState, onSave: saveImage, onClose: closeImageDialog });
|
|
26
45
|
};
|
|
27
|
-
const WrappedEditor = ({ markdown, onChange, editorRef, imageUploadHandler }) => {
|
|
28
|
-
return (_jsx(MDXEditor, { ref: editorRef, markdown: markdown, onChange: onChange, plugins: [
|
|
46
|
+
const WrappedEditor = ({ markdown, onChange, onError, onInsert, editorRef, imageUploadHandler, imagePreviewHandler, }) => {
|
|
47
|
+
return (_jsx(MDXEditor, { ref: editorRef, markdown: markdown, onChange: onChange, onError: onError, plugins: [
|
|
29
48
|
headingsPlugin(),
|
|
30
49
|
listsPlugin(),
|
|
31
50
|
quotePlugin(),
|
|
@@ -33,8 +52,13 @@ const MDXEditorLazy = React.lazy(async () => {
|
|
|
33
52
|
markdownShortcutPlugin(),
|
|
34
53
|
linkPlugin(),
|
|
35
54
|
linkDialogPlugin(),
|
|
36
|
-
imagePlugin({
|
|
55
|
+
imagePlugin({
|
|
56
|
+
imageUploadHandler,
|
|
57
|
+
imagePreviewHandler,
|
|
58
|
+
ImageDialog: MdxImageDialogBridge,
|
|
59
|
+
}),
|
|
37
60
|
tablePlugin(),
|
|
61
|
+
...mdxJsxPlugins(),
|
|
38
62
|
codeBlockPlugin({ defaultCodeBlockLanguage: '' }),
|
|
39
63
|
codeMirrorPlugin({
|
|
40
64
|
codeBlockLanguages: {
|
|
@@ -53,7 +77,7 @@ const MDXEditorLazy = React.lazy(async () => {
|
|
|
53
77
|
},
|
|
54
78
|
}),
|
|
55
79
|
toolbarPlugin({
|
|
56
|
-
toolbarContents: () => (_jsxs(_Fragment, { children: [_jsx(UndoRedo, {}), _jsx(Separator, {}), _jsx(BoldItalicUnderlineToggles, {}), _jsx(CodeToggle, {}), _jsx(Separator, {}), _jsx(BlockTypeSelect, {}), _jsx(Separator, {}), _jsx(ListsToggle, {}), _jsx(Separator, {}), _jsx(CreateLink, {}), _jsx(EntryLinkToolbarButton, {}), _jsx(InsertImage, {}), _jsx(InsertTable, {}), _jsx(InsertThematicBreak, {}), _jsx(InsertCodeBlock, {})] })),
|
|
80
|
+
toolbarContents: () => (_jsxs(_Fragment, { children: [_jsx(UndoRedo, {}), _jsx(Separator, {}), _jsx(BoldItalicUnderlineToggles, {}), _jsx(CodeToggle, {}), _jsx(Separator, {}), _jsx(BlockTypeSelect, {}), _jsx(Separator, {}), _jsx(ListsToggle, {}), _jsx(Separator, {}), _jsx(CreateLink, {}), _jsx(EntryLinkToolbarButton, { onInsert: onInsert }), _jsx(InsertImage, {}), _jsx(InsertTable, {}), _jsx(InsertThematicBreak, {}), _jsx(InsertCodeBlock, {})] })),
|
|
57
81
|
}),
|
|
58
82
|
], contentEditableClassName: "canopy-mdx-content" }));
|
|
59
83
|
};
|
|
@@ -92,14 +116,31 @@ const EditorContentStyles = () => (_jsx("style", { children: `
|
|
|
92
116
|
.canopy-mdx-content p { margin: 0.75em 0; }
|
|
93
117
|
.canopy-mdx-content a { color: var(--mantine-color-blue-6, #228be6); text-decoration: underline; }
|
|
94
118
|
.canopy-mdx-content img { max-width: 100%; height: auto; }
|
|
119
|
+
.canopy-mdx-content .canopy-mdx-jsx {
|
|
120
|
+
border: 1px dashed var(--mantine-color-gray-4, #ced4da);
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
padding: 2px 8px;
|
|
123
|
+
margin: 0.5em 0;
|
|
124
|
+
}
|
|
125
|
+
.canopy-mdx-content .canopy-mdx-jsx-inline { display: inline-block; margin: 0 2px; }
|
|
126
|
+
.canopy-mdx-content .canopy-mdx-jsx-tag {
|
|
127
|
+
font-family: var(--mantine-font-family-monospace, monospace);
|
|
128
|
+
font-size: 0.8em;
|
|
129
|
+
color: var(--mantine-color-gray-6, #868e96);
|
|
130
|
+
}
|
|
95
131
|
` }));
|
|
96
132
|
const FallbackTextarea = ({ value, onChange, }) => (_jsx(Textarea, { value: value, onChange: (e) => onChange(e.currentTarget.value), placeholder: "Loading markdown editor...", autosize: true, minRows: 6, size: "sm", readOnly: true }));
|
|
97
|
-
export const MarkdownField = ({ id, label, value, onChange, dataCanopyField, }) => {
|
|
133
|
+
export const MarkdownField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
|
|
98
134
|
const generatedId = useId();
|
|
99
135
|
const inputId = id ?? generatedId;
|
|
100
136
|
const editorRef = useRef(null);
|
|
101
137
|
const lastExternalValue = useRef(value);
|
|
138
|
+
const lastRejectedSource = useRef(null);
|
|
139
|
+
const pendingInsert = useRef(null);
|
|
102
140
|
const apiClient = useApiClient();
|
|
141
|
+
const [mode, setMode] = useState({ kind: 'rich' });
|
|
142
|
+
const [rejectedInsert, setRejectedInsert] = useState(null);
|
|
143
|
+
const [editorGeneration, setEditorGeneration] = useState(0);
|
|
103
144
|
// Drives both MDXEditor's drag/drop/paste upload and the custom image
|
|
104
145
|
// dialog's Upload tab via the same presign/finalize-or-proxied pipeline
|
|
105
146
|
// (media/upload-asset.ts). Memoized on the API client (itself memoized by
|
|
@@ -108,16 +149,70 @@ export const MarkdownField = ({ id, label, value, onChange, dataCanopyField, })
|
|
|
108
149
|
const asset = await uploadAsset(apiClient, file);
|
|
109
150
|
return asset.src;
|
|
110
151
|
}, [apiClient]);
|
|
111
|
-
//
|
|
152
|
+
// MDXEditor displays each body image at whatever this resolves; the markdown keeps its src.
|
|
153
|
+
const { baseUrl: assetBaseUrl } = useAssetContext();
|
|
154
|
+
const imagePreviewHandler = useCallback(async (src) => editorImageSrc(src, assetBaseUrl), [assetBaseUrl]);
|
|
155
|
+
// Sync external value changes into the editor, recording them even while none
|
|
156
|
+
// is mounted, so a later return to an earlier value is still a change.
|
|
112
157
|
useEffect(() => {
|
|
113
|
-
if (value
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
if (value === lastExternalValue.current)
|
|
159
|
+
return;
|
|
160
|
+
lastExternalValue.current = value;
|
|
161
|
+
editorRef.current?.setMarkdown(value);
|
|
117
162
|
}, [value]);
|
|
118
|
-
const
|
|
163
|
+
const emitChange = useCallback((newValue) => {
|
|
119
164
|
lastExternalValue.current = newValue;
|
|
165
|
+
setRejectedInsert(null);
|
|
120
166
|
onChange(newValue);
|
|
121
167
|
}, [onChange]);
|
|
122
|
-
|
|
168
|
+
// Dropped: MDXEditor's re-serialization of the document it was mounted with
|
|
169
|
+
// (flagged `initialMarkdownNormalize`; it would mark an unedited entry
|
|
170
|
+
// modified), and inserted markdown it rejected, which it emits as if that
|
|
171
|
+
// snippet were the whole document.
|
|
172
|
+
const handleEditorChange = useCallback((newValue, initialMarkdownNormalize) => {
|
|
173
|
+
if (initialMarkdownNormalize || newValue === lastRejectedSource.current)
|
|
174
|
+
return;
|
|
175
|
+
emitChange(newValue);
|
|
176
|
+
}, [emitChange]);
|
|
177
|
+
// MDXEditor reports a rejected insert synchronously, inside insertMarkdown
|
|
178
|
+
// (with no selection it imports nothing, and reports nothing).
|
|
179
|
+
const handleInsert = useCallback((insert, markdown) => {
|
|
180
|
+
pendingInsert.current = markdown;
|
|
181
|
+
try {
|
|
182
|
+
insert();
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
pendingInsert.current = null;
|
|
186
|
+
}
|
|
187
|
+
}, []);
|
|
188
|
+
// MDXEditor can report the error while rendering (it imports as it is
|
|
189
|
+
// created), hence the microtask. Any rejection outside an insert is the
|
|
190
|
+
// document, this render's `value`: MDXEditor is created from it, and gets
|
|
191
|
+
// this handler again before the sync effect hands it a later value. `source`
|
|
192
|
+
// can't identify it, because MDXEditor trims the document it mounts with.
|
|
193
|
+
// Either way MDXEditor emits no further edits until an import succeeds, so a
|
|
194
|
+
// rejected insert remounts it from the unchanged document.
|
|
195
|
+
const handleEditorError = useCallback(({ error, source }) => {
|
|
196
|
+
lastRejectedSource.current = source;
|
|
197
|
+
const isInsert = pendingInsert.current !== null && source === pendingInsert.current;
|
|
198
|
+
queueMicrotask(() => {
|
|
199
|
+
if (isInsert) {
|
|
200
|
+
setRejectedInsert(error);
|
|
201
|
+
setEditorGeneration((n) => n + 1);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
setMode({ kind: 'source', reason: error, failedValue: value });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}, [value]);
|
|
208
|
+
// Edits in the fallback stay in it: the edited text is no more likely to load.
|
|
209
|
+
const handleSourceChange = useCallback((newValue) => {
|
|
210
|
+
setMode((current) => current.kind === 'source' && current.reason !== null
|
|
211
|
+
? { ...current, failedValue: newValue }
|
|
212
|
+
: current);
|
|
213
|
+
emitChange(newValue);
|
|
214
|
+
}, [emitChange]);
|
|
215
|
+
// A fallback holds only for the value MDXEditor rejected.
|
|
216
|
+
const showSource = mode.kind === 'source' && (mode.reason === null || mode.failedValue === value);
|
|
217
|
+
return (_jsxs("div", { id: inputId, "data-canopy-field": dataCanopyField, className: "canopy-markdown-field", ...groupDescriptionProps(inputId, description), children: [_jsxs(Group, { justify: "space-between", align: "flex-end", mb: 4, wrap: "nowrap", children: [label ? (_jsx(Text, { size: "sm", fw: 500, children: label })) : (_jsx("span", {})), _jsx(Button, { variant: "subtle", size: "compact-xs", "data-testid": "markdown-mode-toggle", onClick: () => setMode(showSource ? { kind: 'rich' } : { kind: 'source', reason: null }), children: showSource ? 'Rich text' : 'Edit source' })] }), _jsx(FieldDescription, { baseId: inputId, description: description }), _jsx(EditorContentStyles, {}), showSource ? (_jsxs(_Fragment, { children: [mode.kind === 'source' && mode.reason !== null && (_jsxs(Alert, { color: "yellow", variant: "light", mb: "xs", "data-testid": "markdown-source-fallback", children: [_jsx(Text, { size: "sm", children: "The rich-text editor can't show this content, so it is open as source. Your edits here are saved as usual." }), _jsx(Text, { size: "xs", c: "dimmed", mt: 4, children: mode.reason })] })), _jsx(Textarea, { value: value, onChange: (e) => handleSourceChange(e.currentTarget.value), "aria-label": label ? `${label} (source)` : 'Markdown source', "data-testid": "markdown-source-editor", autosize: true, minRows: 10, size: "sm", styles: { input: { fontFamily: 'var(--mantine-font-family-monospace)' } } })] })) : (_jsxs("div", { style: editorWrapperStyle, children: [rejectedInsert !== null && (_jsxs(Alert, { color: "yellow", variant: "light", mb: "xs", withCloseButton: true, onClose: () => setRejectedInsert(null), "data-testid": "markdown-insert-rejected", children: [_jsx(Text, { size: "sm", children: "The rich-text editor can't show what was inserted, so nothing was added." }), _jsx(Text, { size: "xs", c: "dimmed", mt: 4, children: rejectedInsert })] })), _jsx(Suspense, { fallback: _jsx(FallbackTextarea, { value: value, onChange: onChange }), children: _jsx(MDXEditorLazy, { markdown: value, onChange: handleEditorChange, onError: handleEditorError, onInsert: handleInsert, editorRef: editorRef, imageUploadHandler: imageUploadHandler, imagePreviewHandler: imagePreviewHandler }, editorGeneration) })] }))] }));
|
|
123
218
|
};
|
|
@@ -27,14 +27,14 @@ const toNumericValue = (raw) => {
|
|
|
27
27
|
* would silently store `0.5` after the second keystroke. See
|
|
28
28
|
* NumberField.test.tsx's keystroke matrix.
|
|
29
29
|
*/
|
|
30
|
-
export const NumberField = ({ id, label, value, onChange, dataCanopyField, }) => {
|
|
30
|
+
export const NumberField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
|
|
31
31
|
const generatedId = useId();
|
|
32
32
|
const inputId = id ?? generatedId;
|
|
33
33
|
const [inputValue, setInputValue] = useState(value ?? '');
|
|
34
34
|
// Shows the in-progress text only while it still matches the parent's
|
|
35
35
|
// value; once the parent's value diverges (load, discard, reload), that value wins.
|
|
36
36
|
const displayValue = toNumericValue(inputValue) === value ? inputValue : (value ?? '');
|
|
37
|
-
return (_jsx(NumberInput, { id: inputId, label: label, value: displayValue, size: "sm", onChange: (next) => {
|
|
37
|
+
return (_jsx(NumberInput, { id: inputId, label: label, description: description, value: displayValue, size: "sm", onChange: (next) => {
|
|
38
38
|
setInputValue(next);
|
|
39
39
|
onChange(toNumericValue(next));
|
|
40
40
|
}, "data-canopy-field": dataCanopyField }));
|
|
@@ -27,11 +27,11 @@ const parseTag = (tag) => {
|
|
|
27
27
|
* on Enter indistinguishably from being accepted, with nothing to tell a
|
|
28
28
|
* typo from a broken field — so rejected text is named in the error slot.
|
|
29
29
|
*/
|
|
30
|
-
export const NumberListField = ({ id, label, value, onChange, dataCanopyField, }) => {
|
|
30
|
+
export const NumberListField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
|
|
31
31
|
const generatedId = useId();
|
|
32
32
|
const inputId = id ?? generatedId;
|
|
33
33
|
const [rejected, setRejected] = useState([]);
|
|
34
|
-
return (_jsx(TagsInput, { id: inputId, label: label, value: value.map(String), size: "sm", error: rejected.length > 0
|
|
34
|
+
return (_jsx(TagsInput, { id: inputId, label: label, description: description, value: value.map(String), size: "sm", error: rejected.length > 0
|
|
35
35
|
? `Not ${rejected.length === 1 ? 'a number' : 'numbers'}: ${rejected.join(', ')}`
|
|
36
36
|
: undefined, onChange: (next) => {
|
|
37
37
|
const parsed = [];
|
|
@@ -3,6 +3,7 @@ import type { FieldConfig } from '../../config/index.js';
|
|
|
3
3
|
export type RenderField = (field: FieldConfig, value: unknown, onChange: (v: unknown) => void, path: Array<string | number>) => React.ReactNode;
|
|
4
4
|
export interface ObjectFieldProps {
|
|
5
5
|
label?: string;
|
|
6
|
+
description?: string;
|
|
6
7
|
fields: FieldConfig[];
|
|
7
8
|
value: Record<string, unknown> | undefined;
|
|
8
9
|
onChange: (value: Record<string, unknown>) => void;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
2
3
|
import { Button, Group, Paper, Stack, Text } from '@mantine/core';
|
|
3
4
|
import { formatCanopyPath } from '../canopy-path.js';
|
|
4
|
-
|
|
5
|
+
import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
|
|
6
|
+
export const ObjectField = ({ label, description, fields, value, onChange, renderField, path, dataCanopyField, onRemove, }) => {
|
|
5
7
|
const current = value ?? {};
|
|
6
|
-
|
|
8
|
+
const descriptionBaseId = useId();
|
|
9
|
+
return (_jsx(Paper, { withBorder: true, radius: "md", p: "md", bg: "gray.0", "data-canopy-field": dataCanopyField ?? formatCanopyPath(path), shadow: "xs", ...groupDescriptionProps(descriptionBaseId, description), children: _jsxs(Stack, { gap: "sm", children: [(label || onRemove) && (_jsxs(Group, { justify: "space-between", children: [label && (_jsx(Text, { size: "xs", fw: 700, c: "neutral.8", children: label })), onRemove && (_jsx(Button, { size: "xs", variant: "subtle", color: "red", onClick: onRemove, children: "Clear" }))] })), _jsx(FieldDescription, { baseId: descriptionBaseId, description: description }), _jsx(Stack, { gap: "sm", children: fields.map((field) => {
|
|
7
10
|
const fieldPath = [...path, field.name];
|
|
8
11
|
return (_jsx("div", { children: renderField(field, current[field.name], (next) => onChange({ ...current, [field.name]: next }), fieldPath) }, formatCanopyPath(fieldPath)));
|
|
9
12
|
}) })] }) }));
|
|
@@ -6,7 +6,8 @@ import { IconAlertCircle } from '@tabler/icons-react';
|
|
|
6
6
|
import { createApiClient } from '../../api/client.js';
|
|
7
7
|
import { useOptionalApiClient } from '../context/index.js';
|
|
8
8
|
import { getErrorMessage } from '../../utils/error.js';
|
|
9
|
-
|
|
9
|
+
import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
|
|
10
|
+
export const ReferenceField = ({ id, label, description, options: staticOptions, collections, entryTypes, displayField = 'title', branch = 'main', value, onChange, multiple, dataCanopyField, }) => {
|
|
10
11
|
// Configured with the deployment's basePath when rendered inside an
|
|
11
12
|
// ApiClientProvider (always true in the real Editor tree); `null` outside one
|
|
12
13
|
// (e.g. FormRenderer.stories.tsx), so the fetch effect falls back to a
|
|
@@ -101,10 +102,10 @@ export const ReferenceField = ({ id, label, options: staticOptions, collections,
|
|
|
101
102
|
setRetryCount((count) => count + 1);
|
|
102
103
|
};
|
|
103
104
|
if (loading) {
|
|
104
|
-
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, children: [_jsx(Text, { size: "sm", fw: 500, children: label }), _jsx(Loader, { size: "sm", "data-testid": `reference-loading-${dataCanopyField}` })] }));
|
|
105
|
+
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, ...groupDescriptionProps(inputId, description), children: [_jsx(Text, { size: "sm", fw: 500, children: label }), _jsx(FieldDescription, { baseId: inputId, description: description }), _jsx(Loader, { size: "sm", "data-testid": `reference-loading-${dataCanopyField}` })] }));
|
|
105
106
|
}
|
|
106
107
|
if (error) {
|
|
107
|
-
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, children: [_jsx(Text, { size: "sm", fw: 500, children: label }), _jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Error", "data-testid": `reference-error-${dataCanopyField}`, children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: error }), _jsx(Button, { size: "xs", variant: "light", onClick: handleRetry, "data-testid": `reference-retry-${dataCanopyField}`, children: "Retry" })] }) })] }));
|
|
108
|
+
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, ...groupDescriptionProps(inputId, description), children: [_jsx(Text, { size: "sm", fw: 500, children: label }), _jsx(FieldDescription, { baseId: inputId, description: description }), _jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", title: "Error", "data-testid": `reference-error-${dataCanopyField}`, children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: error }), _jsx(Button, { size: "xs", variant: "light", onClick: handleRetry, "data-testid": `reference-retry-${dataCanopyField}`, children: "Retry" })] }) })] }));
|
|
108
109
|
}
|
|
109
|
-
return (_jsx(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, children: multiple ? (_jsx(MultiSelect, { id: inputId, label: label, data: options, value: normalizedValue, onChange: (next) => onChange(next), searchable: true, placeholder: "Select reference\u2026", size: "sm" })) : (_jsx(Select, { id: inputId, label: label, data: options, value: normalizedValue, onChange: (next) => onChange(next ?? ''), searchable: true, clearable: true, placeholder: "Select reference\u2026", size: "sm" })) }));
|
|
110
|
+
return (_jsx(Stack, { gap: 4, "data-canopy-field": dataCanopyField, "data-testid": `reference-field-${dataCanopyField}`, children: multiple ? (_jsx(MultiSelect, { id: inputId, label: label, description: description, data: options, value: normalizedValue, onChange: (next) => onChange(next), searchable: true, placeholder: "Select reference\u2026", size: "sm" })) : (_jsx(Select, { id: inputId, label: label, description: description, data: options, value: normalizedValue, onChange: (next) => onChange(next ?? ''), searchable: true, clearable: true, placeholder: "Select reference\u2026", size: "sm" })) }));
|
|
110
111
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
3
|
import { MultiSelect, Select, Stack, Text } from '@mantine/core';
|
|
4
|
-
export const SelectField = ({ id, label, options, value, onChange, multiple, placeholder = 'Select…', dataCanopyField, }) => {
|
|
4
|
+
export const SelectField = ({ id, label, description, options, value, onChange, multiple, placeholder = 'Select…', dataCanopyField, }) => {
|
|
5
5
|
const normalizedValue = multiple
|
|
6
6
|
? Array.isArray(value)
|
|
7
7
|
? value
|
|
@@ -11,5 +11,5 @@ export const SelectField = ({ id, label, options, value, onChange, multiple, pla
|
|
|
11
11
|
: '';
|
|
12
12
|
const generatedId = useId();
|
|
13
13
|
const inputId = id ?? generatedId;
|
|
14
|
-
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, children: [multiple ? (_jsx(MultiSelect, { id: inputId, label: label, data: options, value: normalizedValue, onChange: (next) => onChange(next), searchable: true, placeholder: placeholder, size: "sm" })) : (_jsx(Select, { id: inputId, label: label, data: options, value: normalizedValue, onChange: (next) => onChange(next ?? ''), searchable: true, clearable: true, placeholder: placeholder, size: "sm" })), multiple && (_jsx(Text, { size: "xs", c: "dimmed", children: "Searchable multi-select; start typing to filter." }))] }));
|
|
14
|
+
return (_jsxs(Stack, { gap: 4, "data-canopy-field": dataCanopyField, children: [multiple ? (_jsx(MultiSelect, { id: inputId, label: label, description: description, data: options, value: normalizedValue, onChange: (next) => onChange(next), searchable: true, placeholder: placeholder, size: "sm" })) : (_jsx(Select, { id: inputId, label: label, description: description, data: options, value: normalizedValue, onChange: (next) => onChange(next ?? ''), searchable: true, clearable: true, placeholder: placeholder, size: "sm" })), multiple && (_jsx(Text, { size: "xs", c: "dimmed", children: "Searchable multi-select; start typing to filter." }))] }));
|
|
15
15
|
};
|
|
@@ -7,10 +7,10 @@ import { TagsInput } from '@mantine/core';
|
|
|
7
7
|
* Uses Mantine's TagsInput: type + Enter adds an item, each item renders as a
|
|
8
8
|
* removable pill, and Backspace on an empty input removes the last item.
|
|
9
9
|
*/
|
|
10
|
-
export const StringListField = ({ id, label, value, onChange, dataCanopyField, }) => {
|
|
10
|
+
export const StringListField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
|
|
11
11
|
const generatedId = useId();
|
|
12
12
|
const inputId = id ?? generatedId;
|
|
13
|
-
return (_jsx(TagsInput, { id: inputId, label: label, value: value, size: "sm", onChange: onChange,
|
|
13
|
+
return (_jsx(TagsInput, { id: inputId, label: label, description: description, value: value, size: "sm", onChange: onChange,
|
|
14
14
|
// Faithful generic-list semantics, not tag ergonomics: no comma-splitting
|
|
15
15
|
// ("New York, NY" stays one item) and duplicates are legitimate list
|
|
16
16
|
// data — TagsInput's defaults would break both, so existing file data
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
3
|
import { TextInput } from '@mantine/core';
|
|
4
|
-
export const TextField = ({ id, label, value, onChange, dataCanopyField, }) => {
|
|
4
|
+
export const TextField = ({ id, label, description, value, onChange, dataCanopyField, }) => {
|
|
5
5
|
const generatedId = useId();
|
|
6
6
|
const inputId = id ?? generatedId;
|
|
7
|
-
return (_jsx(TextInput, { id: inputId, label: label, value: value, size: "sm", onChange: (e) => onChange(e.currentTarget.value), "data-canopy-field": dataCanopyField }));
|
|
7
|
+
return (_jsx(TextInput, { id: inputId, label: label, description: description, value: value, size: "sm", onChange: (e) => onChange(e.currentTarget.value), "data-canopy-field": dataCanopyField }));
|
|
8
8
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
|
-
import { Switch } from '@mantine/core';
|
|
4
|
-
|
|
3
|
+
import { Stack, Switch } from '@mantine/core';
|
|
4
|
+
import { FieldDescription, fieldDescriptionId } from './FieldDescription.js';
|
|
5
|
+
export const ToggleField = ({ id, label, description, value, onChange, dataCanopyField, testId, }) => {
|
|
5
6
|
const generatedId = useId();
|
|
6
7
|
const inputId = id ?? generatedId;
|
|
7
|
-
|
|
8
|
+
// A Switch's `description` sits inside its <label> and so joins the accessible name;
|
|
9
|
+
// rendering it as a sibling keeps the name to the label alone.
|
|
10
|
+
return (_jsxs(Stack, { gap: 4, children: [_jsx(Switch, { id: inputId, label: label, "aria-describedby": description ? fieldDescriptionId(inputId) : undefined, checked: value, onChange: (e) => onChange(e.currentTarget.checked), size: "md", "data-canopy-field": dataCanopyField, wrapperProps: testId ? { 'data-testid': testId } : undefined }), _jsx(FieldDescription, { baseId: inputId, description: description })] }));
|
|
8
11
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
7
|
interface InsertEntryLinkProps {
|
|
8
|
-
/**
|
|
8
|
+
/** Inserts the link's markdown into the editor (MarkdownField's `handleInsert`). */
|
|
9
9
|
onInsert: (markdown: string) => void;
|
|
10
10
|
}
|
|
11
11
|
export declare const InsertEntryLink: React.FC<InsertEntryLinkProps>;
|
|
@@ -36,8 +36,9 @@ export const InsertEntryLink = ({ onInsert }) => {
|
|
|
36
36
|
}, [filtered]);
|
|
37
37
|
const handleSelect = (entry) => {
|
|
38
38
|
const rawText = entry.label || entry.slug || 'Link';
|
|
39
|
-
// Escape backslashes first, then
|
|
40
|
-
|
|
39
|
+
// Escape backslashes first, then characters that end the link text or that
|
|
40
|
+
// the editor parses as MDX (`{` opens an expression, `<` a JSX tag).
|
|
41
|
+
const linkText = rawText.replace(/\\/g, '\\\\').replace(/([[\]()<>{}])/g, '\\$1');
|
|
41
42
|
const markdown = `[${linkText}](entry:${entry.contentId})`;
|
|
42
43
|
onInsert(markdown);
|
|
43
44
|
setOpened(false);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mdxJsxPlugins(): import("@mdxeditor/editor").RealmPlugin[];
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { NestedLexicalEditor, UnrecognizedMarkdownConstructError, addImportVisitor$, importVisitors$, isMdastHTMLNode, isMdastJsxNode, jsxPlugin, realmPlugin, } from '@mdxeditor/editor';
|
|
4
|
+
function childrenOf(node) {
|
|
5
|
+
return 'children' in node && Array.isArray(node.children) ? node.children : [];
|
|
6
|
+
}
|
|
7
|
+
function describeAttribute(attribute) {
|
|
8
|
+
if (attribute.type === 'mdxJsxExpressionAttribute')
|
|
9
|
+
return '{…}';
|
|
10
|
+
if (attribute.value === null || attribute.value === undefined)
|
|
11
|
+
return attribute.name;
|
|
12
|
+
if (typeof attribute.value === 'string')
|
|
13
|
+
return `${attribute.name}="${attribute.value}"`;
|
|
14
|
+
return `${attribute.name}={…}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Shows an element's tag and attributes read-only (they are edited as source)
|
|
18
|
+
* and edits its children, inline or block per the parsed node: one `*`
|
|
19
|
+
* descriptor serves both, and a block editor rejects inline children.
|
|
20
|
+
*/
|
|
21
|
+
const CatchAllJsxEditor = ({ mdastNode }) => {
|
|
22
|
+
const inline = mdastNode.type === 'mdxJsxTextElement';
|
|
23
|
+
const tag = [mdastNode.name ?? '', ...mdastNode.attributes.map(describeAttribute)]
|
|
24
|
+
.join(' ')
|
|
25
|
+
.trim();
|
|
26
|
+
const Wrapper = inline ? 'span' : 'div';
|
|
27
|
+
return (_jsxs(Wrapper, { className: inline ? 'canopy-mdx-jsx canopy-mdx-jsx-inline' : 'canopy-mdx-jsx', children: [_jsx("span", { className: "canopy-mdx-jsx-tag", "data-testid": "mdx-jsx-tag", title: tag, children: `<${tag}>` }), mdastNode.children.length > 0 ? (_jsx(NestedLexicalEditor, { block: !inline, getContent: (node) => node.children, getUpdatedMdastNode: (node, children) => ({ ...node, children }) })) : null] }));
|
|
28
|
+
};
|
|
29
|
+
const catchAllJsxDescriptor = {
|
|
30
|
+
name: '*',
|
|
31
|
+
kind: 'flow',
|
|
32
|
+
props: [],
|
|
33
|
+
hasChildren: true,
|
|
34
|
+
Editor: CatchAllJsxEditor,
|
|
35
|
+
};
|
|
36
|
+
const isStringAttribute = (attribute, name) => attribute.type === 'mdxJsxAttribute' &&
|
|
37
|
+
attribute.name === name &&
|
|
38
|
+
typeof attribute.value === 'string';
|
|
39
|
+
// MDXEditor's export merges an HTML element's lone `span` child into it, and
|
|
40
|
+
// throws if either one's `className` or `style` is not a string.
|
|
41
|
+
function breaksSpanCollapse(node) {
|
|
42
|
+
const [onlyChild, ...rest] = node.children;
|
|
43
|
+
if (rest.length > 0 || onlyChild === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
if (onlyChild.type !== 'mdxJsxTextElement' || onlyChild.name !== 'span')
|
|
46
|
+
return false;
|
|
47
|
+
return ['className', 'style'].some((name) => {
|
|
48
|
+
const own = node.attributes.find((a) => a.type === 'mdxJsxAttribute' && a.name === name);
|
|
49
|
+
const child = onlyChild.attributes.find((a) => a.type === 'mdxJsxAttribute' && a.name === name);
|
|
50
|
+
return (own !== undefined &&
|
|
51
|
+
child !== undefined &&
|
|
52
|
+
!(isStringAttribute(own, name) && isStringAttribute(child, name)));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* MDXEditor's image plugin drops an `<img>` with no `src`, keeps only string
|
|
57
|
+
* values, lowercases the attribute names it does not model, and parses width
|
|
58
|
+
* and height as integers.
|
|
59
|
+
*/
|
|
60
|
+
function roundTripsImage(node) {
|
|
61
|
+
return (node.attributes.some((a) => isStringAttribute(a, 'src') && a.value !== '') &&
|
|
62
|
+
node.attributes.every((attribute) => {
|
|
63
|
+
if (attribute.type !== 'mdxJsxAttribute' || typeof attribute.value !== 'string')
|
|
64
|
+
return false;
|
|
65
|
+
if (attribute.name === 'width' || attribute.name === 'height') {
|
|
66
|
+
return /^\d+$/.test(attribute.value);
|
|
67
|
+
}
|
|
68
|
+
return attribute.name === attribute.name.toLowerCase();
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
// Why MDXEditor cannot round-trip this JSX element, or null if it can. A
|
|
72
|
+
// fragment has no name, which MDXEditor's HTML handling throws on.
|
|
73
|
+
function unsupportedJsx(node) {
|
|
74
|
+
if (!isMdastJsxNode(node))
|
|
75
|
+
return null;
|
|
76
|
+
if (node.name === null)
|
|
77
|
+
return 'fragments (<>…</>)';
|
|
78
|
+
// `img` is the image plugin's, not in MDXEditor's HTML tag list.
|
|
79
|
+
if (node.name === 'img')
|
|
80
|
+
return roundTripsImage(node) ? null : 'this <img>';
|
|
81
|
+
if (isMdastHTMLNode(node) && breaksSpanCollapse(node)) {
|
|
82
|
+
return `<${node.name}> wrapping a <span>, with an {expression} class or style`;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Typed `string` because the mdast node union this package resolves omits the ESM node. */
|
|
87
|
+
const ESM_NODE_TYPE = 'mdxjsEsm';
|
|
88
|
+
/** Node types the table visitor imports itself, so they have no visitor of their own. */
|
|
89
|
+
const CONSUMED_BY_PARENT_VISITOR = new Set(['tableRow', 'tableCell']);
|
|
90
|
+
/**
|
|
91
|
+
* Reports through `onError`, at import, what MDXEditor would otherwise lose
|
|
92
|
+
* silently: `import`/`export` lines (its visitor for them is a no-op), elements
|
|
93
|
+
* `unsupportedJsx` rejects, and content with no visitor inside a JSX element or
|
|
94
|
+
* table, whose children nested editors import later, only logging a failure and
|
|
95
|
+
* writing partial children back on edit. It throws an error class MDXEditor's
|
|
96
|
+
* import reports; any other error would crash the editor.
|
|
97
|
+
*/
|
|
98
|
+
const roundTripGuardPlugin = realmPlugin({
|
|
99
|
+
init(realm) {
|
|
100
|
+
const guard = {
|
|
101
|
+
priority: 100,
|
|
102
|
+
testNode: (node) => node.type === ESM_NODE_TYPE || node.type === 'table' || isMdastJsxNode(node),
|
|
103
|
+
visitNode({ mdastNode, descriptors, actions }) {
|
|
104
|
+
if (mdastNode.type === ESM_NODE_TYPE) {
|
|
105
|
+
throw new UnrecognizedMarkdownConstructError('import/export statements cannot be edited in the rich-text editor');
|
|
106
|
+
}
|
|
107
|
+
const reject = (node) => {
|
|
108
|
+
const reason = unsupportedJsx(node);
|
|
109
|
+
if (reason !== null) {
|
|
110
|
+
throw new UnrecognizedMarkdownConstructError(`${reason} cannot be edited in the rich-text editor`);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
reject(mdastNode);
|
|
114
|
+
const visitors = realm.getValue(importVisitors$).filter((visitor) => visitor !== guard);
|
|
115
|
+
const hasVisitor = (node) => visitors.some((visitor) => typeof visitor.testNode === 'string'
|
|
116
|
+
? visitor.testNode === node.type
|
|
117
|
+
: visitor.testNode(node, descriptors));
|
|
118
|
+
const check = (node) => {
|
|
119
|
+
for (const child of childrenOf(node)) {
|
|
120
|
+
reject(child);
|
|
121
|
+
if (!CONSUMED_BY_PARENT_VISITOR.has(child.type) && !hasVisitor(child)) {
|
|
122
|
+
const where = isMdastJsxNode(mdastNode) ? `<${mdastNode.name ?? ''}>` : 'A table';
|
|
123
|
+
throw new UnrecognizedMarkdownConstructError(`${where} contains ${child.type} content the rich-text editor cannot edit`);
|
|
124
|
+
}
|
|
125
|
+
check(child);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
check(mdastNode);
|
|
129
|
+
actions.nextVisitor();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
realm.pub(addImportVisitor$, guard);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
export function mdxJsxPlugins() {
|
|
136
|
+
return [jsxPlugin({ jsxComponentDescriptors: [catchAllJsxDescriptor] }), roundTripGuardPlugin()];
|
|
137
|
+
}
|