Skip to content

Commit dcf5ea1

Browse files
authored
fix: catch-all dynamic routes in [baseId] (#2242)
* fix: base node shallow router * feat: catch-all dynamic routes in [baseId] * fix: handle empty table and view IDs in resource handling * fix: update row_number ordering in migration script for base node * feat: add migration scripts to disallow dashboard access * fix: handle empty data cases in Table component rendering * refactor: split [...slug].tsx * refactor: rename resource handling functions for consistency in Dashboard and Table pages * refactor: streamline server-side props handling across Dashboard, Table, and Workflow pages * fix: handle non-array folders in createFolders method of BaseImportService * refactor: update translation props handling * fix: base folder expand * feat: improve tree item show * fix: build error about getTranslationsProps * fix: build tree when treeItems empty * fix: handle empty array cases in import * fix: folder expand * fix: folder expand sync
1 parent 5dafe9b commit dcf5ea1

File tree

44 files changed

+723
-804
lines changed

Some content is hidden

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

44 files changed

+723
-804
lines changed

apps/nestjs-backend/src/features/base/base-import.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,12 @@ export class BaseImportService {
482482
}
483483

484484
private async createFolders(baseId: string, folders: IBaseJson['folders']) {
485+
const folderIdMap: Record<string, string> = {};
486+
if (!Array.isArray(folders) || folders.length === 0) {
487+
return { folderIdMap };
488+
}
485489
const prisma = this.prismaService.txClient();
486490
const userId = this.cls.get('user.id');
487-
const folderIdMap: Record<string, string> = {};
488491
for (const folder of folders) {
489492
const { id, name } = folder;
490493
const newFolderId = generateBaseNodeFolderId();
@@ -507,7 +510,7 @@ export class BaseImportService {
507510
appIdMap?: Record<string, string>;
508511
}
509512
) {
510-
if (!nodes || nodes.length === 0) {
513+
if (!Array.isArray(nodes) || nodes.length === 0) {
511514
return;
512515
}
513516

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { dehydrate } from '@tanstack/react-query';
2+
import { getNodeUrl } from '@/features/app/blocks/base/base-node/hooks';
3+
import { redirect } from './helper';
4+
import type { ISSRContext, SSRResult } from './types';
5+
6+
export const getBaseServerSideProps = async (ctx: ISSRContext): Promise<SSRResult> => {
7+
const { ssrApi, baseId } = ctx;
8+
const [lastVisitNode, nodes] = await Promise.all([
9+
ssrApi.getUserLastVisitBaseNode({ parentResourceId: baseId }),
10+
ssrApi.getBaseNodeList(baseId),
11+
]);
12+
13+
const findNode = nodes.find((n) => n.resourceId === lastVisitNode?.resourceId);
14+
if (findNode) {
15+
const url = getNodeUrl({
16+
baseId,
17+
resourceType: findNode.resourceType,
18+
resourceId: findNode.resourceId,
19+
});
20+
if (url?.pathname) return redirect(url.pathname);
21+
}
22+
23+
return {
24+
props: {
25+
...(await ctx.getTranslationsProps()),
26+
dehydratedState: dehydrate(ctx.queryClient),
27+
},
28+
};
29+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { dehydrate } from '@tanstack/react-query';
2+
import { BaseNodeResourceType, LastVisitResourceType } from '@teable/openapi';
3+
import { ReactQueryKeys } from '@teable/sdk/config';
4+
import { DashboardPage as DashboardPageComponent } from '@/features/app/dashboard/Pages';
5+
import type { IBaseResourceParsed } from '@/features/app/hooks/useBaseResource';
6+
import { redirect } from './helper';
7+
import type { ISSRContext, SSRResult } from './types';
8+
9+
export const getDashboardServerSideProps = async (
10+
ctx: ISSRContext,
11+
parsed: IBaseResourceParsed
12+
): Promise<SSRResult> => {
13+
const { ssrApi, baseId, queryClient } = ctx;
14+
if (parsed.resourceType !== BaseNodeResourceType.Dashboard) return { notFound: true };
15+
16+
const { dashboardId } = parsed;
17+
18+
if (!dashboardId) {
19+
const [lastVisit, dashboardList] = await Promise.all([
20+
ssrApi.getUserLastVisit(LastVisitResourceType.Dashboard, baseId),
21+
queryClient.fetchQuery({
22+
queryKey: ReactQueryKeys.getDashboardList(baseId),
23+
queryFn: () => ssrApi.getDashboardList(baseId),
24+
}),
25+
]);
26+
27+
const ids = dashboardList.map((d) => d.id);
28+
const defaultId =
29+
lastVisit?.resourceId && ids.includes(lastVisit.resourceId) ? lastVisit.resourceId : ids[0];
30+
if (defaultId) return redirect(`/base/${baseId}/dashboard/${defaultId}`);
31+
32+
return {
33+
props: {
34+
...(await ctx.getTranslationsProps()),
35+
dehydratedState: dehydrate(ctx.queryClient),
36+
},
37+
};
38+
}
39+
40+
await queryClient.fetchQuery({
41+
queryKey: ReactQueryKeys.getDashboard(dashboardId),
42+
queryFn: () => ssrApi.getDashboard(baseId, dashboardId),
43+
});
44+
45+
return {
46+
props: {
47+
...(await ctx.getTranslationsProps()),
48+
dehydratedState: dehydrate(ctx.queryClient),
49+
},
50+
};
51+
};
52+
53+
export const DashBoardPage = () => {
54+
return <DashboardPageComponent />;
55+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* eslint-disable sonarjs/cognitive-complexity */
2+
import { dehydrate } from '@tanstack/react-query';
3+
import { BaseNodeResourceType, LastVisitResourceType } from '@teable/openapi';
4+
import { ReactQueryKeys } from '@teable/sdk/config';
5+
import type { SsrApi } from '@/backend/api/rest/ssr-api';
6+
import { Table } from '@/features/app/blocks/table/Table';
7+
import type { IBaseResourceParsed } from '@/features/app/hooks/useBaseResource';
8+
import { getViewPageServerData } from '@/lib/view-pages-data';
9+
import { redirect } from './helper';
10+
import type { ISSRContext, SSRResult, ITablePageProps } from './types';
11+
12+
export const getDefaultViewId = async (ssrApi: SsrApi, tableId: string) => {
13+
const [lastVisit, viewList] = await Promise.all([
14+
ssrApi.getUserLastVisit(LastVisitResourceType.View, tableId),
15+
ssrApi.getViewList(tableId),
16+
]);
17+
const viewIds = viewList.map((v) => v.id);
18+
if (viewIds.length === 0) return undefined;
19+
return lastVisit?.resourceId && viewIds.includes(lastVisit.resourceId)
20+
? lastVisit.resourceId
21+
: viewIds[0]!;
22+
};
23+
24+
export const getTableServerSideProps = async (
25+
ctx: ISSRContext,
26+
parsed: IBaseResourceParsed,
27+
queryParams: Record<string, string | string[] | undefined>
28+
): Promise<SSRResult> => {
29+
const { ssrApi, baseId, queryClient } = ctx;
30+
if (parsed.resourceType !== BaseNodeResourceType.Table) return { notFound: true };
31+
const { tableId, viewId } = parsed;
32+
const { recordId, fromNotify: notifyId } = queryParams as {
33+
recordId?: string;
34+
fromNotify?: string;
35+
};
36+
37+
if (!tableId) {
38+
const [lastVisit, tableList] = await Promise.all([
39+
ssrApi.getUserLastVisit(LastVisitResourceType.Table, baseId),
40+
ssrApi.getTables(baseId),
41+
]);
42+
const tableIds = tableList.map((t) => t.id);
43+
const defaultTableId =
44+
lastVisit?.resourceId && tableIds.includes(lastVisit.resourceId)
45+
? lastVisit.resourceId
46+
: tableIds[0];
47+
48+
const defaultViewId = defaultTableId
49+
? await getDefaultViewId(ssrApi, defaultTableId)
50+
: undefined;
51+
if (defaultTableId && defaultViewId) {
52+
return redirect(`/base/${baseId}/table/${defaultTableId}/${defaultViewId}`);
53+
}
54+
return { notFound: true };
55+
}
56+
57+
if (!viewId) {
58+
const defaultViewId = await getDefaultViewId(ssrApi, tableId);
59+
if (defaultViewId) {
60+
return redirect(`/base/${baseId}/table/${tableId}/${defaultViewId}`);
61+
}
62+
return { notFound: true };
63+
}
64+
65+
// check table exists
66+
const [tableList] = await Promise.all([
67+
queryClient.fetchQuery({
68+
queryKey: ReactQueryKeys.tableList(baseId),
69+
queryFn: () => ssrApi.getTables(baseId),
70+
}),
71+
queryClient.fetchQuery({
72+
queryKey: ReactQueryKeys.getTablePermission(baseId, tableId),
73+
queryFn: () => ssrApi.getTablePermission(baseId, tableId),
74+
}),
75+
]);
76+
77+
const tableIds = tableList.map((t) => t.id);
78+
if (tableIds.length === 0) return { notFound: true };
79+
if (!tableIds.includes(tableId)) return redirect(`/base/${baseId}/table/${tableIds[0]}`);
80+
81+
// check view exists
82+
const viewList = await queryClient.fetchQuery({
83+
queryKey: ReactQueryKeys.viewList(tableId),
84+
queryFn: () => ssrApi.getViewList(tableId),
85+
});
86+
const viewIds = viewList.map((v) => v.id);
87+
if (viewIds.length === 0) return { notFound: true };
88+
if (!viewIds.includes(viewId)) return redirect(`/base/${baseId}/table/${tableId}/${viewIds[0]}`);
89+
90+
// handle recordId
91+
let recordServerData: ITablePageProps['recordServerData'];
92+
if (recordId) {
93+
if (notifyId) await ssrApi.updateNotificationStatus(notifyId, { isRead: true });
94+
recordServerData = await ssrApi.getRecord(tableId, recordId);
95+
if (!recordServerData) return redirect(`/base/${baseId}/table/${tableId}/${viewId}`);
96+
}
97+
98+
const serverData = await getViewPageServerData(ssrApi, baseId, tableId, viewId);
99+
if (!serverData) return { notFound: true };
100+
101+
return {
102+
props: {
103+
...serverData,
104+
...(recordServerData ? { recordServerData } : {}),
105+
...(await ctx.getTranslationsProps()),
106+
dehydratedState: dehydrate(ctx.queryClient),
107+
},
108+
};
109+
};
110+
111+
export const TablePage = ({
112+
fieldServerData,
113+
viewServerData,
114+
recordsServerData,
115+
recordServerData,
116+
groupPointsServerDataMap,
117+
}: ITablePageProps) => {
118+
return (
119+
<Table
120+
fieldServerData={fieldServerData ?? []}
121+
viewServerData={viewServerData ?? []}
122+
recordsServerData={recordsServerData ?? { records: [] }}
123+
recordServerData={recordServerData}
124+
groupPointsServerDataMap={groupPointsServerDataMap}
125+
/>
126+
);
127+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { dehydrate } from '@tanstack/react-query';
2+
import { BaseNodeResourceType } from '@teable/openapi';
3+
import { AutomationPage } from '@/features/app/automation/Pages';
4+
import type { IBaseResourceParsed } from '../hooks/useBaseResource';
5+
import type { ISSRContext, SSRResult } from './types';
6+
7+
export const getWorkflowServerSideProps = async (
8+
ctx: ISSRContext,
9+
parsed: IBaseResourceParsed
10+
): Promise<SSRResult> => {
11+
if (parsed.resourceType !== BaseNodeResourceType.Workflow) return { notFound: true };
12+
13+
return {
14+
props: {
15+
...(await ctx.getTranslationsProps()),
16+
dehydratedState: dehydrate(ctx.queryClient),
17+
},
18+
};
19+
};
20+
21+
export const WorkflowPage = () => {
22+
return <AutomationPage />;
23+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { SSRResult } from './types';
2+
3+
export const redirect = (destination: string): SSRResult => ({
4+
redirect: { destination, permanent: false },
5+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type { ISSRContext, SSRHandler, SSRResult } from './types';
2+
3+
export { redirect } from './helper';
4+
5+
export { TablePage, getTableServerSideProps } from './TablePage';
6+
export { DashBoardPage, getDashboardServerSideProps } from './DashBoardPage';
7+
export { WorkflowPage, getWorkflowServerSideProps } from './WorkflowPage';
8+
export { getBaseServerSideProps } from './BasePage';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { QueryClient } from '@tanstack/react-query';
2+
import type { IFieldVo, IRecord, IViewVo } from '@teable/core';
3+
import type { IGroupPointsVo } from '@teable/openapi';
4+
import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
5+
import type { SSRConfig } from 'next-i18next';
6+
import type { SsrApi } from '@/backend/api/rest/ssr-api';
7+
import type { IBaseResourceParsed } from '@/features/app/hooks/useBaseResource';
8+
import type { IBasePageProps } from '@/lib/type';
9+
export interface ITablePageProps {
10+
fieldServerData?: IFieldVo[];
11+
viewServerData?: IViewVo[];
12+
recordsServerData?: { records: IRecord[] };
13+
recordServerData?: IRecord;
14+
groupPointsServerDataMap?: { [viewId: string]: IGroupPointsVo | undefined };
15+
}
16+
17+
export type IBaseNodePageProps = IBasePageProps & Partial<ITablePageProps>;
18+
19+
export interface ISSRContext {
20+
context: GetServerSidePropsContext;
21+
queryClient: QueryClient;
22+
baseId: string;
23+
ssrApi: SsrApi;
24+
getTranslationsProps: () => Promise<SSRConfig>;
25+
}
26+
27+
export type SSRResult = GetServerSidePropsResult<IBaseNodePageProps>;
28+
29+
export type SSRHandler = (
30+
ctx: ISSRContext,
31+
parsed: IBaseResourceParsed,
32+
queryParams?: Record<string, string | string[] | undefined>
33+
) => Promise<SSRResult>;

0 commit comments

Comments
 (0)