Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: locked view #1256

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
32 changes: 29 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,28 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
</Button>
</>
)}
{permission['view|update'] && (
<>
<Separator className="my-0.5" />
<Button
size="xs"
disabled={!removable}
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 @@ -492,7 +492,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 @@ -477,7 +477,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 @@ -476,7 +476,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 @@ -477,7 +477,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 @@ -491,7 +491,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;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
2 changes: 1 addition & 1 deletion packages/db-main-prisma/prisma/postgres/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
previewFeatures = ["tracing"]
}

datasource db {
Expand Down Expand Up @@ -125,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;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
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';
Loading
Loading