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
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ describe('FrameworkRequirementsClientPage — Description column', () => {

const description = editableCellProps.find((p) => p.columnId === 'description');
expect(description?.expandable).toBe(true);
expect(description?.expandTitle).toBe('Edit Requirement Description');
// Identifier + name are appended so the editor dialog says which requirement
// is being edited (FRAME-7), e.g. "… - AC-2 - Account Management".
expect(description?.expandTitle).toBe(
'Edit Requirement Description - AC-2 - Account Management',
);

// The short single-line columns stay as plain inline edits.
for (const columnId of ['identifier', 'name']) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export function FrameworkRequirementsClientPage({
const router = useRouter();
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
// Row whose large description editor is currently open — highlighted so the
// edited row is obvious behind the (semi-transparent) editor dialog.
const [expandedRowId, setExpandedRowId] = useState<string | null>(null);

const initialGridData: RequirementGridRow[] = useMemo(
() =>
Expand Down Expand Up @@ -155,16 +158,27 @@ export function FrameworkRequirementsClientPage({
header: 'Description',
size: 300,
maxSize: 300,
cell: ({ row, getValue }) => (
<EditableCell
value={getValue()}
rowId={row.original.id}
columnId="description"
onUpdate={updateCell}
expandable
expandTitle="Edit Requirement Description"
/>
),
cell: ({ row, getValue }) => {
const { identifier, name } = row.original;
const titleSuffix = [identifier, name].filter(Boolean).join(' - ');
return (
<EditableCell
value={getValue()}
rowId={row.original.id}
columnId="description"
onUpdate={updateCell}
expandable
expandTitle={
titleSuffix
? `Edit Requirement Description - ${titleSuffix}`
: 'Edit Requirement Description'
}
onExpandedChange={(open) =>
setExpandedRowId(open ? row.original.id : null)
}
/>
);
},
}),
columnHelper.accessor('controlTemplates', {
header: 'Linked Controls',
Expand Down Expand Up @@ -369,7 +383,11 @@ export function FrameworkRequirementsClientPage({
{table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className={`border-border hover:bg-muted/30 border-b transition-colors ${getRowClassName(row.original.id)}`}
className={`border-border hover:bg-muted/30 border-b transition-colors ${getRowClassName(row.original.id)} ${
expandedRowId === row.original.id
? 'ring-primary !bg-primary/15 ring-2 ring-inset'
: ''
}`}
>
{row.getVisibleCells().map((cell) => (
<td
Expand Down
19 changes: 19 additions & 0 deletions apps/framework-editor/app/components/table/EditableCell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,23 @@ describe('EditableCell — expandable', () => {
setup({ expandable: true, disabled: true });
expect(screen.queryByRole('button', { name: /large editor/i })).toBeNull();
});

it('notifies onExpandedChange when the editor opens and on Save', () => {
const onExpandedChange = vi.fn();
setup({ expandable: true, onExpandedChange });
fireEvent.click(screen.getByRole('button', { name: /large editor/i }));
expect(onExpandedChange).toHaveBeenLastCalledWith(true);
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'changed' } });
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onExpandedChange).toHaveBeenLastCalledWith(false);
});

it('notifies onExpandedChange(false) on Cancel', () => {
const onExpandedChange = vi.fn();
setup({ expandable: true, onExpandedChange });
fireEvent.contextMenu(screen.getByText(/assign account managers/i));
expect(onExpandedChange).toHaveBeenLastCalledWith(true);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onExpandedChange).toHaveBeenLastCalledWith(false);
});
});
20 changes: 16 additions & 4 deletions apps/framework-editor/app/components/table/EditableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ interface EditableCellProps {
// values like control descriptions.
expandable?: boolean;
expandTitle?: string;
// Notified when the large editor opens/closes so the parent can highlight
// the row currently being edited.
onExpandedChange?: (open: boolean) => void;
}

export function EditableCell({
Expand All @@ -35,12 +38,21 @@ export function EditableCell({
placeholder = 'Click to edit',
expandable = false,
expandTitle = 'Edit',
onExpandedChange,
}: EditableCellProps) {
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value ?? '');
const [isExpanded, setIsExpanded] = useState(false);
const [expandValue, setExpandValue] = useState(value ?? '');

// Keep local open state and the parent notification in lockstep so the row
// highlight tracks the dialog exactly (open icon, right-click, save, cancel,
// Esc, and overlay click all route through here).
const setExpanded = (open: boolean) => {
setIsExpanded(open);
onExpandedChange?.(open);
};

const handleBlur = () => {
setIsEditing(false);
if (editValue !== (value ?? '')) {
Expand All @@ -66,14 +78,14 @@ export function EditableCell({
const handleOpenExpanded = () => {
if (disabled) return;
setExpandValue(value ?? '');
setIsExpanded(true);
setExpanded(true);
};

const handleExpandSave = () => {
if (expandValue !== (value ?? '')) {
onUpdate(rowId, columnId, expandValue);
}
setIsExpanded(false);
setExpanded(false);
};

if (disabled) {
Expand Down Expand Up @@ -136,7 +148,7 @@ export function EditableCell({
<Maximize2 className="h-3.5 w-3.5" />
</button>

<Dialog open={isExpanded} onOpenChange={setIsExpanded}>
<Dialog open={isExpanded} onOpenChange={setExpanded}>
<DialogContent className="sm:max-w-[760px]">
<DialogHeader>
<DialogTitle>{expandTitle}</DialogTitle>
Expand All @@ -148,7 +160,7 @@ export function EditableCell({
className="min-h-[260px] font-mono text-sm"
/>
<DialogFooter>
<Button variant="outline" onClick={() => setIsExpanded(false)}>
<Button variant="outline" onClick={() => setExpanded(false)}>
Cancel
</Button>
<Button onClick={handleExpandSave} disabled={expandValue === (value ?? '')}>
Expand Down
Loading