Merge pull request #100 from bknd-io/fix/minor-admin-ui-fixes

fix active sidebar active item, added error boundaries for relational form fields, fixed schema diagram for m:n relations
This commit is contained in:
dswbx
2025-03-03 07:14:43 +01:00
committed by GitHub
8 changed files with 100 additions and 35 deletions

View File

@@ -77,7 +77,7 @@ export class JsonSchemaField<
return value; return value;
case "table": case "table":
if (value === null) return null; if (value === null) return null;
return value; return JSON.stringify(value);
case "submit": case "submit":
break; break;
} }

View File

@@ -2,6 +2,7 @@ import React, { Component, type ErrorInfo, type ReactNode } from "react";
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
children: ReactNode; children: ReactNode;
suppressError?: boolean;
fallback?: fallback?:
| (({ error, resetError }: { error: Error; resetError: () => void }) => ReactNode) | (({ error, resetError }: { error: Error; resetError: () => void }) => ReactNode)
| ReactNode; | ReactNode;
@@ -23,31 +24,44 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
} }
override componentDidCatch(error: Error, errorInfo: ErrorInfo) { override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo); const type = this.props.suppressError ? "warn" : "error";
console[type]("ErrorBoundary caught an error:", error, errorInfo);
} }
resetError = () => { resetError = () => {
this.setState({ hasError: false, error: undefined }); this.setState({ hasError: false, error: undefined });
}; };
private renderFallback() {
if (this.props.fallback) {
return typeof this.props.fallback === "function"
? this.props.fallback({ error: this.state.error!, resetError: this.resetError })
: this.props.fallback;
}
return <BaseError>Error</BaseError>;
}
override render() { override render() {
if (this.state.hasError) { if (this.state.hasError) {
return this.props.fallback ? ( return this.renderFallback();
typeof this.props.fallback === "function" ? ( }
this.props.fallback({ error: this.state.error!, resetError: this.resetError })
) : ( if (this.props.suppressError) {
this.props.fallback try {
) return this.props.children;
) : ( } catch (e) {
<div> return this.renderFallback();
<h2>Something went wrong.</h2> }
<button onClick={this.resetError}>Try Again</button>
</div>
);
} }
return this.props.children; return this.props.children;
} }
} }
const BaseError = ({ children }: { children: ReactNode }) => (
<div className="bg-red-700 text-white py-1 px-2 rounded-md leading-none font-mono">
{children}
</div>
);
export default ErrorBoundary; export default ErrorBoundary;

View File

@@ -42,9 +42,16 @@ const useLocationFromRouter = (router) => {
]; ];
}; };
export function isLinkActive(href: string, strict?: boolean) { export function isLinkActive(href: string, strictness?: number) {
const path = window.location.pathname; const path = window.location.pathname;
return strict ? path === href : path.includes(href);
if (!strictness || strictness === 0) {
return path.includes(href);
} else if (strictness === 1) {
return path === href || path.endsWith(href) || path.includes(href + "/");
}
return path === href;
} }
export function Link({ export function Link({

View File

@@ -17,6 +17,8 @@ import { Media } from "ui/elements";
import { useEvent } from "ui/hooks/use-event"; import { useEvent } from "ui/hooks/use-event";
import { EntityJsonSchemaFormField } from "./fields/EntityJsonSchemaFormField"; import { EntityJsonSchemaFormField } from "./fields/EntityJsonSchemaFormField";
import { EntityRelationalFormField } from "./fields/EntityRelationalFormField"; import { EntityRelationalFormField } from "./fields/EntityRelationalFormField";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { Alert } from "ui/components/display/Alert";
type EntityFormProps = { type EntityFormProps = {
entity: Entity; entity: Entity;
@@ -94,20 +96,28 @@ export function EntityForm({
const _key = `${entity.name}-${field.name}-${key}`; const _key = `${entity.name}-${field.name}-${key}`;
return ( return (
<Form.Field <ErrorBoundary
key={_key} key={_key}
name={field.name} fallback={
children={(props) => ( <Alert.Exception className="font-mono">
<EntityFormField Field error: {field.name}
field={field} </Alert.Exception>
fieldApi={props} }
disabled={fieldsDisabled} >
tabIndex={key + 1} <Form.Field
action={action} name={field.name}
data={data} children={(props) => (
/> <EntityFormField
)} field={field}
/> fieldApi={props}
disabled={fieldsDisabled}
tabIndex={key + 1}
action={action}
data={data}
/>
)}
/>
</ErrorBoundary>
); );
})} })}
</div> </div>

View File

@@ -1,5 +1,6 @@
import type { Entity, EntityData } from "data"; import type { Entity, EntityData } from "data";
import { CellValue, DataTable, type DataTableProps } from "ui/components/table/DataTable"; import { CellValue, DataTable, type DataTableProps } from "ui/components/table/DataTable";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
type EntityTableProps<Data extends EntityData = EntityData> = Omit< type EntityTableProps<Data extends EntityData = EntityData> = Omit<
DataTableProps<Data>, DataTableProps<Data>,
@@ -41,7 +42,11 @@ export function EntityTable2({ entity, select, ...props }: EntityTableProps) {
); );
} }
return <CellValue value={_value} property={property} />; return (
<ErrorBoundary fallback={String(_value)}>
<CellValue value={_value} property={property} />
</ErrorBoundary>
);
} }
return ( return (

View File

@@ -22,7 +22,27 @@ function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEnti
} }
function relationsToEdges(relations: AppDataConfig["relations"]) { function relationsToEdges(relations: AppDataConfig["relations"]) {
return Object.entries(relations ?? {}).map(([name, relation]) => { return Object.entries(relations ?? {}).flatMap(([name, relation]) => {
if (relation.type === "m:n") {
const conn_table = `${relation.source}_${relation.target}`;
return [
{
id: name,
target: relation.source,
source: conn_table,
targetHandle: `${relation.source}:id`,
sourceHandle: `${conn_table}:${relation.source}_id`,
},
{
id: `${name}-2`,
target: relation.target,
source: conn_table,
targetHandle: `${relation.target}:id`,
sourceHandle: `${conn_table}:${relation.target}_id`,
},
];
}
let sourceHandle = relation.source + `:${relation.target}`; let sourceHandle = relation.source + `:${relation.target}`;
if (relation.config?.mappedBy) { if (relation.config?.mappedBy) {
sourceHandle = `${relation.source}:${relation.config?.mappedBy}`; sourceHandle = `${relation.source}:${relation.config?.mappedBy}`;
@@ -65,6 +85,8 @@ export function DataSchemaCanvas() {
}, },
})); }));
console.log("-", data, { nodes, edges });
const nodeLayout = layoutWithDagre({ const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({ nodes: nodes.map((n) => ({
id: n.id, id: n.id,

View File

@@ -14,6 +14,8 @@ import { routes } from "ui/lib/routes";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { EntityTable, type EntityTableProps } from "../EntityTable"; import { EntityTable, type EntityTableProps } from "../EntityTable";
import type { ResponseObject } from "modules/ModuleApi"; import type { ResponseObject } from "modules/ModuleApi";
import ErrorBoundary from "ui/components/display/ErrorBoundary";
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
// @todo: allow clear if not required // @todo: allow clear if not required
export function EntityRelationalFormField({ export function EntityRelationalFormField({
@@ -151,8 +153,13 @@ export function EntityRelationalFormField({
<span className="opacity-60 text-nowrap"> <span className="opacity-60 text-nowrap">
{field.getLabel()}: {field.getLabel()}:
</span>{" "} </span>{" "}
{_value !== null && typeof value !== "undefined" ? ( {_value !== null && typeof _value !== "undefined" ? (
<span className="text-nowrap truncate">{_value}</span> <ErrorBoundary
fallback={JSON.stringify(_value)}
suppressError
>
<span className="text-nowrap truncate">{_value}</span>
</ErrorBoundary>
) : ( ) : (
<span className="opacity-30 text-nowrap font-mono mt-0.5"> <span className="opacity-30 text-nowrap font-mono mt-0.5">
null null
@@ -219,7 +226,7 @@ const PopoverTable = ({
return ( return (
<div> <div>
<EntityTable <EntityTable2
classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }} classNames={{ value: "line-clamp-1 truncate max-w-52 text-nowrap" }}
data={container ?? []} data={container ?? []}
entity={entity} entity={entity}

View File

@@ -161,7 +161,7 @@ const EntityLinkList = ({
> >
{entity.label} {entity.label}
{isLinkActive(href) && ( {isLinkActive(href, 1) && (
<Button <Button
IconLeft={IconSwitchHorizontal} IconLeft={IconSwitchHorizontal}
size="small" size="small"