diff --git a/src/app/(web)/tools/fieldmanagement/field-management.tsx b/src/app/(web)/tools/fieldmanagement/field-management.tsx
index d37a172..ba05b3c 100644
--- a/src/app/(web)/tools/fieldmanagement/field-management.tsx
+++ b/src/app/(web)/tools/fieldmanagement/field-management.tsx
@@ -8,7 +8,7 @@ import { FieldOrderEditor } from "@/components/field-management/field-order-edit
import type { PageListItem, PageFieldData, FieldOrderEditorHandle } from "@/components/field-management";
import { fetchPageFieldData, savePageFieldOrder } from "@/components/field-management/actions";
import { ToolParams } from "@/lib/tool-params";
-import { ArrowLeft, Table, Loader2, EyeOff } from "lucide-react";
+import { ArrowLeft, Table, Loader2, EyeOff, SeparatorHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
@@ -73,6 +73,10 @@ export function FieldManagement({ params }: FieldManagementProps) {
editorRef.current?.moveHiddenToOther();
};
+ const handleHideAllSeparators = () => {
+ editorRef.current?.hideAllSeparators();
+ };
+
const handleClose = () => {
router.back();
};
@@ -97,15 +101,26 @@ export function FieldManagement({ params }: FieldManagementProps) {
hideFooter={step === 1 || isLoadingFields || !fieldData}
footerExtra={
step === 2 && fieldData ? (
-
+
+
+
+
) : undefined
}
>
diff --git a/src/components/field-management/field-order-editor.tsx b/src/components/field-management/field-order-editor.tsx
index 0ce74d0..a67d6ff 100644
--- a/src/components/field-management/field-order-editor.tsx
+++ b/src/components/field-management/field-order-editor.tsx
@@ -42,7 +42,8 @@ export const FieldOrderEditor = forwardRef ({
getSavePayload: state.buildSavePayload,
moveHiddenToOther: state.moveHiddenToOther,
- }), [state.buildSavePayload, state.moveHiddenToOther]);
+ hideAllSeparators: state.hideAllSeparators,
+ }), [state.buildSavePayload, state.moveHiddenToOther, state.hideAllSeparators]);
const totalFields = Object.values(state.groupedFields).reduce(
(sum, ids) => sum + ids.length,
diff --git a/src/components/field-management/types.ts b/src/components/field-management/types.ts
index 363149f..290d058 100644
--- a/src/components/field-management/types.ts
+++ b/src/components/field-management/types.ts
@@ -44,4 +44,5 @@ export interface FieldOrderPayload {
export interface FieldOrderEditorHandle {
getSavePayload: () => FieldOrderPayload[];
moveHiddenToOther: () => void;
+ hideAllSeparators: () => void;
}
diff --git a/src/components/field-management/use-field-order-state.test.ts b/src/components/field-management/use-field-order-state.test.ts
new file mode 100644
index 0000000..e700c48
--- /dev/null
+++ b/src/components/field-management/use-field-order-state.test.ts
@@ -0,0 +1,111 @@
+import { describe, it, expect } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useFieldOrderState } from './use-field-order-state';
+import type { PageField } from './types';
+
+const OTHER = '99 - Other Fields';
+
+function makeField(overrides: Partial & { Page_Field_ID: number; Field_Name: string }): PageField {
+ return {
+ Page_Field_ID: overrides.Page_Field_ID,
+ Page_ID: 1,
+ Field_Name: overrides.Field_Name,
+ Group_Name: overrides.Group_Name ?? '1 - General',
+ View_Order: overrides.View_Order ?? 1,
+ Required: overrides.Required ?? false,
+ Hidden: overrides.Hidden ?? false,
+ Default_Value: null,
+ Filter_Clause: null,
+ Depends_On_Field: null,
+ Field_Label: null,
+ Writing_Assistant_Enabled: false,
+ isSeparator: overrides.isSeparator ?? false,
+ };
+}
+
+describe('useFieldOrderState > hideAllSeparators', () => {
+ it('flips Hidden to true and moves separators to "99 - Other Fields"', () => {
+ const fields: PageField[] = [
+ makeField({ Page_Field_ID: 1, Field_Name: 'First_Name', Group_Name: '1 - General', View_Order: 1 }),
+ makeField({ Page_Field_ID: 2, Field_Name: 'Sep_A', Group_Name: '1 - General', View_Order: 2, isSeparator: true }),
+ makeField({ Page_Field_ID: 3, Field_Name: 'Last_Name', Group_Name: '2 - Name', View_Order: 3 }),
+ makeField({ Page_Field_ID: 4, Field_Name: 'Sep_B', Group_Name: '2 - Name', View_Order: 4, isSeparator: true }),
+ ];
+ const { result } = renderHook(() => useFieldOrderState(fields));
+
+ act(() => {
+ result.current.hideAllSeparators();
+ });
+
+ expect(result.current.fieldLookup.get(2)?.Hidden).toBe(true);
+ expect(result.current.fieldLookup.get(4)?.Hidden).toBe(true);
+ expect(result.current.fieldLookup.get(1)?.Hidden).toBe(false);
+ expect(result.current.fieldLookup.get(3)?.Hidden).toBe(false);
+
+ expect(result.current.groupedFields['1 - General']).toEqual([1]);
+ expect(result.current.groupedFields['2 - Name']).toEqual([3]);
+ expect(result.current.groupedFields[OTHER]).toEqual([2, 4]);
+ expect(result.current.groupOrder[result.current.groupOrder.length - 1]).toBe(OTHER);
+ expect(result.current.isDirty).toBe(true);
+ });
+
+ it('preserves existing "Other" fields ahead of newly-moved separators', () => {
+ const fields: PageField[] = [
+ makeField({ Page_Field_ID: 10, Field_Name: 'Existing_Other', Group_Name: OTHER, View_Order: 1 }),
+ makeField({ Page_Field_ID: 11, Field_Name: 'Sep_X', Group_Name: '1 - General', View_Order: 2, isSeparator: true }),
+ ];
+ const { result } = renderHook(() => useFieldOrderState(fields));
+
+ act(() => {
+ result.current.hideAllSeparators();
+ });
+
+ expect(result.current.groupedFields[OTHER]).toEqual([10, 11]);
+ });
+
+ it('no-ops when no separators exist', () => {
+ const fields: PageField[] = [
+ makeField({ Page_Field_ID: 1, Field_Name: 'First_Name', Group_Name: '1 - General', View_Order: 1 }),
+ ];
+ const { result } = renderHook(() => useFieldOrderState(fields));
+
+ act(() => {
+ result.current.hideAllSeparators();
+ });
+
+ expect(result.current.isDirty).toBe(false);
+ expect(result.current.fieldLookup.get(1)?.Hidden).toBe(false);
+ });
+
+ it('handles auto-injected separators with negative Page_Field_IDs', () => {
+ const fields: PageField[] = [
+ makeField({ Page_Field_ID: 1, Field_Name: 'First_Name', Group_Name: '1 - General', View_Order: 1 }),
+ makeField({ Page_Field_ID: -1, Field_Name: 'Sep_Injected', Group_Name: null, View_Order: 2, isSeparator: true }),
+ ];
+ const { result } = renderHook(() => useFieldOrderState(fields));
+
+ act(() => {
+ result.current.hideAllSeparators();
+ });
+
+ expect(result.current.fieldLookup.get(-1)?.Hidden).toBe(true);
+ expect(result.current.groupedFields[OTHER]).toContain(-1);
+ });
+
+ it('flips Hidden values so buildSavePayload persists Hidden=true for separators', () => {
+ const fields: PageField[] = [
+ makeField({ Page_Field_ID: 1, Field_Name: 'First_Name', Group_Name: '1 - General', View_Order: 1 }),
+ makeField({ Page_Field_ID: 2, Field_Name: 'Sep_A', Group_Name: '1 - General', View_Order: 2, isSeparator: true }),
+ ];
+ const { result } = renderHook(() => useFieldOrderState(fields));
+
+ act(() => {
+ result.current.hideAllSeparators();
+ });
+
+ const payload = result.current.buildSavePayload();
+ const sep = payload.find((p) => p.Field_Name === 'Sep_A');
+ expect(sep?.Hidden).toBe(true);
+ expect(sep?.Group_Name).toBe(OTHER);
+ });
+});
diff --git a/src/components/field-management/use-field-order-state.ts b/src/components/field-management/use-field-order-state.ts
index dc1441b..06f2a38 100644
--- a/src/components/field-management/use-field-order-state.ts
+++ b/src/components/field-management/use-field-order-state.ts
@@ -23,6 +23,7 @@ interface FieldOrderState {
addGroup: (name: string) => void;
removeGroup: (name: string) => void;
moveHiddenToOther: () => void;
+ hideAllSeparators: () => void;
updateField: (id: number, updates: Partial) => void;
buildSavePayload: () => FieldOrderPayload[];
}
@@ -162,6 +163,46 @@ export function useFieldOrderState(fields: PageField[]): FieldOrderState {
setIsDirty(true);
}, [fieldLookup]);
+ const hideAllSeparators = useCallback(() => {
+ const separatorIds = new Set();
+ for (const field of fieldLookup.values()) {
+ if (field.isSeparator) separatorIds.add(field.Page_Field_ID);
+ }
+ if (separatorIds.size === 0) return;
+
+ setFieldLookup((prev) => {
+ const next = new Map(prev);
+ for (const id of separatorIds) {
+ const existing = next.get(id);
+ if (existing && !existing.Hidden) {
+ next.set(id, { ...existing, Hidden: true });
+ }
+ }
+ return next;
+ });
+
+ setGroupedFields((prev) => {
+ const next: GroupedFieldsMap = {};
+ for (const [groupName, ids] of Object.entries(prev)) {
+ if (groupName === OTHER_FIELDS_GROUP) continue;
+ next[groupName] = ids.filter((id) => !separatorIds.has(id));
+ }
+ const existingOther = prev[OTHER_FIELDS_GROUP] ?? [];
+ next[OTHER_FIELDS_GROUP] = [
+ ...existingOther.filter((id) => !separatorIds.has(id)),
+ ...separatorIds,
+ ];
+ return next;
+ });
+
+ setGroupOrder((prev) => {
+ if (prev.includes(OTHER_FIELDS_GROUP)) return prev;
+ return [...prev, OTHER_FIELDS_GROUP];
+ });
+
+ setIsDirty(true);
+ }, [fieldLookup]);
+
const updateField = useCallback((id: number, updates: Partial) => {
setFieldLookup((prev) => {
const existing = prev.get(id);
@@ -226,6 +267,7 @@ export function useFieldOrderState(fields: PageField[]): FieldOrderState {
addGroup,
removeGroup,
moveHiddenToOther,
+ hideAllSeparators,
updateField,
buildSavePayload,
};
diff --git a/src/test-setup.ts b/src/test-setup.ts
index 4fa089a..0186974 100644
--- a/src/test-setup.ts
+++ b/src/test-setup.ts
@@ -1,6 +1,15 @@
import '@testing-library/jest-dom';
import { vi } from 'vitest';
+if (typeof globalThis.ResizeObserver === 'undefined') {
+ class ResizeObserverStub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ globalThis.ResizeObserver = ResizeObserverStub as unknown as typeof ResizeObserver;
+}
+
// Mock environment variables for tests
vi.stubEnv('MINISTRY_PLATFORM_BASE_URL', 'https://test-mp.example.com');
vi.stubEnv('MINISTRY_PLATFORM_CLIENT_ID', 'test-client-id');