Implement view create mutation

This commit is contained in:
Hakan Shehu
2024-09-10 11:20:24 +02:00
parent 2b0d73cbbf
commit 626d113360
7 changed files with 474 additions and 13 deletions

View File

@@ -3,6 +3,7 @@ import { ViewTab } from '@/components/databases/view-tab';
import { ViewActions } from '@/components/databases/view-actions';
import { ViewNode } from '@/types/databases';
import { View } from '@/components/databases/view';
import { ViewCreateButton } from '@/components/databases/view-create-button';
interface DatabaseViewsProps {
views: ViewNode[];
@@ -16,7 +17,7 @@ export const DatabaseViews = ({ views }: DatabaseViewsProps) => {
return (
<div className="group/database">
<div className="mt-2 flex flex-row justify-between border-b">
<div className="flex flex-row gap-3">
<div className="flex flex-row items-center gap-3">
{views.map((view) => (
<ViewTab
key={view.id}
@@ -25,6 +26,7 @@ export const DatabaseViews = ({ views }: DatabaseViewsProps) => {
onClick={() => setSelectedView(view)}
/>
))}
<ViewCreateButton />
</div>
<ViewActions />
</div>

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { ViewCreateDialog } from '@/components/databases/view-create-dialog';
import { Icon } from '@/components/ui/icon';
export const ViewCreateButton = () => {
const [open, setOpen] = React.useState(false);
return (
<React.Fragment>
<Icon
name="add-line"
className="mb-1 cursor-pointer text-sm text-muted-foreground hover:text-foreground"
onClick={() => setOpen(true)}
/>
<ViewCreateDialog open={open} onOpenChange={setOpen} />
</React.Fragment>
);
};

View File

@@ -1,5 +1,207 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { cn } from '@/lib/utils';
import { Icon } from '@/components/ui/icon';
import { useTableViewCreateMutation } from '@/mutations/use-table-view-create-mutation';
import { useBoardViewCreateMutation } from '@/mutations/use-board-view-create-mutation';
import { useCalendarViewCreateMutation } from '@/mutations/use-calendar-view-create-mutation';
import { useDatabase } from '@/contexts/database';
export const ViewCreateDialog = () => {
return <div>ViewCreateDialog</div>;
const formSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters long.'),
type: z.enum(['TABLE', 'BOARD', 'CALENDAR']),
});
interface ViewTypeOption {
name: string;
icon: string;
type: 'TABLE' | 'BOARD' | 'CALENDAR';
}
const viewTypes: ViewTypeOption[] = [
{
name: 'Table',
icon: 'table-2',
type: 'TABLE',
},
{
name: 'Board',
icon: 'layout-column-line',
type: 'BOARD',
},
{
name: 'Calendar',
icon: 'calendar-2-line',
type: 'CALENDAR',
},
];
interface ViewCreateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const ViewCreateDialog = ({
open,
onOpenChange,
}: ViewCreateDialogProps) => {
const database = useDatabase();
const tableViewCreateMutation = useTableViewCreateMutation();
const boardViewCreateMutation = useBoardViewCreateMutation();
const calendarViewCreateMutation = useCalendarViewCreateMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
type: 'TABLE',
},
});
const isPending =
tableViewCreateMutation.isPending ||
boardViewCreateMutation.isPending ||
calendarViewCreateMutation.isPending;
const handleCancel = () => {
form.reset();
onOpenChange(false);
};
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
if (isPending) {
return;
}
if (values.type === 'TABLE') {
tableViewCreateMutation.mutate(
{
databaseId: database.id,
name: values.name,
},
{
onSuccess: () => {
form.reset();
onOpenChange(false);
},
},
);
} else if (values.type === 'BOARD') {
boardViewCreateMutation.mutate(
{
databaseId: database.id,
name: values.name,
},
{
onSuccess: () => {
form.reset();
onOpenChange(false);
},
},
);
} else if (values.type === 'CALENDAR') {
calendarViewCreateMutation.mutate(
{
databaseId: database.id,
name: values.name,
},
{
onSuccess: () => {
form.reset();
onOpenChange(false);
},
},
);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create view</DialogTitle>
<DialogDescription>
Create a new view to display your database records
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
className="flex flex-col"
onSubmit={form.handleSubmit(handleSubmit)}
>
<div className="flex-grow space-y-4 py-2 pb-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Name *</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<div className="grid grid-cols-3 gap-4">
{viewTypes.map((viewType) => (
<div
role="presentation"
key={viewType.name}
className={cn(
'flex cursor-pointer flex-col items-center gap-2 rounded-md border p-3 text-muted-foreground',
'hover:border-gray-500 hover:bg-gray-50',
viewType.type === field.value
? 'border-gray-500 text-primary'
: '',
)}
onClick={() => field.onChange(viewType.type)}
>
<Icon name={viewType.icon} />
<p>{viewType.name}</p>
</div>
))}
</div>
)}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isPending}>
{isPending && <Spinner className="mr-1" />}
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,70 @@
import { useWorkspace } from '@/contexts/workspace';
import { AttributeTypes, NodeTypes, ViewNodeTypes } from '@/lib/constants';
import { NeuronId } from '@/lib/id';
import { generateNodeIndex } from '@/lib/nodes';
import { compareString } from '@/lib/utils';
import { useMutation } from '@tanstack/react-query';
interface BoardViewCreateInput {
databaseId: string;
name: string;
}
export const useBoardViewCreateMutation = () => {
const workspace = useWorkspace();
return useMutation({
mutationFn: async (input: BoardViewCreateInput) => {
const siblingsQuery = workspace.schema
.selectFrom('nodes')
.where((eb) =>
eb.and([
eb('parent_id', '=', input.databaseId),
eb('type', 'in', ViewNodeTypes),
]),
)
.selectAll()
.compile();
const result = await workspace.query(siblingsQuery);
const siblings = result.rows;
const maxIndex =
siblings.length > 0
? siblings.sort((a, b) => compareString(a.index, b.index))[
siblings.length - 1
].index
: null;
const boardViewId = NeuronId.generate(NeuronId.Type.BoardView);
const insertBoardViewQuery = workspace.schema
.insertInto('nodes')
.values({
id: boardViewId,
type: NodeTypes.BoardView,
parent_id: input.databaseId,
index: generateNodeIndex(maxIndex, null),
content: null,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
const insertBoardViewNameQuery = workspace.schema
.insertInto('node_attributes')
.values({
node_id: boardViewId,
type: AttributeTypes.Name,
key: '1',
text_value: input.name,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
await workspace.mutate([insertBoardViewQuery, insertBoardViewNameQuery]);
return boardViewId;
},
});
};

View File

@@ -0,0 +1,73 @@
import { useWorkspace } from '@/contexts/workspace';
import { AttributeTypes, NodeTypes, ViewNodeTypes } from '@/lib/constants';
import { NeuronId } from '@/lib/id';
import { generateNodeIndex } from '@/lib/nodes';
import { compareString } from '@/lib/utils';
import { useMutation } from '@tanstack/react-query';
interface CalendarViewCreateInput {
databaseId: string;
name: string;
}
export const useCalendarViewCreateMutation = () => {
const workspace = useWorkspace();
return useMutation({
mutationFn: async (input: CalendarViewCreateInput) => {
const siblingsQuery = workspace.schema
.selectFrom('nodes')
.where((eb) =>
eb.and([
eb('parent_id', '=', input.databaseId),
eb('type', 'in', ViewNodeTypes),
]),
)
.selectAll()
.compile();
const result = await workspace.query(siblingsQuery);
const siblings = result.rows;
const maxIndex =
siblings.length > 0
? siblings.sort((a, b) => compareString(a.index, b.index))[
siblings.length - 1
].index
: null;
const calendarViewId = NeuronId.generate(NeuronId.Type.CalendarView);
const insertCalendarViewQuery = workspace.schema
.insertInto('nodes')
.values({
id: calendarViewId,
type: NodeTypes.CalendarView,
parent_id: input.databaseId,
index: generateNodeIndex(maxIndex, null),
content: null,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
const insertCalendarViewNameQuery = workspace.schema
.insertInto('node_attributes')
.values({
node_id: calendarViewId,
type: AttributeTypes.Name,
key: '1',
text_value: input.name,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
await workspace.mutate([
insertCalendarViewQuery,
insertCalendarViewNameQuery,
]);
return calendarViewId;
},
});
};

View File

@@ -0,0 +1,70 @@
import { useWorkspace } from '@/contexts/workspace';
import { AttributeTypes, NodeTypes, ViewNodeTypes } from '@/lib/constants';
import { NeuronId } from '@/lib/id';
import { generateNodeIndex } from '@/lib/nodes';
import { compareString } from '@/lib/utils';
import { useMutation } from '@tanstack/react-query';
interface TableViewCreateInput {
databaseId: string;
name: string;
}
export const useTableViewCreateMutation = () => {
const workspace = useWorkspace();
return useMutation({
mutationFn: async (input: TableViewCreateInput) => {
const siblingsQuery = workspace.schema
.selectFrom('nodes')
.where((eb) =>
eb.and([
eb('parent_id', '=', input.databaseId),
eb('type', 'in', ViewNodeTypes),
]),
)
.selectAll()
.compile();
const result = await workspace.query(siblingsQuery);
const siblings = result.rows;
const maxIndex =
siblings.length > 0
? siblings.sort((a, b) => compareString(a.index, b.index))[
siblings.length - 1
].index
: null;
const tableViewId = NeuronId.generate(NeuronId.Type.TableView);
const insertTableViewQuery = workspace.schema
.insertInto('nodes')
.values({
id: tableViewId,
type: NodeTypes.TableView,
parent_id: input.databaseId,
index: generateNodeIndex(maxIndex, null),
content: null,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
const insertTableViewNameQuery = workspace.schema
.insertInto('node_attributes')
.values({
node_id: tableViewId,
type: AttributeTypes.Name,
key: '1',
text_value: input.name,
created_at: new Date().toISOString(),
created_by: workspace.userId,
version_id: NeuronId.generate(NeuronId.Type.Version),
})
.compile();
await workspace.mutate([insertTableViewQuery, insertTableViewNameQuery]);
return tableViewId;
},
});
};

View File

@@ -3,6 +3,8 @@ import { SelectNodeWithAttributes } from '@/data/schemas/workspace';
import { AttributeTypes, NodeTypes, ViewNodeTypes } from '@/lib/constants';
import { mapNodeWithAttributes } from '@/lib/nodes';
import {
BoardViewNode,
CalendarViewNode,
DatabaseNode,
FieldDataType,
FieldNode,
@@ -157,7 +159,7 @@ const buildDatabaseNode = (
};
};
export const buildFieldNode = (
const buildFieldNode = (
node: LocalNodeWithAttributes,
selectOptions: LocalNodeWithAttributes[],
): FieldNode | null => {
@@ -272,9 +274,7 @@ export const buildFieldNode = (
}
};
export const buildSelectOption = (
node: LocalNodeWithAttributes,
): SelectOptionNode => {
const buildSelectOption = (node: LocalNodeWithAttributes): SelectOptionNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
)?.textValue;
@@ -290,19 +290,19 @@ export const buildSelectOption = (
};
};
export const buildViewNode = (
node: LocalNodeWithAttributes,
): ViewNode | null => {
const buildViewNode = (node: LocalNodeWithAttributes): ViewNode | null => {
if (node.type === NodeTypes.TableView) {
return buildTableViewNode(node);
} else if (node.type === NodeTypes.BoardView) {
return buildBoardViewNode(node);
} else if (node.type === NodeTypes.CalendarView) {
return buildCalendarViewNode(node);
}
return null;
};
export const buildTableViewNode = (
node: LocalNodeWithAttributes,
): TableViewNode => {
const buildTableViewNode = (node: LocalNodeWithAttributes): TableViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
)?.textValue;
@@ -350,3 +350,29 @@ export const buildTableViewNode = (
versionId: node.versionId,
};
};
const buildBoardViewNode = (node: LocalNodeWithAttributes): BoardViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
)?.textValue;
return {
id: node.id,
name: name ?? 'Unnamed',
type: 'board_view',
};
};
const buildCalendarViewNode = (
node: LocalNodeWithAttributes,
): CalendarViewNode => {
const name = node.attributes.find(
(attribute) => attribute.type === AttributeTypes.Name,
)?.textValue;
return {
id: node.id,
name: name ?? 'Unnamed',
type: 'calendar_view',
};
};