mirror of
https://github.com/colanode/colanode.git
synced 2026-02-24 03:49:48 +01:00
Implement database locks (#279)
This commit is contained in:
@@ -20,6 +20,7 @@ export const databaseAttributesSchema = z.object({
|
||||
parentId: z.string(),
|
||||
fields: z.record(z.string(), fieldAttributesSchema),
|
||||
nameField: databaseNameFieldAttributesSchema.nullable().optional(),
|
||||
locked: z.boolean().nullable().optional(),
|
||||
});
|
||||
|
||||
export type DatabaseAttributes = z.infer<typeof databaseAttributesSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Lock, LockOpen, Trash2 } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
import { ViewAvatarInput } from '@colanode/ui/components/databases/view-avatar-input';
|
||||
@@ -35,12 +35,12 @@ export const BoardViewSettings = () => {
|
||||
name={view.name}
|
||||
avatar={view.avatar}
|
||||
layout={view.layout}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
<ViewRenameInput
|
||||
id={view.id}
|
||||
name={view.name}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -53,13 +53,30 @@ export const BoardViewSettings = () => {
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
database.toggleLock();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
{database.isLocked ? (
|
||||
<LockOpen className="size-4" />
|
||||
) : (
|
||||
<Lock className="size-4" />
|
||||
)}
|
||||
<span>
|
||||
{database.isLocked ? 'Unlock database' : 'Lock database'}
|
||||
</span>
|
||||
</div>
|
||||
{!database.isLocked && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Lock, LockOpen, Trash2 } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
import { ViewAvatarInput } from '@colanode/ui/components/databases/view-avatar-input';
|
||||
@@ -35,12 +35,12 @@ export const CalendarViewSettings = () => {
|
||||
name={view.name}
|
||||
avatar={view.avatar}
|
||||
layout={view.layout}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
<ViewRenameInput
|
||||
id={view.id}
|
||||
name={view.name}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -53,13 +53,30 @@ export const CalendarViewSettings = () => {
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
database.toggleLock();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
{database.isLocked ? (
|
||||
<LockOpen className="size-4" />
|
||||
) : (
|
||||
<Lock className="size-4" />
|
||||
)}
|
||||
<span>
|
||||
{database.isLocked ? 'Unlock database' : 'Lock database'}
|
||||
</span>
|
||||
</div>
|
||||
{!database.isLocked && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Copy, Image, LetterText, Settings, Trash2 } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import {
|
||||
Copy,
|
||||
Image,
|
||||
LetterText,
|
||||
Settings,
|
||||
Trash2,
|
||||
Lock,
|
||||
LockOpen,
|
||||
} from 'lucide-react';
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
|
||||
import { LocalDatabaseNode } from '@colanode/client/types';
|
||||
import { NodeRole, hasNodeRole } from '@colanode/core';
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@colanode/ui/components/ui/dropdown-menu';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
interface DatabaseSettingsProps {
|
||||
database: LocalDatabaseNode;
|
||||
@@ -21,11 +30,33 @@ interface DatabaseSettingsProps {
|
||||
}
|
||||
|
||||
export const DatabaseSettings = ({ database, role }: DatabaseSettingsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteModal] = useState(false);
|
||||
|
||||
const canEdit = hasNodeRole(role, 'editor');
|
||||
const canDelete = hasNodeRole(role, 'admin');
|
||||
const isLocked = database.locked ?? false;
|
||||
|
||||
const handleLockDatabase = useCallback(() => {
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = workspace.collections.nodes;
|
||||
if (!nodes.has(database.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.update(database.id, (draft) => {
|
||||
if (draft.type !== 'database') {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLocked = draft.locked ?? false;
|
||||
draft.locked = !currentLocked;
|
||||
});
|
||||
}, [canEdit, database.id, workspace.userId]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -64,6 +95,18 @@ export const DatabaseSettings = ({ database, role }: DatabaseSettingsProps) => {
|
||||
<Image className="size-4" />
|
||||
Update icon
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
disabled={!canEdit}
|
||||
onClick={handleLockDatabase}
|
||||
>
|
||||
{isLocked ? (
|
||||
<LockOpen className="size-4" />
|
||||
) : (
|
||||
<Lock className="size-4" />
|
||||
)}
|
||||
{isLocked ? 'Unlock database' : 'Lock database'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
disabled
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
|
||||
import { LocalDatabaseNode } from '@colanode/client/types';
|
||||
import { NodeRole, hasNodeRole } from '@colanode/core';
|
||||
import { DatabaseContext } from '@colanode/ui/contexts/database';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
interface DatabaseProps {
|
||||
database: LocalDatabaseNode;
|
||||
@@ -11,9 +12,28 @@ interface DatabaseProps {
|
||||
}
|
||||
|
||||
export const Database = ({ database, role, children }: DatabaseProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const canEdit = hasNodeRole(role, 'editor');
|
||||
const isLocked = database.locked ?? false;
|
||||
const canCreateRecord = hasNodeRole(role, 'editor');
|
||||
|
||||
const toggleLock = useCallback(() => {
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = workspace.collections.nodes;
|
||||
nodes.update(database.id, (draft) => {
|
||||
if (draft.type !== 'database') {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLocked = draft.locked ?? false;
|
||||
draft.locked = !currentLocked;
|
||||
});
|
||||
}, [canEdit, database.id, workspace.userId]);
|
||||
|
||||
return (
|
||||
<DatabaseContext.Provider
|
||||
value={{
|
||||
@@ -22,9 +42,11 @@ export const Database = ({ database, role, children }: DatabaseProps) => {
|
||||
nameField: database.nameField,
|
||||
role,
|
||||
fields: Object.values(database.fields),
|
||||
canEdit,
|
||||
canEdit: canEdit,
|
||||
isLocked,
|
||||
canCreateRecord,
|
||||
rootId: database.rootId,
|
||||
toggleLock,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -161,7 +161,7 @@ export const FieldCreatePopover = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (!database.canEdit) {
|
||||
if (!database.canEdit || database.isLocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export const FieldRenameInput = ({ field }: FieldRenameInputProps) => {
|
||||
<div className="w-full p-1">
|
||||
<Input
|
||||
value={field.name}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
onChange={(event) => {
|
||||
const newValue = event.target.value;
|
||||
mutate(newValue);
|
||||
|
||||
@@ -143,7 +143,7 @@ export const TableViewFieldHeader = ({
|
||||
const [, dragRef] = useDrag<ViewField>({
|
||||
type: 'table-field-header',
|
||||
item: viewField,
|
||||
canDrag: () => database.canEdit,
|
||||
canDrag: () => database.canEdit && !database.isLocked,
|
||||
end: (_item, monitor) => {
|
||||
const dropResult = monitor.getDropResult<{ after: string }>();
|
||||
if (!dropResult?.after) return;
|
||||
@@ -190,7 +190,7 @@ export const TableViewFieldHeader = ({
|
||||
bottomLeft: false,
|
||||
bottomRight: false,
|
||||
left: false,
|
||||
right: database.canEdit,
|
||||
right: database.canEdit && !database.isLocked,
|
||||
top: false,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const TableViewHeader = () => {
|
||||
{view.fields.map((field) => {
|
||||
return <TableViewFieldHeader viewField={field} key={field.field.id} />;
|
||||
})}
|
||||
{database.canEdit && (
|
||||
{database.canEdit && !database.isLocked && (
|
||||
<FieldCreatePopover
|
||||
button={<Plus className="ml-2 size-4 cursor-pointer" />}
|
||||
/>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const TableViewNameHeader = () => {
|
||||
bottomLeft: false,
|
||||
bottomRight: false,
|
||||
left: false,
|
||||
right: database.canEdit,
|
||||
right: database.canEdit && !database.isLocked,
|
||||
top: false,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
@@ -112,7 +112,7 @@ export const TableViewNameHeader = () => {
|
||||
<div className="p-1">
|
||||
<Input
|
||||
value={database.nameField?.name ?? 'Name'}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
if (newName === database.nameField?.name) return;
|
||||
@@ -128,7 +128,7 @@ export const TableViewNameHeader = () => {
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
{database.canEdit && (
|
||||
{database.canEdit && !database.isLocked && (
|
||||
<Fragment>
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-accent rounded-sm"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Lock, LockOpen, Trash2 } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
import { ViewAvatarInput } from '@colanode/ui/components/databases/view-avatar-input';
|
||||
@@ -35,12 +35,12 @@ export const TableViewSettings = () => {
|
||||
name={view.name}
|
||||
avatar={view.avatar}
|
||||
layout={view.layout}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
<ViewRenameInput
|
||||
id={view.id}
|
||||
name={view.name}
|
||||
readOnly={!database.canEdit}
|
||||
readOnly={!database.canEdit || database.isLocked}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -53,13 +53,30 @@ export const TableViewSettings = () => {
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
database.toggleLock();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
{database.isLocked ? (
|
||||
<LockOpen className="size-4" />
|
||||
) : (
|
||||
<Lock className="size-4" />
|
||||
)}
|
||||
<span>
|
||||
{database.isLocked ? 'Unlock database' : 'Lock database'}
|
||||
</span>
|
||||
</div>
|
||||
{!database.isLocked && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-1 rounded-md p-0.5 hover:bg-accent"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<span>Delete view</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ export const ViewCreateDialog = ({
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
if (!database.canEdit) {
|
||||
if (!database.canEdit || database.isLocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,12 @@ export const ViewFieldSettings = () => {
|
||||
<TooltipTrigger>
|
||||
<span
|
||||
className={cn(
|
||||
database.canEdit ? 'cursor-pointer' : 'opacity-50'
|
||||
database.canEdit && !database.isLocked
|
||||
? 'cursor-pointer'
|
||||
: 'opacity-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!database.canEdit) return;
|
||||
if (!database.canEdit || database.isLocked) return;
|
||||
|
||||
handleDisplayChange({
|
||||
id: field.id,
|
||||
@@ -103,13 +105,15 @@ export const ViewFieldSettings = () => {
|
||||
: 'Show field in this view'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{database.canEdit && (
|
||||
{database.canEdit && !database.isLocked && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Trash2
|
||||
className={cn(
|
||||
'size-4',
|
||||
database.canEdit ? 'cursor-pointer' : 'opacity-50'
|
||||
database.canEdit && !database.isLocked
|
||||
? 'cursor-pointer'
|
||||
: 'opacity-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
setDeleteFieldId(field.id);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ViewTabs = () => {
|
||||
onClick={() => databaseViews.setActiveViewId(view.id)}
|
||||
/>
|
||||
))}
|
||||
{database.canEdit && <ViewCreateButton />}
|
||||
{database.canEdit && !database.isLocked && <ViewCreateButton />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ export const View = ({ view }: ViewProps) => {
|
||||
});
|
||||
},
|
||||
initFieldSort: async (fieldId: string, direction: SortDirection) => {
|
||||
if (!database.canEdit) {
|
||||
if (!database.canEdit || database.isLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const RecordField = ({ field }: RecordFieldProps) => {
|
||||
<PopoverContent className="ml-1 flex w-72 flex-col gap-1 p-2 text-sm">
|
||||
<FieldRenameInput field={field} />
|
||||
<Separator />
|
||||
{database.canEdit && (
|
||||
{database.canEdit && !database.isLocked && (
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-accent"
|
||||
onClick={() => {
|
||||
|
||||
@@ -12,9 +12,11 @@ interface DatabaseContext {
|
||||
nameField: DatabaseNameFieldAttributes | null | undefined;
|
||||
fields: FieldAttributes[];
|
||||
canEdit: boolean;
|
||||
isLocked: boolean;
|
||||
canCreateRecord: boolean;
|
||||
role: NodeRole;
|
||||
rootId: string;
|
||||
toggleLock: () => void;
|
||||
}
|
||||
|
||||
export const DatabaseContext = createContext<DatabaseContext>(
|
||||
|
||||
Reference in New Issue
Block a user