Skip to content

Commit

Permalink
feat: locked view (#1256)
Browse files Browse the repository at this point in the history
* feat: locked view

* chore: remove redundant code
  • Loading branch information
Sky-FE authored Jan 23, 2025
1 parent 90262c7 commit 96249c2
Show file tree
Hide file tree
Showing 26 changed files with 157 additions and 25 deletions.
1 change: 1 addition & 0 deletions apps/nestjs-backend/src/features/view/model/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function createViewVoByRaw(viewRaw: View): IViewVo {
createdTime: viewRaw.createdTime.toISOString(),
lastModifiedTime: viewRaw.lastModifiedTime ? viewRaw.lastModifiedTime.toISOString() : undefined,
columnMeta: JSON.parse(viewRaw.columnMeta as string) || undefined,
isLocked: viewRaw.isLocked || undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
IViewInstallPluginRo,
viewPluginUpdateStorageRoSchema,
IViewPluginUpdateStorageRo,
viewLockedRoSchema,
IViewLockedRo,
} from '@teable/openapi';
import type {
IEnableShareViewVo,
Expand Down Expand Up @@ -133,6 +135,23 @@ export class ViewOpenApiController {
);
}

@Permissions('view|update')
@Put('/:viewId/locked')
async updateLocked(
@Param('tableId') tableId: string,
@Param('viewId') viewId: string,
@Body(new ZodValidationPipe(viewLockedRoSchema)) viewLockedRo: IViewLockedRo,
@Headers('x-window-id') windowId?: string
): Promise<void> {
return await this.viewOpenApiService.setViewProperty(
tableId,
viewId,
'isLocked',
viewLockedRo.isLocked,
windowId
);
}

@Permissions('view|update')
@Put('/:viewId/share-meta')
async updateShareMeta(
Expand Down
3 changes: 3 additions & 0 deletions apps/nestjs-backend/test/view.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
updateViewColumnMeta,
updateRecord,
getRecords,
updateViewLocked,
} from '@teable/openapi';
import { VIEW_DEFAULT_SHARE_META } from './data-helpers/caces/view-default-share-meta';
import {
Expand Down Expand Up @@ -119,10 +120,12 @@ describe('OpenAPI ViewController (e2e)', () => {

await updateViewName(table.id, view.id, { name: 'New view 2' });
await updateViewDescription(table.id, view.id, { description: 'description2' });
await updateViewLocked(table.id, view.id, { isLocked: true });
const viewNew = await getView(table.id, view.id);

expect(viewNew.name).toEqual('New view 2');
expect(viewNew.description).toEqual('description2');
expect(viewNew.isLocked).toBeTruthy();
});

it('should create view with field order', async () => {
Expand Down
31 changes: 28 additions & 3 deletions apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ViewType } from '@teable/core';
import { Pencil, Trash2, Export, Copy } from '@teable/icons';
import { Pencil, Trash2, Export, Copy, Lock } from '@teable/icons';
import { useTableId, useTablePermission } from '@teable/sdk/hooks';
import type { IViewInstance } from '@teable/sdk/model';
import {
Expand All @@ -12,10 +12,11 @@ import {
PopoverAnchor,
} from '@teable/ui-lib/shadcn';
import { Input } from '@teable/ui-lib/shadcn/ui/input';
import { Unlock } from 'lucide-react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useState, useRef } from 'react';
import { useState, useRef, Fragment } from 'react';
import { useDownload } from '../../../hooks/useDownLoad';
import { VIEW_ICON_MAP } from '../constant';
import { useGridSearchStore } from '../grid/useGridSearchStore';
Expand Down Expand Up @@ -71,7 +72,10 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
alt={view.name}
/>
) : (
<ViewIcon className="mr-1 size-4 shrink-0" />
<Fragment>
{view.isLocked && <Lock className="mr-[2px] size-4 shrink-0" />}
<ViewIcon className="mr-1 size-4 shrink-0" />
</Fragment>
)}
<div className="flex flex-1 items-center justify-center overflow-hidden">
<div className="truncate text-xs font-medium leading-5">{view.name}</div>
Expand Down Expand Up @@ -193,6 +197,27 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
</Button>
</>
)}
{permission['view|update'] && (
<>
<Separator className="my-0.5" />
<Button
size="xs"
variant="ghost"
className="flex justify-start"
onClick={(e) => {
e.preventDefault();
view.updateLocked(!view.isLocked);
}}
>
{view.isLocked ? (
<Unlock className="size-3 shrink-0" />
) : (
<Lock className="size-3 shrink-0" />
)}
{view.isLocked ? t('view.action.unlock') : t('view.action.lock')}
</Button>
</>
)}
{permission['view|delete'] && (
<>
<Separator className="my-0.5" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { usePersonalView, useTablePermission } from '@teable/sdk/hooks';
import { CalendarViewOperators } from './components';
import { useViewConfigurable } from './hook';
import { Others } from './Others';

export const CalendarToolBar: React.FC = () => {
const permission = useTablePermission();
const { isPersonalView } = usePersonalView();
const { isViewConfigurable } = useViewConfigurable();

return (
<div className="flex items-center gap-2 border-y px-4 py-2 @container/toolbar">
<div className="flex flex-1 justify-between">
<CalendarViewOperators disabled={!permission['view|update'] && !isPersonalView} />
<CalendarViewOperators disabled={!isViewConfigurable} />
<Others />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { usePersonalView, useTablePermission } from '@teable/sdk/hooks';
import { GalleryViewOperators } from './components';
import { useViewConfigurable } from './hook';
import { Others } from './Others';

export const GalleryToolBar: React.FC = () => {
const permission = useTablePermission();
const { isPersonalView } = usePersonalView();
const { isViewConfigurable } = useViewConfigurable();

return (
<div className="flex items-center gap-2 border-y px-4 py-2 @container/toolbar">
<div className="flex flex-1 justify-between">
<GalleryViewOperators disabled={!permission['view|update'] && !isPersonalView} />
<GalleryViewOperators disabled={!isViewConfigurable} />
<Others />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Plus } from '@teable/icons';
import { CreateRecordModal } from '@teable/sdk/components';
import { usePersonalView, useTablePermission } from '@teable/sdk/hooks';
import { useTablePermission } from '@teable/sdk/hooks';
import { Button } from '@teable/ui-lib/shadcn/ui/button';
import { GridViewOperators } from './components';
import { UndoRedoButtons } from './components/UndoRedoButtons';
import { useViewConfigurable } from './hook';
import { Others } from './Others';

export const GridToolBar: React.FC = () => {
const permission = useTablePermission();
const { isPersonalView } = usePersonalView();
const { isViewConfigurable } = useViewConfigurable();

return (
<div className="flex items-center border-t px-1 py-2 sm:gap-1 sm:px-2 md:gap-2 md:px-4">
Expand All @@ -26,7 +27,7 @@ export const GridToolBar: React.FC = () => {
</CreateRecordModal>
<div className="mx-2 h-4 w-px shrink-0 bg-slate-200"></div>
<div className="flex flex-1 justify-between @container/toolbar">
<GridViewOperators disabled={!permission['view|update'] && !isPersonalView} />
<GridViewOperators disabled={!isViewConfigurable} />
<Others />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { usePersonalView, useTablePermission } from '@teable/sdk/hooks';
import { KanbanViewOperators } from './components';
import { useViewConfigurable } from './hook';
import { Others } from './Others';

export const KanbanToolBar: React.FC = () => {
const permission = useTablePermission();
const { isPersonalView } = usePersonalView();
const { isViewConfigurable } = useViewConfigurable();

return (
<div className="flex items-center gap-2 border-y px-4 py-2 @container/toolbar">
<div className="flex flex-1 justify-between">
<KanbanViewOperators disabled={!permission['view|update'] && !isPersonalView} />
<KanbanViewOperators disabled={!isViewConfigurable} />
<Others />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { User, Users } from '@teable/icons';
import { useTablePermission, usePersonalView } from '@teable/sdk/hooks';
import { useTablePermission, usePersonalView, useView } from '@teable/sdk/hooks';
import { ConfirmDialog } from '@teable/ui-lib/base';
import { useTranslation } from 'next-i18next';
import { Fragment, useState } from 'react';
Expand All @@ -13,6 +13,7 @@ interface IPersonalViewSwitchProps {

export const PersonalViewSwitch = (props: IPersonalViewSwitchProps) => {
const { textClassName, buttonClassName } = props;
const view = useView();
const permission = useTablePermission();
const { t } = useTranslation(tableConfig.i18nNamespaces);
const { isPersonalView, openPersonalView, closePersonalView, syncViewProperties } =
Expand All @@ -22,7 +23,7 @@ export const PersonalViewSwitch = (props: IPersonalViewSwitchProps) => {

const toggleViewStatus = () => {
if (isPersonalView) {
!hasSyncPermission ? closePersonalView?.() : setIsConfirmOpen(true);
!hasSyncPermission || view?.isLocked ? closePersonalView?.() : setIsConfirmOpen(true);
} else {
openPersonalView?.();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useViewConfigurable';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { usePersonalView, useTablePermission, useView } from '@teable/sdk/hooks';

export const useViewConfigurable = () => {
const view = useView();
const permission = useTablePermission();
const { isPersonalView } = usePersonalView();

return {
isViewConfigurable: (!view?.isLocked && permission['view|update']) || isPersonalView,
};
};
4 changes: 3 additions & 1 deletion packages/common-i18n/src/locales/en/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,9 @@
"action": {
"rename": "Rename view",
"duplicate": "Duplicate view",
"delete": "Delete view"
"delete": "Delete view",
"lock": "Lock view",
"unlock": "Unlock view"
},
"category": {
"table": "Grid View",
Expand Down
4 changes: 3 additions & 1 deletion packages/common-i18n/src/locales/fr/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,9 @@
"action": {
"rename": "Renommer la vue",
"duplicate": "Dupliquer la vue",
"delete": "Supprimer la vue"
"delete": "Supprimer la vue",
"lock": "Verrouiller la vue",
"unlock": "Déverrouiller la vue"
},
"category": {
"table": "Vue en grille",
Expand Down
4 changes: 3 additions & 1 deletion packages/common-i18n/src/locales/ja/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,9 @@
"action": {
"rename": "ビューの名前を変更",
"duplicate": "ビューを複製",
"delete": "ビューの削除"
"delete": "ビューの削除",
"lock": "ビューをロック",
"unlock": "ビューをロック解除"
},
"category": {
"table": "グリッドビュー",
Expand Down
4 changes: 3 additions & 1 deletion packages/common-i18n/src/locales/ru/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,9 @@
"action": {
"rename": "Переименовать вид",
"duplicate": "Дублировать представление",
"delete": "Удалить вид"
"delete": "Удалить вид",
"lock": "Заблокировать вид",
"unlock": "Разблокировать вид"
},
"category": {
"table": "Табличный вид",
Expand Down
4 changes: 3 additions & 1 deletion packages/common-i18n/src/locales/zh/table.json
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,9 @@
"action": {
"rename": "重命名",
"duplicate": "复制视图",
"delete": "删除视图"
"delete": "删除视图",
"lock": "锁定视图",
"unlock": "解锁视图"
},
"category": {
"table": "表格视图",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/view/view.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const viewVoSchema = z.object({
sort: sortSchema.optional(),
filter: filterSchema.optional(),
group: groupSchema.optional(),
isLocked: z.boolean().optional(),
shareId: z.string().optional(),
enableShare: z.boolean().optional(),
shareMeta: shareViewMetaSchema.optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/models/view/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export abstract class ViewCore implements IViewVo {

enableShare?: boolean;

isLocked?: boolean;

shareMeta?: IShareViewMeta;

abstract options: IViewOptions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "view" ADD COLUMN "is_locked" BOOLEAN;
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/postgres/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ model View {
order Float
version Int
columnMeta String @map("column_meta")
isLocked Boolean? @map("is_locked")
enableShare Boolean? @map("enable_share")
shareId String? @map("share_id")
shareMeta String? @map("share_meta")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "view" ADD COLUMN "is_locked" BOOLEAN;
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/sqlite/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ model View {
order Float
version Int
columnMeta String @map("column_meta")
isLocked Boolean? @map("is_locked")
enableShare Boolean? @map("enable_share")
shareId String? @map("share_id")
shareMeta String? @map("share_meta")
Expand Down
1 change: 1 addition & 0 deletions packages/db-main-prisma/prisma/template.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ model View {
order Float
version Int
columnMeta String @map("column_meta")
isLocked Boolean? @map("is_locked")
enableShare Boolean? @map("enable_share")
shareId String? @map("share_id")
shareMeta String? @map("share_meta")
Expand Down
1 change: 1 addition & 0 deletions packages/openapi/src/view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './filter-link-records';
export * from './plugin-install';
export * from './plugin-update-storage';
export * from './plugin-get';
export * from './update-locked';
47 changes: 47 additions & 0 deletions packages/openapi/src/view/update-locked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { RouteConfig } from '@asteasolutions/zod-to-openapi';
import { axios } from '../axios';
import { registerRoute, urlBuilder } from '../utils';
import { z } from '../zod';

export const VIEW_LOCKED = '/table/{tableId}/view/{viewId}/locked';

export const viewLockedRoSchema = z.object({
isLocked: z.boolean().optional(),
});

export type IViewLockedRo = z.infer<typeof viewLockedRoSchema>;

export const updateViewLockedRoute: RouteConfig = registerRoute({
method: 'put',
path: VIEW_LOCKED,
description: 'Update the locked status of the view',
request: {
params: z.object({
tableId: z.string(),
viewId: z.string(),
}),
body: {
content: {
'application/json': {
schema: viewLockedRoSchema,
},
},
},
},
responses: {
200: {
description: 'Successfully update.',
},
},
tags: ['view'],
});

export const updateViewLocked = async (tableId: string, viewId: string, data: IViewLockedRo) => {
return axios.put<void>(
urlBuilder(VIEW_LOCKED, {
tableId,
viewId,
}),
data
);
};
Loading

0 comments on commit 96249c2

Please sign in to comment.