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
35 changes: 25 additions & 10 deletions src/app/(web)/tools/fieldmanagement/field-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -73,6 +73,10 @@ export function FieldManagement({ params }: FieldManagementProps) {
editorRef.current?.moveHiddenToOther();
};

const handleHideAllSeparators = () => {
editorRef.current?.hideAllSeparators();
};

const handleClose = () => {
router.back();
};
Expand All @@ -97,15 +101,26 @@ export function FieldManagement({ params }: FieldManagementProps) {
hideFooter={step === 1 || isLoadingFields || !fieldData}
footerExtra={
step === 2 && fieldData ? (
<Button
variant="outline"
size="sm"
onClick={handleMoveHiddenToOther}
disabled={isSaving}
>
<EyeOff className="w-4 h-4 mr-1" />
Move Hidden to Other
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleHideAllSeparators}
disabled={isSaving}
>
<SeparatorHorizontal className="w-4 h-4 mr-1" />
Hide All Separators
</Button>
<Button
variant="outline"
size="sm"
onClick={handleMoveHiddenToOther}
disabled={isSaving}
>
<EyeOff className="w-4 h-4 mr-1" />
Move Hidden to Other
</Button>
</div>
) : undefined
}
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/field-management/field-order-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export const FieldOrderEditor = forwardRef<FieldOrderEditorHandle, FieldOrderEdi
useImperativeHandle(ref, () => ({
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,
Expand Down
1 change: 1 addition & 0 deletions src/components/field-management/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ export interface FieldOrderPayload {
export interface FieldOrderEditorHandle {
getSavePayload: () => FieldOrderPayload[];
moveHiddenToOther: () => void;
hideAllSeparators: () => void;
}
111 changes: 111 additions & 0 deletions src/components/field-management/use-field-order-state.test.ts
Original file line number Diff line number Diff line change
@@ -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<PageField> & { 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);
});
});
42 changes: 42 additions & 0 deletions src/components/field-management/use-field-order-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface FieldOrderState {
addGroup: (name: string) => void;
removeGroup: (name: string) => void;
moveHiddenToOther: () => void;
hideAllSeparators: () => void;
updateField: (id: number, updates: Partial<PageField>) => void;
buildSavePayload: () => FieldOrderPayload[];
}
Expand Down Expand Up @@ -162,6 +163,46 @@ export function useFieldOrderState(fields: PageField[]): FieldOrderState {
setIsDirty(true);
}, [fieldLookup]);

const hideAllSeparators = useCallback(() => {
const separatorIds = new Set<number>();
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<PageField>) => {
setFieldLookup((prev) => {
const existing = prev.get(id);
Expand Down Expand Up @@ -226,6 +267,7 @@ export function useFieldOrderState(fields: PageField[]): FieldOrderState {
addGroup,
removeGroup,
moveHiddenToOther,
hideAllSeparators,
updateField,
buildSavePayload,
};
Expand Down
9 changes: 9 additions & 0 deletions src/test-setup.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
Loading