Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/late-kids-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@emdash-cms/admin": patch
---

Fixes plugin block defaults so initial values are seeded without overriding later edits.
39 changes: 33 additions & 6 deletions packages/admin/src/components/PortableTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,33 @@ function SlashCommandMenu({
);
}

function getPluginBlockDefaultValues(fields?: Element[]): Record<string, unknown> {
const defaults: Record<string, unknown> = {};

for (const field of fields ?? []) {
const initialValue = "initial_value" in field ? field.initial_value : undefined;
if (initialValue !== undefined) {
defaults[field.action_id] = initialValue;
}
}

return defaults;
}

function buildPluginBlockFormValues(
block: PluginBlockDef | null,
initialValues?: Record<string, unknown>,
): Record<string, unknown> {
const defaults = getPluginBlockDefaultValues(block?.fields);
return initialValues ? { ...defaults, ...initialValues } : defaults;
}

function hasPluginBlockFormData(values: Record<string, unknown>): boolean {
return Object.values(values).some(
(value) => value !== undefined && value !== null && value !== "",
);
}

/**
* Plugin block insertion/editing modal.
* When the block has `fields`, renders Block Kit elements.
Expand All @@ -992,11 +1019,7 @@ function PluginBlockModal({

React.useEffect(() => {
if (block) {
if (initialValues) {
setFormValues({ ...initialValues });
} else {
setFormValues({});
}
setFormValues(buildPluginBlockFormValues(block, initialValues));
if (!block.fields || block.fields.length === 0) {
setTimeout(() => inputRef.current?.focus(), 0);
}
Expand Down Expand Up @@ -1025,7 +1048,7 @@ function PluginBlockModal({
// For simple URL mode, check if the URL is non-empty
// For Block Kit fields, require at least one field to have a value
const canSubmit = hasFields
? Object.values(formValues).some((v) => v !== undefined && v !== null && v !== "")
? hasPluginBlockFormData(formValues)
: typeof formValues.id === "string" && formValues.id.trim().length > 0;

return (
Expand Down Expand Up @@ -1255,6 +1278,10 @@ export type { PluginBlockDef } from "./editor/PluginBlockNode";
// Exported for unit testing (pure functions, no React dependencies)
export { prosemirrorToPortableText as _prosemirrorToPortableText };
export { portableTextToProsemirror as _portableTextToProsemirror };
export {
buildPluginBlockFormValues as _buildPluginBlockFormValues,
hasPluginBlockFormData as _hasPluginBlockFormData,
};

// =============================================================================
// Editor Footer with Writing Metrics
Expand Down
124 changes: 122 additions & 2 deletions packages/admin/tests/editor/PortableTextEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import * as React from "react";
import { describe, it, expect, vi } from "vitest";

import type { PluginBlockDef } from "../../src/components/PortableTextEditor";
import { PortableTextEditor } from "../../src/components/PortableTextEditor";
import {
_buildPluginBlockFormValues,
_hasPluginBlockFormData,
PortableTextEditor,
} from "../../src/components/PortableTextEditor";
import { render } from "../utils/render";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -173,7 +177,123 @@ function textBlock(
}

// =============================================================================
// 1. Portable Text ↔ ProseMirror Conversion (via component)
// 1. Plugin block helpers
// =============================================================================

describe("plugin block helpers", () => {
it("builds form state from field initial_value defaults", () => {
const block: PluginBlockDef = {
type: "readingTime",
pluginId: "reading-time",
label: "Reading Time",
fields: [
{
type: "select",
action_id: "variant",
label: "Style",
options: [
{ label: "Inline", value: "inline" },
{ label: "Compact", value: "compact" },
],
initial_value: "inline",
},
{
type: "toggle",
action_id: "includeHeadings",
label: "Include headings",
initial_value: true,
},
],
};

expect(_buildPluginBlockFormValues(block)).toEqual({
variant: "inline",
includeHeadings: true,
});
expect(_hasPluginBlockFormData(_buildPluginBlockFormValues(block))).toBe(true);
});

it("merges existing block data over defaults when editing", () => {
const block: PluginBlockDef = {
type: "readingTime",
pluginId: "reading-time",
label: "Reading Time",
fields: [
{
type: "select",
action_id: "variant",
label: "Style",
options: [
{ label: "Inline", value: "inline" },
{ label: "Compact", value: "compact" },
],
initial_value: "inline",
},
{
type: "toggle",
action_id: "includeHeadings",
label: "Include headings",
initial_value: true,
},
],
};

expect(
_buildPluginBlockFormValues(block, {
variant: "compact",
customLabel: "Custom label",
includeHeadings: false,
}),
).toEqual({
variant: "compact",
customLabel: "Custom label",
includeHeadings: false,
});
});

it("keeps explicit existing values over defaults", () => {
const block: PluginBlockDef = {
type: "readingTime",
pluginId: "reading-time",
label: "Reading Time",
fields: [
{
type: "number_input",
action_id: "minutes",
label: "Minutes",
initial_value: 5,
},
{
type: "text_input",
action_id: "label",
label: "Label",
initial_value: "Default label",
},
{
type: "toggle",
action_id: "includeHeadings",
label: "Include headings",
initial_value: true,
},
],
};

expect(
_buildPluginBlockFormValues(block, {
minutes: 0,
label: "",
includeHeadings: false,
}),
).toEqual({
minutes: 0,
label: "",
includeHeadings: false,
});
});
});

// =============================================================================
// 2. Portable Text ↔ ProseMirror Conversion (via component)
// =============================================================================

describe("Portable Text ↔ ProseMirror conversion", () => {
Expand Down
Loading