combine review sections

This commit is contained in:
Josh Hawkins
2026-02-27 09:40:21 -06:00
parent f82a721477
commit bbd62129ca
4 changed files with 91 additions and 26 deletions
@@ -1,9 +1,15 @@
import type { ComponentType } from "react";
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
import CameraReviewSettingsView from "@/views/settings/CameraReviewSettingsView.tsx";
export type RendererComponent = ComponentType<
Record<string, unknown> | undefined
>;
// Props that will be injected into all section renderers
export type SectionRendererProps = {
selectedCamera?: string;
setUnsavedChanges?: (hasChanges: boolean) => void;
[key: string]: unknown; // Allow additional props from uiSchema
};
export type RendererComponent = ComponentType<SectionRendererProps>;
export type SectionRenderers = Record<
string,
@@ -16,10 +22,26 @@ export type SectionRenderers = Record<
// names to React components. These names are referenced from `uiSchema`
// descriptors (e.g., `{ "ui:after": { render: "SemanticSearchReindex" } }`) and
// are resolved by `FieldTemplate` through `formContext.renderers`.
//
// RUNTIME PROPS INJECTION:
// All renderers automatically receive the following props from BaseSection:
// - selectedCamera?: string - The current camera name (camera-level only)
// - setUnsavedChanges?: (hasChanges: boolean) => void - Callback to signal unsaved state
//
// Additional static props can be passed via uiSchema:
// { "ui:after": { render: "MyRenderer", props: { customProp: "value" } } }
//
// ADDING NEW RENDERERS:
// 1. Create your component accepting SectionRendererProps
// 2. Import and add it to the appropriate section in this registry
// 3. Reference it in your section's uiSchema using the { render: "ComponentName" } syntax
export const sectionRenderers: SectionRenderers = {
semantic_search: {
SemanticSearchReindex,
},
review: {
CameraReviewSettingsView,
},
};
export default sectionRenderers;
@@ -53,6 +53,7 @@ import {
import { applySchemaDefaults } from "@/lib/config-schema";
import { isJsonObject } from "@/lib/utils";
import { ConfigSectionData, JsonObject, JsonValue } from "@/types/configForm";
import ActivityIndicator from "@/components/indicators/activity-indicator";
export interface SectionConfig {
/** Field ordering within the section */
@@ -513,7 +514,7 @@ export function ConfigSection({
const needsRestart = requiresRestartForOverrides(overrides);
await axios.put("config/sett", {
await axios.put("config/set", {
requires_restart: needsRestart ? 1 : 0,
update_topic: updateTopic,
config_data: {
@@ -607,7 +608,7 @@ export function ConfigSection({
const configData = level === "global" ? effectiveSchemaDefaults : "";
await axios.put("config/sett", {
await axios.put("config/set", {
requires_restart: requiresRestart ? 0 : 1,
update_topic: updateTopic,
config_data: {
@@ -688,6 +689,42 @@ export function ConfigSection({
);
}, [sectionConfig.customValidate, sectionValidation]);
// Wrap renderers with runtime props (selectedCamera, setUnsavedChanges, etc.)
const wrappedRenderers = useMemo(() => {
const baseRenderers =
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath];
if (!baseRenderers) return undefined;
// Create wrapper that injects runtime props
return Object.fromEntries(
Object.entries(baseRenderers).map(([key, RendererComponent]) => [
key,
(staticProps: Record<string, unknown> = {}) => (
<RendererComponent
{...staticProps}
selectedCamera={cameraName}
setUnsavedChanges={(hasChanges: boolean) => {
// Translate setUnsavedChanges to pending data state
if (hasChanges && !pendingData) {
// Component signaled changes but we don't have pending data yet
// This can happen when the component manages its own state
} else if (!hasChanges && pendingData) {
// Component signaled no changes, clear pending
setPendingData(null);
}
}}
/>
),
]),
);
}, [
sectionConfig?.renderers,
sectionPath,
cameraName,
pendingData,
setPendingData,
]);
if (!modifiedSchema) {
return null;
}
@@ -749,8 +786,7 @@ export function ConfigSection({
// section prefix to templates so they can attempt `${section}.${field}` lookups.
sectionI18nPrefix: sectionPath,
t,
renderers:
sectionConfig?.renderers ?? sectionRenderers?.[sectionPath],
renderers: wrappedRenderers,
sectionDocs: sectionConfig.sectionDocs,
fieldDocs: sectionConfig.fieldDocs,
}}
@@ -775,7 +811,7 @@ export function ConfigSection({
onClick={() => setIsResetDialogOpen(true)}
variant="outline"
disabled={isSaving || disabled}
className="gap-2"
className="flex flex-1 gap-2"
>
<LuRotateCcw className="h-4 w-4" />
{level === "global"
@@ -794,7 +830,7 @@ export function ConfigSection({
onClick={handleReset}
variant="outline"
disabled={isSaving || disabled}
className="gap-2"
className="flex min-w-36 flex-1 gap-2"
>
{t("undo", { ns: "common", defaultValue: "Undo" })}
</Button>
@@ -803,12 +839,19 @@ export function ConfigSection({
onClick={handleSave}
variant="select"
disabled={!hasChanges || isSaving || disabled}
className="gap-2"
className="flex min-w-36 flex-1 gap-2"
>
<LuSave className="h-4 w-4" />
{isSaving
? t("saving", { ns: "common", defaultValue: "Saving..." })
: t("save", { ns: "common", defaultValue: "Save" })}
{isSaving ? (
<>
<ActivityIndicator className="h-4 w-4" />
{t("saving", { ns: "common", defaultValue: "Saving..." })}
</>
) : (
<>
<LuSave className="h-4 w-4" />
{t("save", { ns: "common", defaultValue: "Save" })}
</>
)}
</Button>
</div>
</div>