mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Initial implementation of view sorting
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
41
desktop/src/components/databases/sorts/view-sort-button.tsx
Normal file
41
desktop/src/components/databases/sorts/view-sort-button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
87
desktop/src/components/databases/sorts/view-sort.tsx
Normal file
87
desktop/src/components/databases/sorts/view-sort.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
56
desktop/src/components/databases/sorts/view-sorts.tsx
Normal file
56
desktop/src/components/databases/sorts/view-sorts.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
29
desktop/src/components/databases/view-sorts-and-filters.tsx
Normal file
29
desktop/src/components/databases/view-sorts-and-filters.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
81
desktop/src/mutations/use-view-sort-create-mutation.tsx
Normal file
81
desktop/src/mutations/use-view-sort-create-mutation.tsx
Normal 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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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.*,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}'`;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user