fix: pin Writing toolbar and Entities header, fix Entities form alignment and red hint text, and make Settings category bar horizontal
This commit is contained in:
@@ -9,8 +9,6 @@
|
||||
import React from "react";
|
||||
import type { Entity, FieldDef } from "@pubscript/core";
|
||||
import { ENTITY_FIELDS } from "@pubscript/core";
|
||||
import { Input } from "@pubscript/ui";
|
||||
import { Select } from "@pubscript/ui";
|
||||
|
||||
/** A short display label for an entity, regardless of kind. */
|
||||
export function entityLabel(e: Entity): string {
|
||||
@@ -23,7 +21,9 @@ export function entityLabel(e: Entity): string {
|
||||
/** Read a field value from an entity, regardless of where it is stored. */
|
||||
export function getFieldValue(e: Entity, key: string): string | string[] {
|
||||
if ("fields" in e) {
|
||||
if (key === "name") return e.name;
|
||||
const v = e.fields[key];
|
||||
if (Array.isArray(v)) return v;
|
||||
return typeof v === "string" ? v : "";
|
||||
}
|
||||
const v = (e as unknown as Record<string, unknown>)[key];
|
||||
@@ -34,6 +34,9 @@ export function getFieldValue(e: Entity, key: string): string | string[] {
|
||||
/** Return a copy of `e` with `key` set to `value`. */
|
||||
export function setFieldValue(e: Entity, key: string, value: string | string[]): Entity {
|
||||
if ("fields" in e) {
|
||||
if (key === "name" && typeof value === "string") {
|
||||
return { ...e, name: value };
|
||||
}
|
||||
return { ...e, fields: { ...e.fields, [key]: value } };
|
||||
}
|
||||
return { ...e, [key]: value };
|
||||
@@ -52,69 +55,66 @@ interface EntityFieldsProps {
|
||||
entities?: Entity[];
|
||||
}
|
||||
|
||||
function renderField(
|
||||
function renderFieldControl(
|
||||
field: FieldDef,
|
||||
value: string | string[],
|
||||
onChange: (v: string | string[]) => void,
|
||||
currentEntityId: string,
|
||||
entities?: Entity[],
|
||||
) {
|
||||
const label = (
|
||||
<label className="ps-field-label">
|
||||
{field.label}
|
||||
{field.hint ? <span className="ps-field-hint">{field.hint}</span> : null}
|
||||
</label>
|
||||
);
|
||||
|
||||
switch (field.type) {
|
||||
case "textarea":
|
||||
return (
|
||||
<div className="ps-field-span-2">
|
||||
{label}
|
||||
<textarea
|
||||
className="ps-textarea ps-input"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
className="ps-textarea ps-input"
|
||||
rows={3}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
);
|
||||
case "select":
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
<Select
|
||||
value={typeof value === "string" ? value : ""}
|
||||
options={(field.options ?? []).map((o) => ({ value: o, label: o }))}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
className="ps-select"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
>
|
||||
{(field.options ?? []).map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
case "list":
|
||||
return <TagField value={Array.isArray(value) ? value : []} onChange={onChange} />;
|
||||
case "link": {
|
||||
const opts = (entities ?? [])
|
||||
.filter((e) => e.id !== value)
|
||||
.filter((e) => e.id !== currentEntityId && !("body" in e))
|
||||
.map((e) => ({ value: e.id, label: entityLabel(e) }));
|
||||
return (
|
||||
<div>
|
||||
{label}
|
||||
<Select
|
||||
value={typeof value === "string" ? value : ""}
|
||||
options={[{ value: "", label: "— none —" }, ...opts]}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
className="ps-select"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
>
|
||||
<option value="">— None —</option>
|
||||
{opts.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
case "text":
|
||||
return (
|
||||
<div className="ps-field-span-2">
|
||||
{label}
|
||||
<Input
|
||||
className="ps-input"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="ps-input"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={(ev) => onChange(ev.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -139,25 +139,27 @@ function TagField({
|
||||
|
||||
return (
|
||||
<div className="ps-tag-field">
|
||||
<div className="ps-tag-list">
|
||||
{value.map((t, i) => (
|
||||
<span key={`${t}-${i}`} className="ps-tag-chip">
|
||||
{t}
|
||||
<button
|
||||
type="button"
|
||||
className="ps-tag-remove"
|
||||
aria-label={`Remove ${t}`}
|
||||
onClick={() => onChange(value.filter((_, idx) => idx !== i))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{value.length > 0 && (
|
||||
<div className="ps-tag-list">
|
||||
{value.map((t, i) => (
|
||||
<span key={`${t}-${i}`} className="ps-tag-chip">
|
||||
{t}
|
||||
<button
|
||||
type="button"
|
||||
className="ps-tag-remove"
|
||||
aria-label={`Remove ${t}`}
|
||||
onClick={() => onChange(value.filter((_, idx) => idx !== i))}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className="ps-input"
|
||||
value={draft}
|
||||
placeholder="Add tag + Enter"
|
||||
placeholder="Add item + press Enter"
|
||||
onChange={(ev) => setDraft(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
@@ -173,18 +175,31 @@ function TagField({
|
||||
export function EntityFields({ entity, onChange, entities }: EntityFieldsProps) {
|
||||
const fields = fieldsFor(entity);
|
||||
|
||||
const handleFieldChange = (key: string, val: string | string[]) => {
|
||||
const updated = setFieldValue(entity, key, val);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ps-entity-fields">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="ps-field">
|
||||
{renderField(
|
||||
field,
|
||||
getFieldValue(entity, field.key),
|
||||
(v) => onChange({ [field.key]: v } as Partial<Entity>),
|
||||
entities,
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{fields.map((field) => {
|
||||
const isCompact = field.type === "select" || field.type === "link";
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
className={`ps-field${isCompact ? " ps-field-half" : " ps-field-full"}`}
|
||||
>
|
||||
<label className="ps-field-label">{field.label}</label>
|
||||
{renderFieldControl(
|
||||
field,
|
||||
getFieldValue(entity, field.key),
|
||||
(v) => handleFieldChange(field.key, v),
|
||||
entity.id,
|
||||
entities,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user