This commit is contained in:
Philipinho
2026-03-09 01:08:15 +00:00
parent 4ff13cef62
commit 084746e65a
12 changed files with 667 additions and 105 deletions
@@ -13,7 +13,6 @@ import {
import { useUpdateRowMutation } from "@/features/base/queries/base-row-query";
import { useCreateRowMutation } from "@/features/base/queries/base-row-query";
import { useReorderRowMutation } from "@/features/base/queries/base-row-query";
import { useCreatePropertyMutation } from "@/features/base/queries/base-property-query";
import { useCreateViewMutation } from "@/features/base/queries/base-view-query";
import { activeViewIdAtom } from "@/features/base/atoms/base-atoms";
import { useBaseTable } from "@/features/base/hooks/use-base-table";
@@ -45,7 +44,6 @@ export function BaseTable({ baseId }: BaseTableProps) {
const updateRowMutation = useUpdateRowMutation();
const createRowMutation = useCreateRowMutation();
const reorderRowMutation = useReorderRowMutation();
const createPropertyMutation = useCreatePropertyMutation();
const createViewMutation = useCreateViewMutation();
useEffect(() => {
@@ -76,14 +74,6 @@ export function BaseTable({ baseId }: BaseTableProps) {
createRowMutation.mutate({ baseId });
}, [baseId, createRowMutation]);
const handleAddColumn = useCallback(() => {
createPropertyMutation.mutate({
baseId,
name: t("New property"),
type: "text",
});
}, [baseId, createPropertyMutation, t]);
const handleViewChange = useCallback(
(viewId: string) => {
setActiveViewId(viewId);
@@ -188,7 +178,7 @@ export function BaseTable({ baseId }: BaseTableProps) {
table={table}
onCellUpdate={handleCellUpdate}
onAddRow={handleAddRow}
onAddColumn={handleAddColumn}
baseId={baseId}
onColumnReorder={handleColumnReorder}
onResizeEnd={handleResizeEnd}
onRowReorder={handleRowReorder}
@@ -1,7 +1,10 @@
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
import { Popover } from "@mantine/core";
import { IconX } from "@tabler/icons-react";
import { IBaseProperty } from "@/features/base/types/base.types";
import {
IBaseProperty,
PersonTypeOptions,
} from "@/features/base/types/base.types";
import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query";
import { CustomAvatar } from "@/components/ui/custom-avatar";
import cellClasses from "@/features/base/styles/cells.module.css";
@@ -17,10 +20,14 @@ type CellPersonProps = {
export function CellPerson({
value,
property,
isEditing,
onCommit,
onCancel,
}: CellPersonProps) {
const allowMultiple =
(property.typeOptions as PersonTypeOptions)?.allowMultiple !== false;
const personIds = Array.isArray(value)
? (value as string[])
: typeof value === "string"
@@ -53,31 +60,38 @@ export function CellPerson({
)
: members;
const handleAdd = useCallback(
const handleSelect = useCallback(
(memberId: string) => {
if (personIds.includes(memberId)) return;
onCommit([...personIds, memberId]);
if (allowMultiple) {
// Multi mode: toggle add/remove
if (personIds.includes(memberId)) {
const newIds = personIds.filter((id) => id !== memberId);
onCommit(newIds.length > 0 ? newIds : null);
} else {
onCommit([...personIds, memberId]);
}
} else {
// Single mode: replace or clear
if (personIds.includes(memberId)) {
onCommit(null);
} else {
onCommit(memberId);
}
}
},
[personIds, onCommit],
[allowMultiple, personIds, onCommit],
);
const handleRemove = useCallback(
(memberId: string) => {
const newIds = personIds.filter((id) => id !== memberId);
onCommit(newIds.length > 0 ? newIds : null);
},
[personIds, onCommit],
);
const handleToggle = useCallback(
(memberId: string) => {
if (personIds.includes(memberId)) {
handleRemove(memberId);
if (allowMultiple) {
const newIds = personIds.filter((id) => id !== memberId);
onCommit(newIds.length > 0 ? newIds : null);
} else {
handleAdd(memberId);
onCommit(null);
}
},
[personIds, handleAdd, handleRemove],
[allowMultiple, personIds, onCommit],
);
const handleKeyDown = useCallback(
@@ -150,9 +164,11 @@ export function CellPerson({
{/* Dropdown */}
<div className={cellClasses.personDropdownDivider} />
<div className={cellClasses.personDropdownHint}>
Select as many as you like
</div>
{allowMultiple && (
<div className={cellClasses.personDropdownHint}>
Select as many as you like
</div>
)}
<div className={cellClasses.selectDropdown}>
{filteredMembers.map((member) => (
<div
@@ -160,7 +176,7 @@ export function CellPerson({
className={`${cellClasses.selectOption} ${
selectedSet.has(member.id) ? cellClasses.selectOptionActive : ""
}`}
onClick={() => handleToggle(member.id)}
onClick={() => handleSelect(member.id)}
>
<CustomAvatar
avatarUrl={member.avatarUrl}
@@ -33,7 +33,7 @@ type GridContainerProps = {
table: Table<IBaseRow>;
onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void;
onAddRow?: () => void;
onAddColumn?: () => void;
baseId?: string;
onColumnReorder?: (columnId: string, overColumnId: string) => void;
onResizeEnd?: () => void;
onRowReorder?: (rowId: string, targetRowId: string, position: "above" | "below") => void;
@@ -46,7 +46,7 @@ export function GridContainer({
table,
onCellUpdate,
onAddRow,
onAddColumn,
baseId,
onColumnReorder,
onResizeEnd,
onRowReorder,
@@ -115,8 +115,8 @@ export function GridContainer({
const gridTemplateColumns = useMemo(() => {
const visibleColumns = table.getVisibleLeafColumns();
const columnWidths = visibleColumns.map((col) => `${col.getSize()}px`);
return columnWidths.join(" ") + (onAddColumn ? " 40px" : "");
}, [table, table.getState().columnSizing, table.getState().columnVisibility, onAddColumn]);
return columnWidths.join(" ") + (baseId ? " 40px" : "");
}, [table, table.getState().columnSizing, table.getState().columnVisibility, table.getState().columnOrder, baseId]);
const totalHeight = virtualizer.getTotalSize();
@@ -148,6 +148,18 @@ export function GridContainer({
onAddRow?.();
}, [onAddRow]);
const handlePropertyCreated = useCallback(() => {
// Wait for React to re-render with the new column, then scroll to it
requestAnimationFrame(() => {
requestAnimationFrame(() => {
scrollRef.current?.scrollTo({
left: scrollRef.current.scrollWidth,
behavior: "smooth",
});
});
});
}, []);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
@@ -194,7 +206,12 @@ export function GridContainer({
items={sortableColumnIds}
strategy={horizontalListSortingStrategy}
>
<GridHeader table={table} onAddColumn={onAddColumn} />
<GridHeader
table={table}
baseId={baseId}
columnOrder={table.getState().columnOrder}
onPropertyCreated={handlePropertyCreated}
/>
</SortableContext>
{paddingTop > 0 && (
@@ -1,39 +1,38 @@
import { memo, useCallback } from "react";
import { Table } from "@tanstack/react-table";
import { memo } from "react";
import { Table, ColumnOrderState } from "@tanstack/react-table";
import { IBaseRow } from "@/features/base/types/base.types";
import { GridHeaderCell } from "./grid-header-cell";
import { IconPlus } from "@tabler/icons-react";
import { CreatePropertyPopover } from "@/features/base/components/property/create-property-popover";
import classes from "@/features/base/styles/grid.module.css";
type GridHeaderProps = {
table: Table<IBaseRow>;
onAddColumn?: () => void;
baseId?: string;
// Passed explicitly to break memo when columns change
// (table ref is stable from useReactTable, so memo won't fire without this)
columnOrder: ColumnOrderState;
onPropertyCreated?: () => void;
};
export const GridHeader = memo(function GridHeader({
table,
onAddColumn,
baseId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
columnOrder: _columnOrder,
onPropertyCreated,
}: GridHeaderProps) {
const headerGroups = table.getHeaderGroups();
const handleAddColumn = useCallback(() => {
onAddColumn?.();
}, [onAddColumn]);
return (
<div className={classes.headerRow} role="row">
{headerGroups[0]?.headers.map((header) => (
<GridHeaderCell key={header.id} header={header} />
))}
{onAddColumn && (
<div
className={classes.addColumnButton}
onClick={handleAddColumn}
role="button"
tabIndex={0}
>
<IconPlus size={16} />
</div>
{baseId && (
<CreatePropertyPopover
baseId={baseId}
onPropertyCreated={onPropertyCreated}
/>
)}
</div>
);
@@ -54,6 +54,7 @@ type ChoiceEditorProps = {
onClose: () => void;
onDirtyChange?: (dirty: boolean) => void;
showCategories?: boolean;
hideButtons?: boolean;
};
export function ChoiceEditor({
@@ -62,14 +63,28 @@ export function ChoiceEditor({
onClose,
onDirtyChange,
showCategories = false,
hideButtons = false,
}: ChoiceEditorProps) {
const { t } = useTranslation();
const [draft, setDraft] = useState<Choice[]>(initialChoices);
const [focusChoiceId, setFocusChoiceId] = useState<string | null>(null);
// Sync from parent only when not in live mode (hideButtons = create flow)
useEffect(() => {
setDraft(initialChoices);
}, [initialChoices]);
if (!hideButtons) {
setDraft(initialChoices);
}
}, [initialChoices, hideButtons]);
// In live mode, propagate draft changes to parent immediately
const onSaveRef = useRef(onSave);
onSaveRef.current = onSave;
useEffect(() => {
if (hideButtons) {
onSaveRef.current(draft.filter((c) => c.name.trim()));
}
}, [hideButtons, draft]);
const isDirty = useMemo(() => {
if (draft.length !== initialChoices.length) return true;
@@ -195,16 +210,20 @@ export function ChoiceEditor({
/>
)}
<Divider />
{!hideButtons && (
<>
<Divider />
<Group justify="flex-end" gap="xs">
<Button variant="default" size="xs" onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSave} disabled={!isDirty || hasEmptyNames}>
{t("Save")}
</Button>
</Group>
<Group justify="flex-end" gap="xs">
<Button variant="default" size="xs" onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSave} disabled={!isDirty || hasEmptyNames}>
{t("Save")}
</Button>
</Group>
</>
)}
</Stack>
);
}
@@ -0,0 +1,316 @@
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
import {
Popover,
Portal,
TextInput,
Button,
Group,
Stack,
Divider,
UnstyledButton,
Text,
ScrollArea,
} from "@mantine/core";
import { IconPlus, IconChevronRight } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import {
BasePropertyType,
IBaseProperty,
TypeOptions,
} from "@/features/base/types/base.types";
import { useCreatePropertyMutation } from "@/features/base/queries/base-property-query";
import { PropertyTypePicker, propertyTypes } from "./property-type-picker";
import { PropertyOptions } from "./property-options";
import classes from "@/features/base/styles/grid.module.css";
type CreatePropertyPopoverProps = {
baseId: string;
onPropertyCreated?: () => void;
};
type Panel = "typePicker" | "configure" | "confirmDiscard";
const noop = () => {};
// Keep in sync with the switch cases in PropertyOptions
const typesWithOptions = new Set<BasePropertyType>([
"select",
"multiSelect",
"status",
"number",
"date",
"person",
]);
export function CreatePropertyPopover({ baseId, onPropertyCreated }: CreatePropertyPopoverProps) {
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const [panel, setPanel] = useState<Panel>("typePicker");
const [selectedType, setSelectedType] = useState<BasePropertyType | null>(null);
const [name, setName] = useState("");
const [typeOptions, setTypeOptions] = useState<Record<string, unknown>>({});
const nameInputRef = useRef<HTMLInputElement>(null);
const createPropertyMutation = useCreatePropertyMutation();
const selectedTypeDef = useMemo(
() => propertyTypes.find((pt) => pt.type === selectedType),
[selectedType],
);
const selectedTypeLabel = selectedTypeDef ? t(selectedTypeDef.labelKey) : "";
const selectedTypeIcon = selectedTypeDef?.icon;
const hasContent = useMemo(() => {
return name.trim().length > 0 || Object.keys(typeOptions).length > 0;
}, [name, typeOptions]);
const resetState = useCallback(() => {
setPanel("typePicker");
setSelectedType(null);
setName("");
setTypeOptions({});
}, []);
const handleOpen = useCallback(() => {
resetState();
setOpened(true);
}, [resetState]);
const handleClose = useCallback(() => {
setOpened(false);
resetState();
}, [resetState]);
const attemptClose = useCallback(() => {
if (panel === "configure" && hasContent) {
setPanel("confirmDiscard");
} else {
handleClose();
}
}, [panel, hasContent, handleClose]);
const handleConfirmDiscard = useCallback(() => {
handleClose();
}, [handleClose]);
const handleCancelDiscard = useCallback(() => {
setPanel("configure");
}, []);
const handleTypeSelect = useCallback((type: BasePropertyType) => {
setSelectedType(type);
setTypeOptions({});
setPanel("configure");
}, []);
useEffect(() => {
if (panel === "configure") {
setTimeout(() => nameInputRef.current?.focus(), 0);
}
}, [panel]);
const handleCreate = useCallback(() => {
if (!selectedType) return;
const finalName = name.trim() || selectedTypeLabel;
createPropertyMutation.mutate(
{
baseId,
name: finalName,
type: selectedType,
typeOptions: Object.keys(typeOptions).length > 0
? typeOptions as TypeOptions
: undefined,
},
{
onSuccess: () => {
onPropertyCreated?.();
},
},
);
handleClose();
}, [selectedType, name, selectedTypeLabel, typeOptions, baseId, createPropertyMutation, handleClose, onPropertyCreated]);
const handleBackToTypePicker = useCallback(() => {
setPanel("typePicker");
setTypeOptions({});
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
if (panel === "confirmDiscard") {
handleCancelDiscard();
} else if (panel === "configure") {
handleBackToTypePicker();
} else {
handleClose();
}
}
},
[panel, handleBackToTypePicker, handleClose, handleCancelDiscard],
);
const handleNameKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleCreate();
}
},
[handleCreate],
);
const handleOptionsUpdate = useCallback(
(newTypeOptions: Record<string, unknown>) => {
setTypeOptions(newTypeOptions);
},
[],
);
const syntheticProperty: IBaseProperty = useMemo(() => ({
id: "",
baseId,
name: name || "",
type: selectedType ?? "text",
position: "",
typeOptions: typeOptions as TypeOptions,
isPrimary: false,
workspaceId: "",
createdAt: "",
updatedAt: "",
}), [baseId, name, selectedType, typeOptions]);
const TypeIcon = selectedTypeIcon;
const showOptions = selectedType && typesWithOptions.has(selectedType);
return (
<>
{opened && (
<Portal>
<div
style={{
position: "fixed",
inset: 0,
zIndex: 299,
}}
onClick={attemptClose}
/>
</Portal>
)}
<Popover
opened={opened}
onClose={noop}
position="bottom-start"
shadow="md"
width={320}
withinPortal
>
<Popover.Target>
<div
className={classes.addColumnButton}
onClick={handleOpen}
role="button"
tabIndex={0}
>
<IconPlus size={16} />
</div>
</Popover.Target>
<Popover.Dropdown
p={0}
onClick={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
style={{ zIndex: 300 }}
>
{panel === "typePicker" && (
<Stack gap={0} p={4}>
<PropertyTypePicker
onSelect={handleTypeSelect}
showSearch
/>
</Stack>
)}
{(panel === "configure" || panel === "confirmDiscard") && (
<Stack gap={0} p="sm" style={panel === "confirmDiscard" ? { display: "none" } : undefined}>
<TextInput
ref={nameInputRef}
size="xs"
placeholder={selectedTypeLabel}
value={name}
onChange={(e) => setName(e.currentTarget.value)}
onKeyDown={handleNameKeyDown}
mb="xs"
/>
<UnstyledButton
onClick={handleBackToTypePicker}
py={6}
px={0}
mb={showOptions ? "xs" : 0}
>
<Group gap={8} wrap="nowrap">
{TypeIcon && <TypeIcon size={14} />}
<Text size="sm" style={{ flex: 1 }}>
{selectedTypeLabel}
</Text>
<IconChevronRight size={14} />
</Group>
</UnstyledButton>
{showOptions && (
<>
<Divider mb="xs" />
<ScrollArea.Autosize mah={300} scrollbarSize={6} offsetScrollbars>
<PropertyOptions
property={syntheticProperty}
onUpdate={handleOptionsUpdate}
onClose={noop}
onDirtyChange={noop}
hideButtons
/>
</ScrollArea.Autosize>
</>
)}
<Divider my="xs" />
<Group gap="xs" justify="flex-end">
<Button variant="default" size="xs" onClick={attemptClose}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleCreate}>
{t("Create field")}
</Button>
</Group>
</Stack>
)}
{panel === "confirmDiscard" && (
<Stack gap="xs" p="sm">
<Text size="sm" fw={600}>
{t("Unsaved changes")}
</Text>
<Text size="xs" c="dimmed">
{t("You have unsaved changes. Do you want to discard them?")}
</Text>
<Group gap="xs" justify="flex-end">
<Button
variant="default"
size="xs"
onClick={handleCancelDiscard}
>
{t("Keep editing")}
</Button>
<Button
color="red"
size="xs"
onClick={handleConfirmDiscard}
>
{t("Discard")}
</Button>
</Group>
</Stack>
)}
</Popover.Dropdown>
</Popover>
</>
);
}
@@ -5,6 +5,7 @@ import {
SelectTypeOptions,
NumberTypeOptions,
DateTypeOptions,
PersonTypeOptions,
Choice,
} from "@/features/base/types/base.types";
import { ChoiceEditor } from "./choice-editor";
@@ -15,9 +16,10 @@ type PropertyOptionsProps = {
onUpdate: (typeOptions: Record<string, unknown>) => void;
onClose: () => void;
onDirtyChange?: (dirty: boolean) => void;
hideButtons?: boolean;
};
export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange }: PropertyOptionsProps) {
export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange, hideButtons }: PropertyOptionsProps) {
const { t } = useTranslation();
switch (property.type) {
@@ -29,6 +31,7 @@ export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange }:
onUpdate={onUpdate}
onClose={onClose}
onDirtyChange={onDirtyChange}
hideButtons={hideButtons}
/>
);
case "status":
@@ -38,6 +41,7 @@ export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange }:
onUpdate={onUpdate}
onClose={onClose}
onDirtyChange={onDirtyChange}
hideButtons={hideButtons}
/>
);
case "number":
@@ -54,6 +58,13 @@ export function PropertyOptions({ property, onUpdate, onClose, onDirtyChange }:
onUpdate={onUpdate}
/>
);
case "person":
return (
<PersonOptions
property={property}
onUpdate={onUpdate}
/>
);
default:
return (
<Text size="xs" c="dimmed">
@@ -68,11 +79,13 @@ function SelectOptions({
onUpdate,
onClose,
onDirtyChange,
hideButtons,
}: {
property: IBaseProperty;
onUpdate: (typeOptions: Record<string, unknown>) => void;
onClose: () => void;
onDirtyChange?: (dirty: boolean) => void;
hideButtons?: boolean;
}) {
const options = property.typeOptions as SelectTypeOptions | undefined;
const choices = options?.choices ?? [];
@@ -95,6 +108,7 @@ function SelectOptions({
onClose={onClose}
onDirtyChange={onDirtyChange}
showCategories={false}
hideButtons={hideButtons}
/>
);
}
@@ -104,11 +118,13 @@ function StatusOptions({
onUpdate,
onClose,
onDirtyChange,
hideButtons,
}: {
property: IBaseProperty;
onUpdate: (typeOptions: Record<string, unknown>) => void;
onClose: () => void;
onDirtyChange?: (dirty: boolean) => void;
hideButtons?: boolean;
}) {
const options = property.typeOptions as SelectTypeOptions | undefined;
const choices = options?.choices ?? [];
@@ -131,6 +147,7 @@ function StatusOptions({
onClose={onClose}
onDirtyChange={onDirtyChange}
showCategories
hideButtons={hideButtons}
/>
);
}
@@ -215,3 +232,30 @@ function DateOptions({
</Stack>
);
}
function PersonOptions({
property,
onUpdate,
}: {
property: IBaseProperty;
onUpdate: (typeOptions: Record<string, unknown>) => void;
}) {
const { t } = useTranslation();
const options = property.typeOptions as PersonTypeOptions | undefined;
return (
<Stack gap="xs">
<Switch
size="xs"
label={t("Allow multiple people")}
checked={options?.allowMultiple !== false}
onChange={(e) =>
onUpdate({
...property.typeOptions,
allowMultiple: e.currentTarget.checked,
})
}
/>
</Stack>
);
}
@@ -1,4 +1,4 @@
import { UnstyledButton, Group, Text } from "@mantine/core";
import { UnstyledButton, Group, Text, TextInput } from "@mantine/core";
import {
IconLetterT,
IconHash,
@@ -15,9 +15,11 @@ import {
IconClockEdit,
IconUserEdit,
IconCheck,
IconSearch,
} from "@tabler/icons-react";
import { BasePropertyType } from "@/features/base/types/base.types";
import { useTranslation } from "react-i18next";
import { useState, useRef, useEffect } from "react";
import classes from "@/features/base/styles/cells.module.css";
const propertyTypes: {
@@ -45,21 +47,46 @@ type PropertyTypePickerProps = {
onSelect: (type: BasePropertyType) => void;
currentType?: BasePropertyType;
excludeTypes?: Set<BasePropertyType>;
showSearch?: boolean;
};
export function PropertyTypePicker({
onSelect,
currentType,
excludeTypes,
showSearch,
}: PropertyTypePickerProps) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
const searchRef = useRef<HTMLInputElement>(null);
const types = excludeTypes
? propertyTypes.filter(({ type }) => !excludeTypes.has(type))
: propertyTypes;
useEffect(() => {
if (showSearch) {
setTimeout(() => searchRef.current?.focus(), 0);
}
}, [showSearch]);
const types = propertyTypes
.filter(({ type }) => !excludeTypes?.has(type))
.filter(({ labelKey }) =>
!search || t(labelKey).toLowerCase().includes(search.toLowerCase())
);
return (
<>
{showSearch && (
<TextInput
ref={searchRef}
size="xs"
placeholder={t("Find a field type")}
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
mx="sm"
mt="sm"
mb={4}
/>
)}
{types.map(({ type, icon: Icon, labelKey }) => (
<UnstyledButton
key={type}
@@ -33,10 +33,19 @@ export function useBaseRowsQuery(
filters?: ViewFilterConfig[],
sorts?: ViewSortConfig[],
) {
// Normalize empty arrays to undefined so query keys stay stable
const activeFilters = filters?.length ? filters : undefined;
const activeSorts = sorts?.length ? sorts : undefined;
return useInfiniteQuery({
queryKey: ["base-rows", baseId, filters, sorts],
queryKey: ["base-rows", baseId, activeFilters, activeSorts],
queryFn: ({ pageParam }) =>
listRows(baseId!, { cursor: pageParam, limit: 100, filters, sorts }),
listRows(baseId!, {
cursor: pageParam,
limit: 100,
filters: activeFilters,
sorts: activeSorts,
}),
enabled: !!baseId,
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage: IPagination<IBaseRow>) =>
@@ -59,6 +59,10 @@ export type EmailTypeOptions = {
defaultValue?: string | null;
};
export type PersonTypeOptions = {
allowMultiple?: boolean;
};
export type TypeOptions =
| SelectTypeOptions
| NumberTypeOptions
@@ -67,6 +71,7 @@ export type TypeOptions =
| CheckboxTypeOptions
| UrlTypeOptions
| EmailTypeOptions
| PersonTypeOptions
| Record<string, unknown>;
export type IBaseProperty = {