Initial implementation of view sorting

This commit is contained in:
Hakan Shehu
2024-09-14 18:14:41 +02:00
parent ffe013f414
commit f7b697e8ab
24 changed files with 803 additions and 158 deletions

View File

@@ -37,7 +37,7 @@ export const BoardViewColumnRecords = ({
},
];
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =
useRecordsQuery(database.id, filters);
useRecordsQuery(database.id, filters, view.sorts);
const records = data ?? [];
return (

View File

@@ -2,9 +2,10 @@ import React from 'react';
import { BoardViewNode } from '@/types/databases';
import { ViewTabs } from '@/components/databases/view-tabs';
import { useDatabase } from '@/contexts/database';
import { ViewActionButton } from '@/components/databases/view-action-button';
import { ViewFilters } from '@/components/databases/filters/view-filters';
import { BoardViewColumn } from '@/components/databases/boards/board-view-column';
import { ViewSortsAndFilters } from '@/components/databases/view-sorts-and-filters';
import { ViewSortButton } from '@/components/databases/sorts/view-sort-button';
import { ViewFilterButton } from '@/components/databases/filters/view-filter.button';
interface BoardViewProps {
node: BoardViewNode;
@@ -12,8 +13,7 @@ interface BoardViewProps {
export const BoardView = ({ node }: BoardViewProps) => {
const database = useDatabase();
const [openFilters, setOpenFilters] = React.useState(true);
const [openSort, setOpenSort] = React.useState(false);
const [openSortsAndFilters, setOpenSortsAndFilters] = React.useState(true);
const groupByField = database.fields.find(
(field) => field.id === node.groupBy,
@@ -28,17 +28,27 @@ export const BoardView = ({ node }: BoardViewProps) => {
<div className="mt-2 flex flex-row justify-between border-b">
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<ViewActionButton
icon="sort-desc"
onClick={() => setOpenSort((prev) => !prev)}
<ViewSortButton
viewId={node.id}
sorts={node.sorts}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
<ViewActionButton
icon="filter-line"
onClick={() => setOpenFilters((prev) => !prev)}
<ViewFilterButton
viewId={node.id}
filters={node.filters}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
</div>
</div>
{openFilters && <ViewFilters viewId={node.id} filters={node.filters} />}
{openSortsAndFilters && (
<ViewSortsAndFilters
viewId={node.id}
filters={node.filters}
sorts={node.sorts}
/>
)}
<div className="mt-2 flex w-full min-w-full max-w-full flex-row gap-2 overflow-auto pr-5">
{groupByField.options.map((option) => {
return (

View File

@@ -51,7 +51,7 @@ export const CalendarViewGrid = ({ view, field }: CalendarViewGridProps) => {
];
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =
useRecordsQuery(database.id, filters);
useRecordsQuery(database.id, filters, view.sorts);
if (isPending) {
return null;

View File

@@ -2,9 +2,10 @@ import React from 'react';
import { CalendarViewNode } from '@/types/databases';
import { ViewTabs } from '@/components/databases/view-tabs';
import { useDatabase } from '@/contexts/database';
import { ViewActionButton } from '@/components/databases/view-action-button';
import { ViewFilters } from '@/components/databases/filters/view-filters';
import { CalendarViewGrid } from './calendar-view-grid';
import { CalendarViewGrid } from '@/components/databases/calendars/calendar-view-grid';
import { ViewSortsAndFilters } from '@/components/databases/view-sorts-and-filters';
import { ViewSortButton } from '@/components/databases/sorts/view-sort-button';
import { ViewFilterButton } from '@/components/databases/filters/view-filter.button';
interface CalendarViewProps {
node: CalendarViewNode;
@@ -13,8 +14,7 @@ interface CalendarViewProps {
export const CalendarView = ({ node }: CalendarViewProps) => {
const database = useDatabase();
const [openFilters, setOpenFilters] = React.useState(true);
const [openSort, setOpenSort] = React.useState(false);
const [openSortsAndFilters, setOpenSortsAndFilters] = React.useState(true);
const groupByField = database.fields.find(
(field) => field.id === node.groupBy,
@@ -29,17 +29,27 @@ export const CalendarView = ({ node }: CalendarViewProps) => {
<div className="mt-2 flex flex-row justify-between border-b">
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<ViewActionButton
icon="sort-desc"
onClick={() => setOpenSort((prev) => !prev)}
<ViewSortButton
viewId={node.id}
sorts={node.sorts}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
<ViewActionButton
icon="filter-line"
onClick={() => setOpenFilters((prev) => !prev)}
<ViewFilterButton
viewId={node.id}
filters={node.filters}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
</div>
</div>
{openFilters && <ViewFilters viewId={node.id} filters={node.filters} />}
{openSortsAndFilters && (
<ViewSortsAndFilters
viewId={node.id}
filters={node.filters}
sorts={node.sorts}
/>
)}
<div className="mt-2 w-full min-w-full max-w-full overflow-auto pr-5">
<CalendarViewGrid view={node} field={groupByField} />
</div>

View File

@@ -18,15 +18,19 @@ import { ViewFilterNode } from '@/types/databases';
import { getFieldFilterOperators, getFieldIcon } from '@/lib/databases';
import { useViewFilterCreateMutation } from '@/mutations/use-view-filter-create-mutation';
interface ViewAddFilterButtonProps {
interface ViewFilterAddPopoverProps {
viewId: string;
existingFilters: ViewFilterNode[];
children: React.ReactNode;
onCreate?: () => void;
}
export const ViewAddFilterButton = ({
export const ViewFilterAddPopover = ({
viewId,
existingFilters,
}: ViewAddFilterButtonProps) => {
children,
onCreate,
}: ViewFilterAddPopoverProps) => {
const database = useDatabase();
const { mutate, isPending } = useViewFilterCreateMutation();
@@ -41,12 +45,7 @@ export const ViewAddFilterButton = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button className="flex cursor-pointer flex-row items-center gap-1 rounded-lg p-1 text-sm text-muted-foreground hover:bg-gray-50">
<Icon name="add-line" />
Add filter
</button>
</PopoverTrigger>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent className="w-96 p-1">
<Command className="min-h-min">
<CommandInput placeholder="Search fields..." className="h-9" />
@@ -62,12 +61,19 @@ export const ViewAddFilterButton = ({
}
const operators = getFieldFilterOperators(field.dataType);
mutate({
viewId,
fieldId: field.id,
operator: operators[0].value,
});
setOpen(false);
mutate(
{
viewId,
fieldId: field.id,
operator: operators[0].value,
},
{
onSuccess: (data) => {
onCreate?.();
setOpen(false);
},
},
);
}}
>
<div className="flex w-full flex-row items-center gap-2">

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { ViewFilterNode } from '@/types/databases';
import { ViewFilterAddPopover } from '@/components/databases/filters/view-filter-add-popover';
import { Icon } from '@/components/ui/icon';
interface ViewFilterButtonProps {
viewId: string;
filters: ViewFilterNode[];
open: boolean;
setOpen: (open: boolean) => void;
}
export const ViewFilterButton = ({
viewId,
filters,
open,
setOpen,
}: ViewFilterButtonProps) => {
if (filters.length > 0) {
return (
<button
className="flex cursor-pointer items-center rounded-md p-1.5 hover:bg-gray-50"
onClick={() => setOpen(!open)}
>
<Icon name="filter-line" />
</button>
);
}
return (
<ViewFilterAddPopover
viewId={viewId}
existingFilters={filters}
onCreate={() => setOpen(true)}
>
<button className="flex cursor-pointer items-center rounded-md p-1.5 hover:bg-gray-50">
<Icon name="filter-line" />
</button>
</ViewFilterAddPopover>
);
};

View File

@@ -2,11 +2,12 @@ import React from 'react';
import { ViewFilterNode } from '@/types/databases';
import { useDatabase } from '@/contexts/database';
import { ViewTextFieldFilter } from '@/components/databases/filters/view-text-field-filter';
import { ViewAddFilterButton } from '@/components/databases/filters/view-filter-add-button';
import { ViewNumberFieldFilter } from '@/components/databases/filters/view-number-field-filter';
import { ViewEmailFieldFilter } from '@/components/databases/filters/view-email-field-filter';
import { ViewUrlFieldFilter } from '@/components/databases/filters/view-url-field-filter';
import { ViewPhoneFieldFilter } from '@/components/databases/filters/view-phone-field-filter';
import { ViewFilterAddPopover } from './view-filter-add-popover';
import { Icon } from '@/components/ui/icon';
interface ViewFiltersProps {
viewId: string;
@@ -17,7 +18,7 @@ export const ViewFilters = ({ viewId, filters }: ViewFiltersProps) => {
const database = useDatabase();
return (
<div className="mt-3 flex flex-row items-center gap-2">
<div className="flex flex-row items-center gap-2">
{filters &&
filters.map((filter) => {
const field = database.fields.find(
@@ -91,7 +92,12 @@ export const ViewFilters = ({ viewId, filters }: ViewFiltersProps) => {
return null;
}
})}
<ViewAddFilterButton viewId={viewId} existingFilters={filters} />
<ViewFilterAddPopover viewId={viewId} existingFilters={filters}>
<button className="flex cursor-pointer flex-row items-center gap-1 rounded-lg p-1 text-sm text-muted-foreground hover:bg-gray-50">
<Icon name="add-line" />
Add filter
</button>
</ViewFilterAddPopover>
</div>
);
};

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { Icon } from '@/components/ui/icon';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { useDatabase } from '@/contexts/database';
import { getFieldIcon, isSortableField } from '@/lib/databases';
import { useViewSortCreateMutation } from '@/mutations/use-view-sort-create-mutation';
import { SortDirections } from '@/lib/constants';
import { ViewSortNode } from '@/types/databases';
interface ViewSortAddPopoverProps {
viewId: string;
existingSorts: ViewSortNode[];
children: React.ReactNode;
onCreate?: () => void;
}
export const ViewSortAddPopover = ({
viewId,
existingSorts,
onCreate,
children,
}: ViewSortAddPopoverProps) => {
const database = useDatabase();
const { mutate, isPending } = useViewSortCreateMutation();
const [open, setOpen] = React.useState(false);
const sortableFields = database.fields.filter(
(field) =>
isSortableField(field) &&
!existingSorts.some((sort) => sort.fieldId === field.id),
);
if (sortableFields.length === 0) {
return null;
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent className="w-96 p-1">
<Command className="min-h-min">
<CommandInput placeholder="Search fields..." className="h-9" />
<CommandEmpty>No sortable field found.</CommandEmpty>
<CommandList>
<CommandGroup className="h-min max-h-96">
{sortableFields.map((field) => (
<CommandItem
key={field.id}
onSelect={() => {
if (isPending) {
return;
}
mutate(
{
viewId,
fieldId: field.id,
direction: SortDirections.Ascending,
},
{
onSuccess: () => {
onCreate?.();
setOpen(false);
},
},
);
}}
>
<div className="flex w-full flex-row items-center gap-2">
<Icon name={getFieldIcon(field.dataType)} />
<p>{field.name}</p>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { ViewSortNode } from '@/types/databases';
import { Icon } from '@/components/ui/icon';
import { ViewSortAddPopover } from '@/components/databases/sorts/view-sort-add-popover';
interface ViewSortButtonProps {
viewId: string;
sorts: ViewSortNode[];
open: boolean;
setOpen: (open: boolean) => void;
}
export const ViewSortButton = ({
viewId,
sorts,
open,
setOpen,
}: ViewSortButtonProps) => {
if (sorts.length > 0) {
return (
<button
className="flex cursor-pointer items-center rounded-md p-1.5 hover:bg-gray-50"
onClick={() => setOpen(!open)}
>
<Icon name="sort-desc" />
</button>
);
}
return (
<ViewSortAddPopover
viewId={viewId}
existingSorts={sorts}
onCreate={() => setOpen(true)}
>
<button className="flex cursor-pointer items-center rounded-md p-1.5 hover:bg-gray-50">
<Icon name="sort-desc" />
</button>
</ViewSortAddPopover>
);
};

View File

@@ -0,0 +1,87 @@
import { Icon } from '@/components/ui/icon';
import { getFieldIcon } from '@/lib/databases';
import { FieldNode, ViewSortNode } from '@/types/databases';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import React from 'react';
import { Button } from '@/components/ui/button';
import { useNodeAttributeUpsertMutation } from '@/mutations/use-node-attribute-upsert-mutation';
import { useNodeDeleteMutation } from '@/mutations/use-node-delete-mutation';
import { AttributeTypes, SortDirections } from '@/lib/constants';
interface ViewSortProps {
sort: ViewSortNode;
field: FieldNode;
}
export const ViewSort = ({ sort, field }: ViewSortProps) => {
const { mutate: upsertAttribute } = useNodeAttributeUpsertMutation();
const { mutate: deleteSort } = useNodeDeleteMutation();
return (
<div className="flex flex-row items-center gap-3 text-sm">
<div className="flex flex-row items-center gap-0.5 p-1">
<Icon name={getFieldIcon(field.dataType)} className="h-4 w-4" />
<p>{field.name}</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex flex-grow flex-row items-center gap-1 rounded-md p-1 font-semibold hover:cursor-pointer hover:bg-gray-100">
<p>
{sort.direction === SortDirections.Ascending
? 'Ascending'
: 'Descending'}
</p>
<Icon
name="arrow-down-s-line"
className="h-4 w-4 text-muted-foreground"
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => {
upsertAttribute({
nodeId: sort.id,
type: AttributeTypes.Direction,
key: '1',
textValue: SortDirections.Ascending,
numberValue: null,
foreignNodeId: null,
});
}}
>
<p>Ascending</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
upsertAttribute({
nodeId: sort.id,
type: AttributeTypes.Direction,
key: '1',
textValue: SortDirections.Descending,
numberValue: null,
foreignNodeId: null,
});
}}
>
<p>Descending</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="ghost"
size="icon"
onClick={() => {
deleteSort(sort.id);
}}
>
<Icon name="delete-bin-line" className="h-4 w-4" />
</Button>
</div>
);
};

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { ViewSortNode } from '@/types/databases';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { ViewSort } from '@/components/databases/sorts/view-sort';
import { useDatabase } from '@/contexts/database';
import { ViewSortAddPopover } from './view-sort-add-popover';
import { Icon } from '@/components/ui/icon';
interface ViewSortsProps {
viewId: string;
sorts: ViewSortNode[];
}
export const ViewSorts = ({ viewId, sorts }: ViewSortsProps) => {
const database = useDatabase();
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="border-dashed text-xs text-muted-foreground"
>
Sorts ({sorts.length})
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col gap-2 p-2">
{sorts.map((sort) => {
const field = database.fields.find(
(field) => field.id === sort.fieldId,
);
if (!field) return null;
return <ViewSort key={sort.id} sort={sort} field={field} />;
})}
<ViewSortAddPopover
viewId={viewId}
existingSorts={sorts}
onCreate={() => setOpen(true)}
>
<button className="flex cursor-pointer flex-row items-center gap-1 rounded-lg p-1 text-sm text-muted-foreground hover:bg-gray-50">
<Icon name="add-line" />
Add sort
</button>
</ViewSortAddPopover>
</PopoverContent>
</Popover>
);
};

View File

@@ -11,7 +11,7 @@ export const TableViewBody = () => {
const tableView = useTableView();
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =
useRecordsQuery(database.id, tableView.filters);
useRecordsQuery(database.id, tableView.filters, tableView.sorts);
const records = data ?? [];
return (

View File

@@ -10,11 +10,12 @@ import { ViewTabs } from '@/components/databases/view-tabs';
import { TableViewSettingsPopover } from '@/components/databases/tables/table-view-settings-popover';
import { getDefaultFieldWidth, getDefaultNameWidth } from '@/lib/databases';
import { generateNodeIndex } from '@/lib/nodes';
import { ViewFilters } from '@/components/databases/filters/view-filters';
import { ViewActionButton } from '@/components/databases/view-action-button';
import { useNodeAttributeUpsertMutation } from '@/mutations/use-node-attribute-upsert-mutation';
import { useNodeAttributeDeleteMutation } from '@/mutations/use-node-attribute-delete-mutation';
import { AttributeTypes } from '@/lib/constants';
import { ViewSortsAndFilters } from '@/components/databases/view-sorts-and-filters';
import { ViewFilterButton } from '@/components/databases/filters/view-filter.button';
import { ViewSortButton } from '@/components/databases/sorts/view-sort-button';
interface TableViewProps {
node: TableViewNode;
@@ -39,8 +40,7 @@ export const TableView = ({ node }: TableViewProps) => {
node.nameWidth ?? getDefaultNameWidth(),
);
const [openFilters, setOpenFilters] = React.useState(true);
const [openSort, setOpenSort] = React.useState(false);
const [openSortsAndFilters, setOpenSortsAndFilters] = React.useState(false);
React.useEffect(() => {
setHiddenFields(node.hiddenFields ?? []);
@@ -67,6 +67,7 @@ export const TableView = ({ node }: TableViewProps) => {
name: node.name,
fields,
filters: node.filters,
sorts: node.sorts,
hideField: (id: string) => {
if (hiddenFields.includes(id)) {
return;
@@ -204,17 +205,27 @@ export const TableView = ({ node }: TableViewProps) => {
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<TableViewSettingsPopover />
<ViewActionButton
icon="sort-desc"
onClick={() => setOpenSort((prev) => !prev)}
<ViewSortButton
viewId={node.id}
sorts={node.sorts}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
<ViewActionButton
icon="filter-line"
onClick={() => setOpenFilters((prev) => !prev)}
<ViewFilterButton
viewId={node.id}
filters={node.filters}
open={openSortsAndFilters}
setOpen={setOpenSortsAndFilters}
/>
</div>
</div>
{openFilters && <ViewFilters viewId={node.id} filters={node.filters} />}
{openSortsAndFilters && (
<ViewSortsAndFilters
viewId={node.id}
filters={node.filters}
sorts={node.sorts}
/>
)}
<div className="mt-2 w-full min-w-full max-w-full overflow-auto pr-5">
<TableViewHeader />
<TableViewBody />

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { Icon } from '@/components/ui/icon';
interface ViewActionButtonProps {
onClick: () => void;
icon: string;
}
export const ViewActionButton = ({ onClick, icon }: ViewActionButtonProps) => {
return (
<button
className="flex cursor-pointer items-center rounded-md p-1.5 hover:bg-gray-50"
onClick={onClick}
>
<Icon name={icon} />
</button>
);
};

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { ViewSorts } from '@/components/databases/sorts/view-sorts';
import { ViewFilters } from '@/components/databases/filters/view-filters';
import { ViewFilterNode, ViewSortNode } from '@/types/databases';
import { Separator } from '@/components/ui/separator';
interface ViewSortsAndFiltersProps {
viewId: string;
filters: ViewFilterNode[];
sorts: ViewSortNode[];
}
export const ViewSortsAndFilters = ({
viewId,
filters,
sorts,
}: ViewSortsAndFiltersProps) => {
return (
<div className="mt-3 flex flex-row items-center gap-2">
{sorts.length > 0 && (
<React.Fragment>
<ViewSorts viewId={viewId} sorts={sorts} />
<Separator orientation="vertical" className="mx-1 h-4" />
</React.Fragment>
)}
<ViewFilters viewId={viewId} filters={filters} />
</div>
);
};

View File

@@ -1,4 +1,9 @@
import { FieldNode, FieldDataType, ViewFilterNode } from '@/types/databases';
import {
FieldNode,
FieldDataType,
ViewFilterNode,
ViewSortNode,
} from '@/types/databases';
import { createContext, useContext } from 'react';
interface TableViewContext {
@@ -6,6 +11,7 @@ interface TableViewContext {
name: string;
fields: FieldNode[];
filters: ViewFilterNode[];
sorts: ViewSortNode[];
hideField: (id: string) => void;
showField: (id: string) => void;
isHiddenField: (id: string) => boolean;

View File

@@ -27,6 +27,7 @@ export const NodeTypes = {
Field: 'field',
SelectOption: 'select_option',
ViewFilter: 'view_filter',
ViewSort: 'view_sort',
};
export const EditorNodeTypes = {
@@ -83,4 +84,10 @@ export const AttributeTypes = {
Operator: 'operator',
Value: 'value',
GroupBy: 'group_by',
Direction: 'direction',
};
export const SortDirections = {
Ascending: 'asc',
Descending: 'desc',
};

View File

@@ -1002,3 +1002,16 @@ const recordMatchesUrlFilter = (
return false;
};
export const isSortableField = (field: FieldNode) => {
return (
field.dataType === 'text' ||
field.dataType === 'number' ||
field.dataType === 'date' ||
field.dataType === 'created_at' ||
field.dataType === 'email' ||
field.dataType === 'phone' ||
field.dataType === 'select' ||
field.dataType === 'url'
);
};

View File

@@ -37,6 +37,7 @@ enum IdType {
Field = 'fi',
SelectOption = 'so',
ViewFilter = 'vf',
ViewSort = 'vs',
}
export class NeuronId {
@@ -108,6 +109,8 @@ export class NeuronId {
return IdType.SelectOption;
case NodeTypes.ViewFilter:
return IdType.ViewFilter;
case NodeTypes.ViewSort:
return IdType.ViewSort;
default:
return IdType.Node;
}

View File

@@ -0,0 +1,81 @@
import { useWorkspace } from '@/contexts/workspace';
import { AttributeTypes, NodeTypes } from '@/lib/constants';
import { NeuronId } from '@/lib/id';
import { generateNodeIndex } from '@/lib/nodes';
import { useMutation } from '@tanstack/react-query';
interface CreateViewSortInput {
viewId: string;
fieldId: string;
direction: string;
}
export const useViewSortCreateMutation = () => {
const workspace = useWorkspace();
return useMutation({
mutationFn: async (input: CreateViewSortInput) => {
const lastViewSortQuery = workspace.schema
.selectFrom('nodes')
.where((eb) =>
eb.and({
parent_id: input.viewId,
type: NodeTypes.ViewSort,
}),
)
.selectAll()
.orderBy('index', 'desc')
.limit(1)
.compile();
const result = await workspace.query(lastViewSortQuery);
const lastViewSort =
result.rows && result.rows.length > 0 ? result.rows[0] : null;
const maxIndex = lastViewSort?.index ? lastViewSort.index : null;
const viewSortId = NeuronId.generate(NeuronId.Type.ViewSort);
const insertViewSortQuery = workspace.schema
.insertInto('nodes')
.values({
id: viewSortId,
type: NodeTypes.ViewSort,
parent_id: input.viewId,
index: generateNodeIndex(maxIndex, null),
content: null,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
const insertViewSortAttributesQuery = workspace.schema
.insertInto('node_attributes')
.values([
{
node_id: viewSortId,
type: AttributeTypes.FieldId,
key: '1',
foreign_node_id: input.fieldId,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
},
{
node_id: viewSortId,
type: AttributeTypes.Direction,
key: '1',
text_value: input.direction,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
},
])
.compile();
await workspace.mutate([
insertViewSortQuery,
insertViewSortAttributesQuery,
]);
return viewSortId;
},
});
};

View File

@@ -33,11 +33,6 @@ export const useDatabaseQuery = (databaseId: string) => {
FROM nodes
WHERE parent_id = ${databaseId} AND type = ${NodeTypes.Field}
),
view_nodes AS (
SELECT *
FROM nodes
WHERE parent_id = ${databaseId} AND type IN (${sql.join(ViewNodeTypes)})
),
select_option_nodes AS (
SELECT *
FROM nodes
@@ -48,26 +43,12 @@ export const useDatabaseQuery = (databaseId: string) => {
)
AND type = ${NodeTypes.SelectOption}
),
view_filter_nodes AS (
SELECT *
FROM nodes
WHERE parent_id IN
(
SELECT id
FROM view_nodes
)
AND type = ${NodeTypes.ViewFilter}
),
all_nodes AS (
SELECT * FROM database_node
UNION ALL
SELECT * FROM field_nodes
UNION ALL
SELECT * FROM view_nodes
UNION ALL
SELECT * FROM select_option_nodes
UNION ALL
SELECT * FROM view_filter_nodes
)
SELECT
n.*,

View File

@@ -9,6 +9,7 @@ import {
ViewFilterNode,
ViewFilterValueNode,
ViewNode,
ViewSortNode,
} from '@/types/databases';
import { LocalNodeWithAttributes } from '@/types/nodes';
import { useQuery } from '@tanstack/react-query';
@@ -22,10 +23,10 @@ export const useDatabaseViewsQuery = (databaseId: string) => {
ViewNode[],
string[]
>({
queryKey: ['database', databaseId],
queryKey: ['database-views', databaseId],
queryFn: async ({ queryKey }) => {
const query = sql<SelectNodeWithAttributes>`
view_nodes AS (
WITH view_nodes AS (
SELECT *
FROM nodes
WHERE parent_id = ${databaseId} AND type IN (${sql.join(ViewNodeTypes)})
@@ -40,10 +41,22 @@ export const useDatabaseViewsQuery = (databaseId: string) => {
)
AND type = ${NodeTypes.ViewFilter}
),
view_sort_nodes AS (
SELECT *
FROM nodes
WHERE parent_id IN
(
SELECT id
FROM view_nodes
)
AND type = ${NodeTypes.ViewSort}
),
all_nodes AS (
SELECT * FROM view_nodes
UNION ALL
SELECT * FROM view_filter_nodes
UNION ALL
SELECT * FROM view_sort_nodes
)
SELECT
n.*,
@@ -95,7 +108,11 @@ const buildViewNodes = (rows: SelectNodeWithAttributes[]): ViewNode[] => {
(node) =>
node.type === NodeTypes.ViewFilter && node.parentId === viewNode.id,
);
const view = buildViewNode(viewNode, viewFilters);
const viewSorts = nodes.filter(
(node) =>
node.type === NodeTypes.ViewSort && node.parentId === viewNode.id,
);
const view = buildViewNode(viewNode, viewFilters, viewSorts);
if (view) {
views.push(view);
}
@@ -107,13 +124,14 @@ const buildViewNodes = (rows: SelectNodeWithAttributes[]): ViewNode[] => {
const buildViewNode = (
node: LocalNodeWithAttributes,
filters: LocalNodeWithAttributes[],
sorts: LocalNodeWithAttributes[],
): ViewNode | null => {
if (node.type === NodeTypes.TableView) {
return buildTableViewNode(node, filters);
return buildTableViewNode(node, filters, sorts);
} else if (node.type === NodeTypes.BoardView) {
return buildBoardViewNode(node, filters);
return buildBoardViewNode(node, filters, sorts);
} else if (node.type === NodeTypes.CalendarView) {
return buildCalendarViewNode(node, filters);
return buildCalendarViewNode(node, filters, sorts);
}
return null;
@@ -122,6 +140,7 @@ const buildViewNode = (
const buildTableViewNode = (
node: LocalNodeWithAttributes,
filters: LocalNodeWithAttributes[],
sorts: LocalNodeWithAttributes[],
): TableViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
@@ -160,6 +179,7 @@ const buildTableViewNode = (
)?.numberValue;
const viewFilters = filters.map(buildViewFilterNode);
const viewSorts = sorts.map(buildViewSortNode);
return {
id: node.id,
@@ -171,12 +191,14 @@ const buildTableViewNode = (
nameWidth: nameWidth,
versionId: node.versionId,
filters: viewFilters,
sorts: viewSorts,
};
};
const buildBoardViewNode = (
node: LocalNodeWithAttributes,
filters: LocalNodeWithAttributes[],
sorts: LocalNodeWithAttributes[],
): BoardViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
@@ -187,12 +209,14 @@ const buildBoardViewNode = (
)?.foreignNodeId;
const viewFilters = filters.map(buildViewFilterNode);
const viewSorts = sorts.map(buildViewSortNode);
return {
id: node.id,
name: name ?? 'Unnamed',
type: 'board_view',
filters: viewFilters,
sorts: viewSorts,
groupBy,
};
};
@@ -200,6 +224,7 @@ const buildBoardViewNode = (
const buildCalendarViewNode = (
node: LocalNodeWithAttributes,
filters: LocalNodeWithAttributes[],
sorts: LocalNodeWithAttributes[],
): CalendarViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
@@ -210,12 +235,14 @@ const buildCalendarViewNode = (
)?.foreignNodeId;
const viewFilters = filters.map(buildViewFilterNode);
const viewSorts = sorts.map(buildViewSortNode);
return {
id: node.id,
name: name ?? 'Unnamed',
type: 'calendar_view',
filters: viewFilters,
sorts: viewSorts,
groupBy,
};
};
@@ -244,3 +271,19 @@ const buildViewFilterNode = (node: LocalNodeWithAttributes): ViewFilterNode => {
values,
};
};
const buildViewSortNode = (node: LocalNodeWithAttributes): ViewSortNode => {
const fieldId = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.FieldId,
)?.foreignNodeId;
const direction = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Direction,
)?.textValue;
return {
id: node.id,
fieldId,
direction: direction as any,
};
};

View File

@@ -17,6 +17,7 @@ import {
TextFieldNode,
UrlFieldNode,
ViewFilterNode,
ViewSortNode,
} from '@/types/databases';
import { User } from '@/types/users';
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
@@ -28,12 +29,16 @@ const RECORDS_PER_PAGE = 50;
export const useRecordsQuery = (
databaseId: string,
filters: ViewFilterNode[],
sorts: ViewSortNode[],
) => {
const workspace = useWorkspace();
const database = useDatabase();
const json = filters.length > 0 ? JSON.stringify(filters) : '';
const hash = filters.length > 0 ? sha256(json) : '';
let hash = '';
if (filters.length > 0 || sorts.length > 0) {
const json = JSON.stringify({ filters, sorts });
hash = sha256(json);
}
return useInfiniteQuery<
QueryResult<SelectNodeWithAttributes>,
@@ -58,17 +63,23 @@ export const useRecordsQuery = (
},
queryFn: async ({ queryKey, pageParam }) => {
const offset = pageParam * RECORDS_PER_PAGE;
const filterQuery = buildFiltersQuery(filters, database.fields);
const joinsQuery = buildSortJoinsQuery(sorts, database.fields);
const orderByQuery = `ORDER BY ${sorts.length > 0 ? buildSortOrdersQuery(sorts, database.fields) : 'n."index" ASC'}`;
const query = sql<SelectNodeWithAttributes>`
WITH record_nodes AS (
SELECT *
FROM nodes
WHERE parent_id = ${databaseId} AND type = ${NodeTypes.Record} ${sql.raw(buildFiltersQuery(filters, database.fields))}
ORDER BY ${sql.ref('index')} ASC
SELECT n.*, ROW_NUMBER() OVER (${sql.raw(orderByQuery)}) AS order_number
FROM nodes n
${sql.raw(joinsQuery)}
WHERE n.parent_id = ${databaseId} AND n.type = ${NodeTypes.Record} ${sql.raw(filterQuery)}
${sql.raw(orderByQuery)}
LIMIT ${sql.lit(RECORDS_PER_PAGE)}
OFFSET ${sql.lit(offset)}
),
author_nodes AS (
SELECT *
SELECT *, NULL AS order_number
FROM nodes
WHERE id IN (SELECT DISTINCT created_by FROM record_nodes)
),
@@ -99,7 +110,8 @@ export const useRecordsQuery = (
) as attributes
FROM all_nodes n
LEFT JOIN node_attributes na ON n.id = na.node_id
GROUP BY n.id;
GROUP BY n.id
ORDER BY n.order_number ASC
`.compile(workspace.schema);
return await workspace.queryAndSubscribe({
@@ -166,7 +178,7 @@ const buildRecords = (rows: SelectNodeWithAttributes[]): RecordNode[] => {
records.push(record);
}
return records.sort((a, b) => compareString(a.index, b.index));
return records;
};
const buildFiltersQuery = (
@@ -193,7 +205,7 @@ const buildFiltersQuery = (
.map((query) => query.whereQuery)
.filter((query) => query !== null && query.length > 0);
return `AND id IN
return `AND n.id IN
(
SELECT na.node_id
FROM node_attributes na
@@ -255,14 +267,14 @@ const buildBooleanFilterQuery = (
): FilterQuery | null => {
if (filter.operator === 'is_true') {
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = 1`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = 1`,
whereQuery: null,
};
}
if (filter.operator === 'is_false') {
return {
joinQuery: buildLeftJoinNodeAttributesQuery(filter.id, field.id),
joinQuery: buildFilterLeftJoinNodeAttributesQuery(filter.id, field.id),
whereQuery: `na_${filter.id}.node_id IS NULL OR na_${filter.id}.number_value = 0`,
};
}
@@ -294,32 +306,32 @@ const buildNumberFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = ${value}`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value != ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value != ${value}`,
whereQuery: null,
};
case 'is_greater_than':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value > ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value > ${value}`,
whereQuery: null,
};
case 'is_less_than':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value < ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value < ${value}`,
whereQuery: null,
};
case 'is_greater_than_or_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value >= ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value >= ${value}`,
whereQuery: null,
};
case 'is_less_than_or_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value <= ${value}`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value <= ${value}`,
whereQuery: null,
};
default:
@@ -351,32 +363,32 @@ const buildTextFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
whereQuery: null,
};
case 'contains':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
whereQuery: null,
};
case 'does_not_contain':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
whereQuery: null,
};
case 'starts_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
whereQuery: null,
};
case 'ends_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
whereQuery: null,
};
default:
@@ -408,32 +420,32 @@ const buildEmailFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
whereQuery: null,
};
case 'contains':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
whereQuery: null,
};
case 'does_not_contain':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
whereQuery: null,
};
case 'starts_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
whereQuery: null,
};
case 'ends_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
whereQuery: null,
};
default:
@@ -465,32 +477,32 @@ const buildPhoneFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
whereQuery: null,
};
case 'contains':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
whereQuery: null,
};
case 'does_not_contain':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
whereQuery: null,
};
case 'starts_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
whereQuery: null,
};
case 'ends_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
whereQuery: null,
};
default:
@@ -522,32 +534,32 @@ const buildUrlFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
whereQuery: null,
};
case 'contains':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
whereQuery: null,
};
case 'does_not_contain':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
whereQuery: null,
};
case 'starts_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
whereQuery: null,
};
case 'ends_with':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
whereQuery: null,
};
default:
@@ -579,12 +591,12 @@ const buildSelectFilterQuery = (
switch (filter.operator) {
case 'is_in':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
whereQuery: null,
};
case 'is_not_in':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id NOT IN (${joinIds(ids)})`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id NOT IN (${joinIds(ids)})`,
whereQuery: null,
};
default:
@@ -616,12 +628,12 @@ const buildMultiSelectFilterQuery = (
switch (filter.operator) {
case 'is_in':
return {
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
joinQuery: `${buildFilterJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
whereQuery: null,
};
case 'is_not_in':
return {
joinQuery: `${buildLeftJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
joinQuery: `${buildFilterLeftJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
whereQuery: `na_${filter.id}.node_id IS NULL`,
};
default:
@@ -651,32 +663,32 @@ const buildCreatedAtFilterQuery = (
switch (filter.operator) {
case 'is_equal_to':
return {
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) = '${dateString}'`,
joinQuery: `${buildFilterJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) = '${dateString}'`,
whereQuery: null,
};
case 'is_not_equal_to':
return {
joinQuery: `${buildLeftJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) != '${dateString}'`,
joinQuery: `${buildFilterLeftJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) != '${dateString}'`,
whereQuery: null,
};
case 'is_on_or_after':
return {
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) >= '${dateString}'`,
joinQuery: `${buildFilterJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) >= '${dateString}'`,
whereQuery: null,
};
case 'is_on_or_before':
return {
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) <= '${dateString}'`,
joinQuery: `${buildFilterJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) <= '${dateString}'`,
whereQuery: null,
};
case 'is_after':
return {
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) > '${dateString}'`,
joinQuery: `${buildFilterJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) > '${dateString}'`,
whereQuery: null,
};
case 'is_before':
return {
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) < '${dateString}'`,
joinQuery: `${buildFilterJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) < '${dateString}'`,
whereQuery: null,
};
default:
@@ -689,7 +701,7 @@ const buildIsEmptyFilterQuery = (
fieldId: string,
): FilterQuery => {
return {
joinQuery: buildLeftJoinNodeAttributesQuery(filterId, fieldId),
joinQuery: buildFilterLeftJoinNodeAttributesQuery(filterId, fieldId),
whereQuery: `na_${filterId}.node_id IS NULL`,
};
};
@@ -699,33 +711,151 @@ const buildIsNotEmptyFilterQuery = (
fieldId: string,
): FilterQuery => {
return {
joinQuery: buildJoinNodeAttributesQuery(filterId, fieldId),
joinQuery: buildFilterJoinNodeAttributesQuery(filterId, fieldId),
whereQuery: null,
};
};
const buildJoinNodesQuery = (filterId: string): string => {
const buildFilterJoinNodesQuery = (filterId: string): string => {
return `JOIN nodes n_${filterId} ON n_${filterId}.id = na.node_id`;
};
const buildLeftJoinNodesQuery = (filterId: string): string => {
const buildFilterLeftJoinNodesQuery = (filterId: string): string => {
return `LEFT JOIN nodes n_${filterId} ON n_${filterId}.id = na.node_id`;
};
const buildJoinNodeAttributesQuery = (
const buildFilterJoinNodeAttributesQuery = (
filterId: string,
fieldId: string,
): string => {
return `JOIN node_attributes na_${filterId} ON na_${filterId}.node_id = na.node_id AND na_${filterId}.type = '${fieldId}'`;
};
const buildLeftJoinNodeAttributesQuery = (
const buildFilterLeftJoinNodeAttributesQuery = (
filterId: string,
fieldId: string,
): string => {
return `LEFT JOIN node_attributes na_${filterId} ON na_${filterId}.node_id = na.node_id AND na_${filterId}.type = '${fieldId}'`;
return `LEFT JOIN node_attributes na_${filterId} ON na_${filterId}.node_id = na.node_id AND na_${filterId}.type = '${fieldId}'`;
};
const joinIds = (ids: string[]): string => {
return ids.map((id) => `'${id}'`).join(',');
};
const buildSortJoinsQuery = (
sorts: ViewSortNode[],
fields: FieldNode[],
): string => {
if (sorts.length === 0) {
return '';
}
const joinQueries = sorts
.map((sort) => buildSortJoinQuery(sort, fields))
.filter((query) => query !== null && query.length > 0);
return joinQueries.join(' ');
};
const buildSortJoinQuery = (
sort: ViewSortNode,
fields: FieldNode[],
): string | null => {
const field = fields.find((field) => field.id === sort.fieldId);
if (!field) {
return null;
}
switch (field.dataType) {
case 'boolean':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'collaborator':
return null;
case 'created_at':
return buildSortLeftJoinNodesQuery(sort.id);
case 'created_by':
return null;
case 'date':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'email':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'file':
return null;
case 'multi_select':
return null;
case 'number':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'phone':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'select':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'text':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
case 'url':
return buildSortLeftJoinNodeAttributesQuery(sort.id, field.id);
default:
return null;
}
};
const buildSortOrdersQuery = (
sorts: ViewSortNode[],
fields: FieldNode[],
): string => {
return sorts
.map((sort) => buildSortOrderQuery(sort, fields))
.filter((query) => query !== null && query.length > 0)
.join(', ');
};
const buildSortOrderQuery = (
sort: ViewSortNode,
fields: FieldNode[],
): string | null => {
const field = fields.find((field) => field.id === sort.fieldId);
if (!field) {
return null;
}
switch (field.dataType) {
case 'boolean':
return `na_${sort.id}.number_value ${sort.direction}`;
case 'collaborator':
return null;
case 'created_at':
return `n_${sort.id}.created_at ${sort.direction}`;
case 'created_by':
return null;
case 'date':
return `na_${sort.id}.text_value ${sort.direction}`;
case 'email':
return `na_${sort.id}.text_value ${sort.direction}`;
case 'file':
return null;
case 'multi_select':
return null;
case 'number':
return `na_${sort.id}.number_value ${sort.direction}`;
case 'phone':
return `na_${sort.id}.text_value ${sort.direction}`;
case 'select':
return `na_${sort.id}.text_value ${sort.direction}`;
case 'text':
return `na_${sort.id}.text_value ${sort.direction}`;
case 'url':
return `na_${sort.id}.text_value ${sort.direction}`;
default:
return null;
}
};
const buildSortLeftJoinNodesQuery = (sortId: string): string => {
return `LEFT JOIN nodes n_${sortId} ON n_${sortId}.id = n.id`;
};
const buildSortLeftJoinNodeAttributesQuery = (
sortId: string,
fieldId: string,
): string => {
return `LEFT JOIN node_attributes na_${sortId} ON na_${sortId}.node_id = n.id AND na_${sortId}.type = '${fieldId}'`;
};

View File

@@ -19,6 +19,7 @@ export type TableViewNode = {
nameWidth: number;
versionId: string;
filters: ViewFilterNode[];
sorts: ViewSortNode[];
};
export type BoardViewNode = {
@@ -26,6 +27,7 @@ export type BoardViewNode = {
name: string;
type: 'board_view';
filters: ViewFilterNode[];
sorts: ViewSortNode[];
groupBy: string | null;
};
@@ -34,6 +36,7 @@ export type CalendarViewNode = {
name: string;
type: 'calendar_view';
filters: ViewFilterNode[];
sorts: ViewSortNode[];
groupBy: string | null;
};
@@ -189,3 +192,9 @@ export type ViewFilterValueNode = {
numberValue: number | null;
foreignNodeId: string | null;
};
export type ViewSortNode = {
id: string;
fieldId: string;
direction: 'asc' | 'desc';
};