mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Implement view create mutation
This commit is contained in:
@@ -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>
|
||||
|
||||
18
desktop/src/components/databases/view-create-button.tsx
Normal file
18
desktop/src/components/databases/view-create-button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
70
desktop/src/mutations/use-board-view-create-mutation.tsx
Normal file
70
desktop/src/mutations/use-board-view-create-mutation.tsx
Normal 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;
|
||||
},
|
||||
});
|
||||
};
|
||||
73
desktop/src/mutations/use-calendar-view-create-mutation.tsx
Normal file
73
desktop/src/mutations/use-calendar-view-create-mutation.tsx
Normal 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;
|
||||
},
|
||||
});
|
||||
};
|
||||
70
desktop/src/mutations/use-table-view-create-mutation.tsx
Normal file
70
desktop/src/mutations/use-table-view-create-mutation.tsx
Normal 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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user