Skip to content

Commit 8deafda

Browse files
authored
feat: base node (#2168)
* feat: add BaseNode and BaseNodeFolder models with migration * feat: add tree component in ui-lib * feat: implement BaseNode and BaseNodeFolder functionality with CRUD operations and event handling * feat: enhance migration script * feat: add support for user last visit tracking and resource deletion events * feat: implement permission management for BaseNode with role-based access control * refactor: PinService to optimize resource fetching and enhance code readability * fix: router * feat: base import/export/duplicae support base node * test: add unit tests for BaseNodeService methods including SQL generation and edge cases * feat: implement folder depth validation and enhance node movement logic in BaseNodeService * feat: integrate performance caching for base node list * refactor: remove unused routes from BasePageRouter * feat: enhance dashboard renaming functionality with improved state management and keyboard shortcuts * refactor: simplify BaseNodeTree component by removing unnecessary separator and enhancing drop logic * feat: enhance QuickAction search * fix: sorting for nodes in BaseImportService to ensure proper parent-child relationships * fix: delete folder and pin list * feat: add permanent delete functionality for base nodes and enhance delete logic in BaseNodeService * feat: enhance error handling in BaseNodeService and BaseNodeFolderService with localized messages * refactor: rename hooks and reorganize imports in base node feature * refactor: remove console log and clean up imports in PinItem component * fix: pin sql * fix: e2e * fix: sharedb presence handling * fix: e2e * refactor: optimize database transactions in BaseNodeService * fix: improve URL generation in BaseNode components * refactor: remove unnecessary permission decorator and adjust layout in BaseNodeTree component * feat: add validation for folder depth when moving nodes * fix: refine anchorId logic in BaseNodeTree component for improved node positioning * fix: adjust emoji picker size in BaseNodeTree component for better UI consistency * fix: enhance expanded when create * feat: implement auto-scroll functionality during drag in BaseNodeTree component * fix: update TreeItemLabel and TreeDragLine styles for improved visual consistency * fix: enhance canDrop logic in BaseNodeTree for improved item drop validation * refactor: add resourceMeta in baseNodeSchema * fix: e2e * refactor: update folder creation and update endpoints to return structured response objects * fix: e2e * feat: add disallowDashboard setting and deprecation banner in dashboard components * fix: type check * feat: add loading state to BaseNodeContext and integrate skeleton loading in BaseNodeTree * feat: enhance BaseNode service and UI to include defaultViewId in resourceMeta * refactor: simplify URL construction in getNodeUrl and streamline table navigation in BaseNodeTree * refactor: improve styling and structure in BaseNodeTree for better responsiveness and accessibility * feat: add workflow state render * fix: sync dataLoader returned undefined error * refactor: update styling in BaseNodeTree for improved layout and consistency * refactor: remove setEditingNodeId when create and duplicate * refactor: extract table creation logic for improved readability * refactor: update dropdown menu width and enhance delete confirmation title with resource type * fix: common noun i18n * feat: introduce useBaseNodeContext hook for improved context management in BaseNode components * refactor: update useBaseNode for enhanced context management * refactor: enhance BaseNodeTree component with edit mode support and improved local storage handling * feat: add onUpdateError callback to useBaseNodeCrud and BaseNodeTree for improved error handling * refactor: improved UI consistency * refactor: improve menu invalidation * refactor: remove permanent delete functionality from UI components * refactor: permission handling by consolidating base node actions * feat: implement base node event handling with create, update, and delete events * refactor: base node event * refactor: remove table iist in ssr * feat: enhance BaseNodeTree with highlight * feat: update BaseNodeTree to expand parent nodes on selection * refactor: replace nativeEnum with enum * fix: enhance router visits check * refactor: remove unused user last visit mutation from BaseNodeTree
1 parent 6fd609a commit 8deafda

File tree

163 files changed

+10005
-782
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

163 files changed

+10005
-782
lines changed

apps/nestjs-backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AiModule } from './features/ai/ai.module';
1212
import { AttachmentsModule } from './features/attachments/attachments.module';
1313
import { AuthModule } from './features/auth/auth.module';
1414
import { BaseModule } from './features/base/base.module';
15+
import { BaseNodeModule } from './features/base-node/base-node.module';
1516
import { ChatModule } from './features/chat/chat.module';
1617
import { CollaboratorModule } from './features/collaborator/collaborator.module';
1718
import { CommentOpenApiModule } from './features/comment/comment-open-api.module';
@@ -59,6 +60,7 @@ export const appModules = {
5960
FieldOpenApiModule,
6061
TemplateOpenApiModule,
6162
BaseModule,
63+
BaseNodeModule,
6264
IntegrityModule,
6365
ChatModule,
6466
AttachmentsModule,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { match } from 'ts-pattern';
2+
import type { IEventContext } from '../core-event';
3+
import { CoreEvent } from '../core-event';
4+
import { Events } from '../event.enum';
5+
6+
interface IAppVo {
7+
id: string;
8+
name: string;
9+
}
10+
11+
type IAppCreatePayload = { baseId: string; app: IAppVo };
12+
type IAppDeletePayload = { baseId: string; appId: string };
13+
type IAppUpdatePayload = { baseId: string; app: IAppVo };
14+
15+
export class AppCreateEvent extends CoreEvent<IAppCreatePayload> {
16+
public readonly name = Events.APP_CREATE;
17+
18+
constructor(baseId: string, app: IAppVo, context: IEventContext) {
19+
super({ baseId, app }, context);
20+
}
21+
}
22+
23+
export class AppDeleteEvent extends CoreEvent<IAppDeletePayload> {
24+
public readonly name = Events.APP_DELETE;
25+
constructor(baseId: string, appId: string, context: IEventContext) {
26+
super({ baseId, appId }, context);
27+
}
28+
}
29+
30+
export class AppUpdateEvent extends CoreEvent<IAppUpdatePayload> {
31+
public readonly name = Events.APP_UPDATE;
32+
33+
constructor(baseId: string, app: IAppVo, context: IEventContext) {
34+
super({ baseId, app }, context);
35+
}
36+
}
37+
38+
export class AppEventFactory {
39+
static create(
40+
name: string,
41+
payload: IAppCreatePayload | IAppDeletePayload | IAppUpdatePayload,
42+
context: IEventContext
43+
) {
44+
return match(name)
45+
.with(Events.APP_CREATE, () => {
46+
const { baseId, app } = payload as IAppCreatePayload;
47+
return new AppCreateEvent(baseId, app, context);
48+
})
49+
.with(Events.APP_UPDATE, () => {
50+
const { baseId, app } = payload as IAppUpdatePayload;
51+
return new AppUpdateEvent(baseId, app, context);
52+
})
53+
.with(Events.APP_DELETE, () => {
54+
const { baseId, appId } = payload as IAppDeletePayload;
55+
return new AppDeleteEvent(baseId, appId, context);
56+
})
57+
.otherwise(() => null);
58+
}
59+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { BaseNodeResourceType, type IBaseNodeVo, type IDeleteBaseNodeVo } from '@teable/openapi';
2+
import { match } from 'ts-pattern';
3+
import { AppEventFactory } from '../app/app.event';
4+
import type { IEventContext } from '../core-event';
5+
import { DashboardEventFactory } from '../dashboard/dashboard.event';
6+
import { Events } from '../event.enum';
7+
import { WorkflowEventFactory } from '../workflow/workflow.event';
8+
import { BaseFolderEventFactory } from './folder/base.folder.event';
9+
10+
type IBaseNodeCreatePayload = { baseId: string; node: IBaseNodeVo };
11+
type IBaseNodeDeletePayload = { baseId: string; node: IDeleteBaseNodeVo };
12+
type IBaseNodeUpdatePayload = IBaseNodeCreatePayload;
13+
14+
// base node event to resource event(folder, dashboard, workflow, app); table event is handled by ops2Event;
15+
export class BaseNodeEventFactory {
16+
static create(
17+
name: string,
18+
payload: IBaseNodeCreatePayload | IBaseNodeDeletePayload | IBaseNodeUpdatePayload,
19+
context: IEventContext
20+
) {
21+
return match(name)
22+
.with(Events.BASE_NODE_CREATE, () => {
23+
const { baseId, node } = payload as IBaseNodeCreatePayload;
24+
const { resourceId, resourceType, resourceMeta } = node;
25+
switch (resourceType) {
26+
case BaseNodeResourceType.Folder:
27+
return BaseFolderEventFactory.create(
28+
Events.BASE_FOLDER_CREATE,
29+
{
30+
baseId,
31+
folder: {
32+
id: resourceId,
33+
...resourceMeta,
34+
},
35+
},
36+
context
37+
);
38+
case BaseNodeResourceType.Dashboard:
39+
return DashboardEventFactory.create(
40+
Events.DASHBOARD_CREATE,
41+
{
42+
baseId,
43+
dashboard: {
44+
id: resourceId,
45+
...resourceMeta,
46+
},
47+
},
48+
context
49+
);
50+
case BaseNodeResourceType.Workflow:
51+
return WorkflowEventFactory.create(
52+
Events.WORKFLOW_CREATE,
53+
{
54+
baseId,
55+
workflow: {
56+
id: resourceId,
57+
...resourceMeta,
58+
},
59+
},
60+
context
61+
);
62+
case BaseNodeResourceType.App:
63+
return AppEventFactory.create(
64+
Events.APP_CREATE,
65+
{
66+
baseId,
67+
app: {
68+
id: resourceId,
69+
...resourceMeta,
70+
},
71+
},
72+
context
73+
);
74+
75+
default:
76+
return null;
77+
}
78+
})
79+
.with(Events.BASE_NODE_UPDATE, () => {
80+
const { baseId, node } = payload as IBaseNodeUpdatePayload;
81+
const { resourceId, resourceType, resourceMeta } = node;
82+
switch (resourceType) {
83+
case BaseNodeResourceType.Folder:
84+
return BaseFolderEventFactory.create(
85+
Events.BASE_FOLDER_UPDATE,
86+
{
87+
baseId,
88+
folder: {
89+
id: resourceId,
90+
...resourceMeta,
91+
},
92+
},
93+
context
94+
);
95+
case BaseNodeResourceType.Dashboard:
96+
return DashboardEventFactory.create(
97+
Events.DASHBOARD_UPDATE,
98+
{
99+
baseId,
100+
dashboard: {
101+
id: resourceId,
102+
...resourceMeta,
103+
},
104+
},
105+
context
106+
);
107+
case BaseNodeResourceType.Workflow:
108+
return WorkflowEventFactory.create(
109+
Events.WORKFLOW_UPDATE,
110+
{
111+
baseId,
112+
workflow: {
113+
id: resourceId,
114+
...resourceMeta,
115+
},
116+
},
117+
context
118+
);
119+
case BaseNodeResourceType.App:
120+
return AppEventFactory.create(
121+
Events.APP_UPDATE,
122+
{
123+
baseId,
124+
app: {
125+
id: resourceId,
126+
...resourceMeta,
127+
},
128+
},
129+
context
130+
);
131+
132+
default:
133+
return null;
134+
}
135+
})
136+
.with(Events.BASE_NODE_DELETE, () => {
137+
const { baseId, node } = payload as IBaseNodeDeletePayload;
138+
const { resourceId, resourceType } = node;
139+
switch (resourceType) {
140+
case BaseNodeResourceType.Folder:
141+
return BaseFolderEventFactory.create(
142+
Events.BASE_FOLDER_DELETE,
143+
{ baseId, folderId: resourceId },
144+
context
145+
);
146+
case BaseNodeResourceType.Dashboard:
147+
return DashboardEventFactory.create(
148+
Events.DASHBOARD_DELETE,
149+
{ baseId, dashboardId: resourceId },
150+
context
151+
);
152+
case BaseNodeResourceType.Workflow:
153+
return WorkflowEventFactory.create(
154+
Events.WORKFLOW_DELETE,
155+
{ baseId, workflowId: resourceId },
156+
context
157+
);
158+
case BaseNodeResourceType.App:
159+
return AppEventFactory.create(
160+
Events.APP_DELETE,
161+
{ baseId, appId: resourceId },
162+
context
163+
);
164+
default:
165+
return null;
166+
}
167+
})
168+
169+
.otherwise(() => null);
170+
}
171+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { match } from 'ts-pattern';
2+
import type { IEventContext } from '../../core-event';
3+
import { CoreEvent } from '../../core-event';
4+
import { Events } from '../../event.enum';
5+
6+
type IBaseFolder = {
7+
id: string;
8+
name: string;
9+
};
10+
11+
type IBaseFolderCreatePayload = { baseId: string; folder: IBaseFolder };
12+
type IBaseFolderDeletePayload = { baseId: string; folderId: string };
13+
type IBaseFolderUpdatePayload = IBaseFolderCreatePayload;
14+
15+
export class BaseFolderCreateEvent extends CoreEvent<IBaseFolderCreatePayload> {
16+
public readonly name = Events.BASE_FOLDER_CREATE;
17+
18+
constructor(payload: IBaseFolderCreatePayload, context: IEventContext) {
19+
super(payload, context);
20+
}
21+
}
22+
23+
export class BaseFolderDeleteEvent extends CoreEvent<IBaseFolderDeletePayload> {
24+
public readonly name = Events.BASE_FOLDER_DELETE;
25+
constructor(payload: IBaseFolderDeletePayload, context: IEventContext) {
26+
super(payload, context);
27+
}
28+
}
29+
30+
export class BaseFolderUpdateEvent extends CoreEvent<IBaseFolderUpdatePayload> {
31+
public readonly name = Events.BASE_FOLDER_UPDATE;
32+
33+
constructor(payload: IBaseFolderUpdatePayload, context: IEventContext) {
34+
super(payload, context);
35+
}
36+
}
37+
38+
export class BaseFolderEventFactory {
39+
static create(
40+
name: string,
41+
payload: IBaseFolderCreatePayload | IBaseFolderDeletePayload | IBaseFolderUpdatePayload,
42+
context: IEventContext
43+
) {
44+
return match(name)
45+
.with(Events.BASE_FOLDER_CREATE, () => {
46+
const { baseId, folder } = payload as IBaseFolderCreatePayload;
47+
return new BaseFolderCreateEvent({ baseId, folder }, context);
48+
})
49+
.with(Events.BASE_FOLDER_DELETE, () => {
50+
const { baseId, folderId } = payload as IBaseFolderDeletePayload;
51+
return new BaseFolderDeleteEvent({ baseId, folderId }, context);
52+
})
53+
.with(Events.BASE_FOLDER_UPDATE, () => {
54+
const { baseId, folder } = payload as IBaseFolderUpdatePayload;
55+
return new BaseFolderUpdateEvent({ baseId, folder }, context);
56+
})
57+
.otherwise(() => null);
58+
}
59+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { ICreateDashboardVo } from '@teable/openapi';
2+
import { match } from 'ts-pattern';
3+
import type { IEventContext } from '../core-event';
4+
import { CoreEvent } from '../core-event';
5+
import { Events } from '../event.enum';
6+
7+
type IDashboardCreatePayload = { baseId: string; dashboard: ICreateDashboardVo };
8+
type IDashboardUpdatePayload = { baseId: string; dashboard: ICreateDashboardVo };
9+
type IDashboardDeletePayload = { baseId: string; dashboardId: string };
10+
11+
export class DashboardCreateEvent extends CoreEvent<IDashboardCreatePayload> {
12+
public readonly name = Events.DASHBOARD_CREATE;
13+
14+
constructor(payload: IDashboardCreatePayload, context: IEventContext) {
15+
super(payload, context);
16+
}
17+
}
18+
19+
export class DashboardDeleteEvent extends CoreEvent<IDashboardDeletePayload> {
20+
public readonly name = Events.DASHBOARD_DELETE;
21+
constructor(payload: IDashboardDeletePayload, context: IEventContext) {
22+
super(payload, context);
23+
}
24+
}
25+
26+
export class DashboardUpdateEvent extends CoreEvent<IDashboardUpdatePayload> {
27+
public readonly name = Events.DASHBOARD_UPDATE;
28+
29+
constructor(payload: IDashboardUpdatePayload, context: IEventContext) {
30+
super(payload, context);
31+
}
32+
}
33+
34+
export class DashboardEventFactory {
35+
static create(
36+
name: string,
37+
payload: IDashboardCreatePayload | IDashboardDeletePayload | IDashboardUpdatePayload,
38+
context: IEventContext
39+
) {
40+
return match(name)
41+
.with(Events.DASHBOARD_CREATE, () => {
42+
const { baseId, dashboard } = payload as IDashboardCreatePayload;
43+
return new DashboardCreateEvent({ baseId, dashboard }, context);
44+
})
45+
.with(Events.DASHBOARD_DELETE, () => {
46+
const { baseId, dashboardId } = payload as IDashboardDeletePayload;
47+
return new DashboardDeleteEvent({ baseId, dashboardId }, context);
48+
})
49+
.with(Events.DASHBOARD_UPDATE, () => {
50+
const { baseId, dashboard } = payload as IDashboardUpdatePayload;
51+
return new DashboardUpdateEvent({ baseId, dashboard }, context);
52+
})
53+
.otherwise(() => null);
54+
}
55+
}

apps/nestjs-backend/src/event-emitter/events/event.enum.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export enum Events {
1111
// BASE_CLONE = 'base.clone',
1212
// BASE_MOVE = 'base.move',
1313

14+
BASE_NODE_CREATE = 'base.node.create',
15+
BASE_NODE_DELETE = 'base.node.delete',
16+
BASE_NODE_UPDATE = 'base.node.update',
17+
1418
TABLE_CREATE = 'table.create',
1519
TABLE_DELETE = 'table.delete',
1620
TABLE_UPDATE = 'table.update',
@@ -62,9 +66,24 @@ export enum Events {
6266
COLLABORATOR_CREATE = 'collaborator.create',
6367
COLLABORATOR_DELETE = 'collaborator.delete',
6468

69+
BASE_FOLDER_CREATE = 'base.folder.create',
70+
BASE_FOLDER_DELETE = 'base.folder.delete',
71+
BASE_FOLDER_UPDATE = 'base.folder.update',
72+
73+
DASHBOARD_CREATE = 'dashboard.create',
74+
DASHBOARD_DELETE = 'dashboard.delete',
75+
DASHBOARD_UPDATE = 'dashboard.update',
76+
77+
WORKFLOW_CREATE = 'workflow.create',
78+
WORKFLOW_DELETE = 'workflow.delete',
79+
WORKFLOW_UPDATE = 'workflow.update',
6580
WORKFLOW_ACTIVATE = 'workflow.activate',
6681
WORKFLOW_DEACTIVATE = 'workflow.deactivate',
6782

83+
APP_CREATE = 'app.create',
84+
APP_DELETE = 'app.delete',
85+
APP_UPDATE = 'app.update',
86+
6887
CROP_IMAGE = 'crop.image',
6988
CROP_IMAGE_COMPLETE = 'crop.image.complete',
7089

0 commit comments

Comments
 (0)