Skip to content
Open
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
13 changes: 7 additions & 6 deletions apps/backend/src/app/agents/agents.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CompaniesRepository,
DealsRepository,
ActivityRepository,
TableColumnRepository
} from '@zuko/sales';

@Module({
Expand All @@ -28,8 +29,8 @@ import {
},
{
provide: ContactsService,
useFactory: (contactsRepository: ContactsRepository, eventEmitter: EventEmitter2) => {
return new ContactsService(contactsRepository, eventEmitter);
useFactory: (contactsRepository: ContactsRepository, eventEmitter: EventEmitter2, tableColumnRepository: TableColumnRepository) => {
return new ContactsService(contactsRepository, eventEmitter, tableColumnRepository);
},
inject: [ContactsRepository, EventEmitter2],
},
Expand All @@ -42,8 +43,8 @@ import {
},
{
provide: CompaniesService,
useFactory: (companiesRepository: CompaniesRepository, eventEmitter: EventEmitter2) => {
return new CompaniesService(companiesRepository, eventEmitter);
useFactory: (companiesRepository: CompaniesRepository, eventEmitter: EventEmitter2, tableColumnRepository: TableColumnRepository) => {
return new CompaniesService(companiesRepository, eventEmitter, tableColumnRepository);
},
inject: [CompaniesRepository, EventEmitter2],
},
Expand All @@ -70,8 +71,8 @@ import {
},
{
provide: DealsService,
useFactory: (dealsRepository: DealsRepository, eventEmitter: EventEmitter2) => {
return new DealsService(dealsRepository, eventEmitter);
useFactory: (dealsRepository: DealsRepository, eventEmitter: EventEmitter2, tableColumnRepository: TableColumnRepository) => {
return new DealsService(dealsRepository, eventEmitter, tableColumnRepository);
},
inject: [DealsRepository, EventEmitter2],
},
Expand Down
15 changes: 9 additions & 6 deletions apps/backend/src/app/sales/sales.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ import { TableRowBuilder } from './table/row-builder/table-row.builder';
useFactory: (
contactsRepository: ContactsRepository,
eventEmitter: EventEmitter2,
tableColumnRepository: TableColumnRepository,
) => {
return new ContactsService(contactsRepository, eventEmitter);
return new ContactsService(contactsRepository, eventEmitter, tableColumnRepository);
},
inject: [ContactsRepository, EventEmitter2],
inject: [ContactsRepository, EventEmitter2, TableColumnRepository],
},
{
provide: CompaniesRepository,
Expand All @@ -76,10 +77,11 @@ import { TableRowBuilder } from './table/row-builder/table-row.builder';
useFactory: (
companiesRepository: CompaniesRepository,
eventEmitter: EventEmitter2,
tableColumnRepository: TableColumnRepository,
) => {
return new CompaniesService(companiesRepository, eventEmitter);
return new CompaniesService(companiesRepository, eventEmitter, tableColumnRepository);
},
inject: [CompaniesRepository, EventEmitter2],
inject: [CompaniesRepository, EventEmitter2, TableColumnRepository],
},
{
provide: ActivityRepository,
Expand Down Expand Up @@ -107,10 +109,11 @@ import { TableRowBuilder } from './table/row-builder/table-row.builder';
useFactory: (
dealsRepository: DealsRepository,
eventEmitter: EventEmitter2,
tableColumnRepository: TableColumnRepository,
) => {
return new DealsService(dealsRepository, eventEmitter);
return new DealsService(dealsRepository, eventEmitter, tableColumnRepository);
},
inject: [DealsRepository, EventEmitter2],
inject: [DealsRepository, EventEmitter2, TableColumnRepository],
},
{
provide: DealActivityListener,
Expand Down
90 changes: 90 additions & 0 deletions apps/web-e2e/src/deals.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,3 +983,93 @@ test.describe("Cell Editing Flow", () => {
await expect(page.getByText(labelValue, { exact: true })).toBeVisible();
});
});

test.describe.serial("Custom Column Flow - Select Type", () => {
const identifier = Date.now();
const columnName = `Priority Level ${identifier}`;
const columnKey = `priority_level_${identifier}`;

test("Creates a column with type select and options", async ({
dealsPage,
page,
}) => {
await dealsPage.goto();

// Ensure at least one row exists
const deals = await dealsPage.getDealItems();
if (deals.length === 0) {
await dealsPage.addRow();
await expect(page.getByText(/New deal added/i)).toBeVisible();
}

// Open add column dialog
const addColumnButton = page.getByRole("button", { name: /Add column/i });
await addColumnButton.click();
await expect(page.getByText(/Add new field/i)).toBeVisible();

// Fill column details
await page.getByPlaceholder("Field name").fill(columnName);
await page.getByPlaceholder(/Unique column key/i).fill(columnKey);
await page.locator("select").selectOption("select");

// Test option management: add and remove
const addOptionButton = page.getByRole("button", { name: /Add option/i });

// Default 1 option exists
await expect(page.getByPlaceholder("Option value")).toHaveCount(1);

await addOptionButton.click();
await expect(page.getByPlaceholder("Option value")).toHaveCount(2);

// Remove the newly added option
const secondOptionContainer = page.locator("div.group").nth(1);
await secondOptionContainer.hover();
await secondOptionContainer.locator("button").click();
await expect(page.getByPlaceholder("Option value")).toHaveCount(1);

// Add final options
await page.getByPlaceholder("Option value").nth(0).fill("High");
await addOptionButton.click();
await page.getByPlaceholder("Option value").nth(1).fill("Low");

// Create the field
await page.getByRole("button", { name: "Create field" }).click();

// Verify creation
await expect(page.getByText("Column created successfully")).toBeVisible();
await expect(
page.getByRole("columnheader", { name: columnName })
).toBeVisible();
});

test("Updates a cell value for the new select type column", async ({
dealsPage,
page,
}) => {
await dealsPage.goto();

// Find the dynamic column index
const headers = page.getByRole("columnheader");
const headerTexts = await headers.allInnerTexts();
const columnIndex = headerTexts.findIndex((h) => h.includes(columnName));
expect(columnIndex).toBeGreaterThan(-1);

const firstRow = page.getByRole("row").nth(1);
const cell = firstRow.locator("td").nth(columnIndex);

// Click to view options
await cell.click();

// Validate select dropdown and its options
const select = cell.locator("select");
await expect(select).toBeVisible();
const options = await select.locator("option").allInnerTexts();
expect(options).toContain("High");
expect(options).toContain("Low");

// Select an option and verify update
await select.selectOption("High");
await expect(page.getByText("Cell updated successfully")).toBeVisible();
await expect(cell).toHaveText("High");
});
});
5 changes: 3 additions & 2 deletions apps/web/src/components/Companies/CompaniesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BaseTable, createColumnsFromMetadata, type BaseRow } from '../Table';
import { useAddColumn } from '@/hooks/use-add-column';
import { useAddRow } from '@/hooks/use-add-row';
import { useCellUpdate } from '@/hooks/use-cell-update';
import { ColumnConfig } from '@/types/table-metadata';

const CompaniesList = () => {
const router = useRouter();
Expand Down Expand Up @@ -48,8 +49,8 @@ const CompaniesList = () => {
router.push('/companies/new');
};

const handleNewColumn = (name: string, key: string, type: string) => {
addColumn({ label: name, columnKey: key, fieldType: type });
const handleNewColumn = (name: string, key: string, type: string, config?: ColumnConfig) => {
addColumn({ label: name, columnKey: key, fieldType: type, config });
};

return (
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/Contacts/ContactsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BaseTable, createColumnsFromMetadata, type BaseRow } from '../Table';
import { useAddColumn } from '@/hooks/use-add-column';
import { useAddRow } from '@/hooks/use-add-row';
import { useCellUpdate } from '@/hooks/use-cell-update';
import { ColumnConfig } from '@/types/table-metadata';

const ContactsList = () => {
const router = useRouter();
Expand Down Expand Up @@ -48,8 +49,8 @@ const ContactsList = () => {
addRow();
};

const handleNewColumn = (name: string, key: string, type: string) => {
addColumn({ label: name, columnKey: key, fieldType: type });
const handleNewColumn = (name: string, key: string, type: string, config?: ColumnConfig) => {
addColumn({ label: name, columnKey: key, fieldType: type, config });
};

return (
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/Deals/DealsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BaseTable, createColumnsFromMetadata, type BaseRow } from '../Table';
import { useAddColumn } from '@/hooks/use-add-column';
import { useAddRow } from '@/hooks/use-add-row';
import { useCellUpdate } from '@/hooks/use-cell-update';
import { ColumnConfig } from '@/types/table-metadata';

const DealsList = () => {
const router = useRouter();
Expand Down Expand Up @@ -48,8 +49,8 @@ const DealsList = () => {
router.push('/deals/new');
};

const handleNewColumn = (name: string, key: string, type: string) => {
addColumn({ label: name, columnKey: key, fieldType: type });
const handleNewColumn = (name: string, key: string, type: string, config?: ColumnConfig) => {
addColumn({ label: name, columnKey: key, fieldType: type, config });
};

return (
Expand Down
73 changes: 70 additions & 3 deletions apps/web/src/components/Table/AddColumnDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ import {
Label,
ErrorMessage,
} from '@zuko/ui-kit';
import {
PlusIcon,
XMarkIcon,
} from '@heroicons/react/20/solid';
import { ColumnConfig } from '@/types/table-metadata';

interface AddColumnDialogProps {
isOpen: boolean;
onClose: () => void;
onAdd: (name: string, key: string, type: string) => void;
onAdd: (name: string, key: string, type: string, config?: ColumnConfig) => void;
}

export function AddColumnDialog({ isOpen, onClose, onAdd }: AddColumnDialogProps) {
const [fieldName, setFieldName] = useState('');
const [columnKey, setColumnKey] = useState('');
const [fieldType, setFieldType] = useState('text');
const [options, setOptions] = useState<string[]>(['']);
const [errors, setErrors] = useState<Record<string, string>>({});
const validKeyRegex = /^[a-z0-9_]+$/;

Expand All @@ -50,16 +56,48 @@ export function AddColumnDialog({ isOpen, onClose, onAdd }: AddColumnDialogProps
setFieldName('');
setColumnKey('');
setFieldType('text');
setOptions(['']);
setErrors({});
onClose();
};

const handleAddOption = () => {
setOptions([...options, '']);
};

const handleRemoveOption = (index: number) => {
if (options.length > 1) {
setOptions(options.filter((_, i) => i !== index));
} else {
setOptions(['']);
}
};

const handleOptionChange = (index: number, value: string) => {
const newOptions = [...options];
newOptions[index] = value;
setOptions(newOptions);
};

const handleAdd = () => {
if (validate()) {
onAdd(fieldName, columnKey, fieldType);
let config: ColumnConfig | undefined = undefined;

if (fieldType === 'select') {
const validOptions = options
.filter((opt) => opt.trim() !== '')
.map((opt) => ({ label: opt.trim(), value: opt.trim() }));

if (validOptions.length > 0) {
config = { options: validOptions };
}
}

onAdd(fieldName, columnKey, fieldType, config);
setFieldName('');
setColumnKey('');
setFieldType('text');
setOptions(['']);
setErrors({});
onClose();
}
Expand Down Expand Up @@ -110,10 +148,39 @@ export function AddColumnDialog({ isOpen, onClose, onAdd }: AddColumnDialogProps
<option value="text">Single line text</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="boolean">Checkbox</option>
<option value="select">Select</option>
</Select>
{errors.fieldType && <ErrorMessage>{errors.fieldType}</ErrorMessage>}
</Field>

{fieldType === 'select' && (
<Field>
<Label>Options</Label>
<div data-slot="control" className="space-y-2">
{options.map((option, index) => (
<div key={index} className="flex items-center gap-2 group">
<Input
placeholder="Option value"
value={option}
onChange={(e) => handleOptionChange(index, e.target.value)}
className="flex-1"
/>
<Button
plain
onClick={() => handleRemoveOption(index)}
className="p-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<XMarkIcon className="h-4 w-4 text-zinc-400" />
</Button>
</div>
))}
<Button plain onClick={handleAddOption} className="mt-1 !h-8 text-xs !flex !items-center">
<PlusIcon className="h-4 w-4 mr-1" />
<span>Add option</span>
</Button>
</div>
</Field>
)}
</DialogBody>
<DialogActions>
<Button plain onClick={onCloseDialog}>Cancel</Button>
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/Table/BaseTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PlusIcon } from '@heroicons/react/24/outline';
import type { PaginationState } from '@tanstack/react-table';
import { AddColumnDialog } from './AddColumnDialog';
import React from 'react';
import { ColumnConfig } from '@/types/table-metadata';

const ChevronLeftIcon = '/icons/chevron-left.svg';
const ChevronRightIcon = '/icons/chevron-right.svg';
Expand Down Expand Up @@ -125,8 +126,8 @@ export function BaseTable<TData extends BaseRow>(props: BaseTableProps<TData>) {
<AddColumnDialog
isOpen={isAddColumnDialogOpen}
onClose={closeAddColumnDialog}
onAdd={(name: string, key: string, type: string) => {
onAddColumn?.(name, key, type);
onAdd={(name: string, key: string, type: string, config?: ColumnConfig) => {
onAddColumn?.(name, key, type, config);
}}
/>

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/Table/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ColumnConfig } from '@/types/table-metadata';
import {
ColumnDef,
PaginationState,
Expand Down Expand Up @@ -57,7 +58,7 @@ export interface BaseTableProps<TData extends BaseRow> {
showAddRow?: boolean;
onAddRow?: () => void;
showAddColumn?: boolean;
onAddColumn?: (name: string, key: string, type: string) => void;
onAddColumn?: (name: string, key: string, type: string, config?: ColumnConfig) => void;
onCellUpdate?: (rowId: string | number, columnId: string, value: any) => void;
showEmptyState?: boolean;
emptyStateConfig?: {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/api/tables.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { JSONValue } from 'next/dist/server/config-shared';
import { apiClient } from '../api-client';
import { ColumnConfig } from '@/types/table-metadata';

export interface CreateColumnDto {
label: string;
columnKey: string;
fieldType: string;
config?: ColumnConfig;
}

export interface TableColumn {
Expand Down
Loading
Loading