Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5fbb079
feat: space layout
younocode Dec 9, 2025
d8b0c8e
feat: add lastModifiedTime and createdUser details to service responses
younocode Dec 11, 2025
e4183de
refactor: update trash service and UI components
younocode Dec 11, 2025
e8ae500
refactor: remove unused router dependency
younocode Dec 11, 2025
26725fc
feat: enhance UI for shared bases and trash pages with improved layou…
younocode Dec 11, 2025
1bf3f1a
feat: add createdTime field to service responses and update sorting l…
younocode Dec 11, 2025
bd1834b
fix: adjust layout in SpaceInnerPage component
younocode Dec 11, 2025
214ba1b
feat: add new translation keys in multiple languages
younocode Dec 11, 2025
09e3084
fix: adjust padding in BaseItem component for improved layout
younocode Dec 11, 2025
518c8c0
feat: update translations and replace icon in trash components
younocode Dec 11, 2025
75b5240
feat: double-click editing in BaseNodeTree
younocode Dec 11, 2025
5976f00
refactor: remove dragHandleProps from BaseItem and BaseList component…
younocode Dec 11, 2025
d74d2ae
fix: update some style issue for space layout
hammond-lj Dec 11, 2025
1232966
refactor: improve layout and styling
younocode Dec 11, 2025
be532b3
fix: adjust layout in BaseItem and BaseList components
younocode Dec 11, 2025
4a198a3
refactor: enhance layout and styling
younocode Dec 11, 2025
4652ba8
refactor: rename variables for clarity in user-related data handling …
younocode Dec 11, 2025
085ca6f
refactor: initialize treeItems from cache to prevent empty state fla…
younocode Dec 11, 2025
e4bfcef
refactor: optimize BaseList sorting logic to handle null values and i…
younocode Dec 11, 2025
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
25 changes: 24 additions & 1 deletion apps/nestjs-backend/src/features/base/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
IUpdateBaseRo,
IUpdateOrderRo,
} from '@teable/openapi';
import { keyBy } from 'lodash';
import { ClsService } from 'nestjs-cls';
import { IThresholdConfig, ThresholdConfig } from '../../configs/threshold.config';
import { CustomHttpException } from '../../custom.exception';
Expand All @@ -20,6 +21,7 @@ import { IDbProvider } from '../../db-provider/db.provider.interface';
import type { IClsStore } from '../../types/cls';
import { getMaxLevelRole } from '../../utils/get-max-level-role';
import { updateOrder } from '../../utils/update-order';
import { getPublicFullStorageUrl } from '../attachments/plugins/utils';
import { PermissionService } from '../auth/permission.service';
import { CollaboratorService } from '../collaborator/collaborator.service';
import { GraphService } from '../graph/graph.service';
Expand Down Expand Up @@ -52,6 +54,7 @@ export class BaseService {
name: true,
icon: true,
spaceId: true,
createdBy: true,
},
where: {
id: baseId,
Expand Down Expand Up @@ -101,6 +104,9 @@ export class BaseService {
order: true,
spaceId: true,
icon: true,
createdBy: true,
createdTime: true,
lastModifiedTime: true,
},
where: {
deletedTime: null,
Expand All @@ -122,9 +128,26 @@ export class BaseService {
},
orderBy: [{ spaceId: 'asc' }, { order: 'asc' }],
});

const createdUserList = await this.prismaService.user.findMany({
where: { id: { in: baseList.map((base) => base.createdBy) } },
select: { id: true, name: true, avatar: true },
});
const createdUserMap = keyBy(createdUserList, 'id');

return baseList.map((base) => {
const role = roleMap[base.id] || roleMap[base.spaceId];
return { ...base, role };
const createdUser = createdUserMap[base.createdBy];
return {
...base,
role,
lastModifiedTime: base.lastModifiedTime?.toISOString(),
createdTime: base.createdTime?.toISOString(),
createdUser: {
...(createdUser ?? {}),
avatar: createdUser?.avatar && getPublicFullStorageUrl(createdUser.avatar),
},
};
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
} from '@teable/openapi';
import { CollaboratorType, PrincipalType } from '@teable/openapi';
import { Knex } from 'knex';
import { difference, map } from 'lodash';
import { difference, keyBy, map } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { ClsService } from 'nestjs-cls';
import { CustomHttpException } from '../../custom.exception';
Expand Down Expand Up @@ -830,6 +830,12 @@ export class CollaboratorService {
},
},
});

const createdUserList = await this.prismaService.txClient().user.findMany({
where: { id: { in: bases.map((base) => base.createdBy) } },
select: { id: true, name: true, avatar: true },
});
const createdUserMap = keyBy(createdUserList, 'id');
return bases.map((base) => ({
id: base.id,
name: base.name,
Expand All @@ -838,6 +844,15 @@ export class CollaboratorService {
spaceId: base.spaceId,
spaceName: base.space?.name,
collaboratorType: CollaboratorType.Base,
lastModifiedTime: base.lastModifiedTime?.toISOString(),
createdTime: base.createdTime?.toISOString(),
createdBy: base.createdBy,
createdUser: {
...(createdUserMap[base.createdBy] ?? {}),
avatar:
createdUserMap[base.createdBy]?.avatar &&
getPublicFullStorageUrl(createdUserMap[base.createdBy]?.avatar ?? ''),
},
}));
}

Expand Down
25 changes: 22 additions & 3 deletions apps/nestjs-backend/src/features/space/space.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ import type {
IUpdateSpaceRo,
} from '@teable/openapi';
import { ResourceType, CollaboratorType, PrincipalType, IntegrationType } from '@teable/openapi';
import { map } from 'lodash';
import { keyBy, map } from 'lodash';
import { ClsService } from 'nestjs-cls';
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
import { CustomHttpException } from '../../custom.exception';
import { PerformanceCache, PerformanceCacheService } from '../../performance-cache';
import { generateIntegrationCacheKey } from '../../performance-cache/generate-keys';
import type { IClsStore } from '../../types/cls';
import { getPublicFullStorageUrl } from '../attachments/plugins/utils';
import { PermissionService } from '../auth/permission.service';
import { BaseService } from '../base/base.service';
import { CollaboratorService } from '../collaborator/collaborator.service';
import { SettingOpenApiService } from '../setting/open-api/setting-open-api.service';
import { SettingService } from '../setting/setting.service';

@Injectable()
export class SpaceService {
constructor(
Expand Down Expand Up @@ -267,6 +267,9 @@ export class SpaceService {
order: true,
spaceId: true,
icon: true,
createdBy: true,
lastModifiedTime: true,
createdTime: true,
},
where: {
spaceId,
Expand All @@ -277,9 +280,25 @@ export class SpaceService {
},
});

const createdUserList = await this.prismaService.user.findMany({
where: { id: { in: baseList.map((base) => base.createdBy) } },
select: { id: true, name: true, avatar: true },
});
const createdUserMap = keyBy(createdUserList, 'id');

return baseList.map((base) => {
const role = roleMap[base.id] || roleMap[base.spaceId];
return { ...base, role };
const createdUser = createdUserMap[base.createdBy];
return {
...base,
role,
lastModifiedTime: base.lastModifiedTime?.toISOString(),
createdTime: base.createdTime?.toISOString(),
createdUser: {
...createdUser,
avatar: createdUser?.avatar && getPublicFullStorageUrl(createdUser.avatar),
},
};
});
}

Expand Down
19 changes: 11 additions & 8 deletions apps/nestjs-backend/src/features/trash/trash.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ export class TrashService {
}

async getTrash(trashRo: ITrashRo) {
const { resourceType } = trashRo;
const { resourceType, spaceId } = trashRo;

switch (resourceType) {
case ResourceType.Space:
return await this.getSpaceTrash();
case ResourceType.Base:
return await this.getBaseTrash();
return await this.getBaseTrash(spaceId);
default:
throw new CustomHttpException(
`Invalid resource type ${resourceType}`,
Expand Down Expand Up @@ -150,23 +150,26 @@ export class TrashService {
};
}

private async getBaseTrash() {
private async getBaseTrash(spaceId?: string) {
const { bases } = await this.getAuthorizedSpacesAndBases();
const baseIds = bases.map((base) => base.id);
const spaceIds = bases.map((base) => base.spaceId);
const authorizedBaseIds = bases.map((base) => base.id);
const authorizedBaseSpaceIds = bases.map((base) => base.spaceId);
const baseIdMap = keyBy(bases, 'id');

const trashedSpaces = await this.prismaService.trash.findMany({
where: {
resourceType: ResourceType.Space,
resourceId: { in: spaceIds },
resourceId: { in: authorizedBaseSpaceIds },
},
select: { resourceId: true },
});
const list = await this.prismaService.trash.findMany({
where: {
parentId: { notIn: trashedSpaces.map((space) => space.resourceId) },
resourceId: { in: baseIds },
parentId: {
notIn: trashedSpaces.map((space) => space.resourceId),
in: spaceId ? [spaceId] : undefined,
},
resourceId: { in: authorizedBaseIds },
resourceType: ResourceType.Base,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,33 @@ export class LastVisitService {
};
}

async spaceVisit(userId: string, parentResourceId: string) {
const lastVisit = await this.prismaService.userLastVisit.findFirst({
where: {
userId,
parentResourceId,
resourceType: LastVisitResourceType.Space,
},
orderBy: {
lastVisitTime: 'desc',
},
take: 1,
select: {
resourceId: true,
resourceType: true,
},
});

if (lastVisit) {
return {
resourceId: lastVisit.resourceId,
resourceType: lastVisit.resourceType as LastVisitResourceType,
};
}

return undefined;
}

async tableVisit(userId: string, baseId: string): Promise<IUserLastVisitVo | undefined> {
const knex = this.knex;

Expand Down Expand Up @@ -404,6 +431,7 @@ export class LastVisitService {
resourceIcon: 'b.icon',
resourceRole: 'c.role_name',
spaceId: 's.id',
createBy: 'b.created_by',
})
.from('user_last_visit as ulv')
.join('base as b', function () {
Expand Down Expand Up @@ -436,6 +464,7 @@ export class LastVisitService {
resourceIcon: string;
resourceRole: IRole;
spaceId: string;
createBy: string;
}[]
>(query.toQuery());

Expand All @@ -449,6 +478,7 @@ export class LastVisitService {
icon: result.resourceIcon,
role: result.resourceRole,
spaceId: result.spaceId,
createdBy: result.createBy,
},
}));

Expand All @@ -463,6 +493,8 @@ export class LastVisitService {
params: IGetUserLastVisitRo
): Promise<IUserLastVisitVo | undefined> {
switch (params.resourceType) {
case LastVisitResourceType.Space:
return this.spaceVisit(userId, params.parentResourceId);
case LastVisitResourceType.Table:
return this.tableVisit(userId, params.parentResourceId);
case LastVisitResourceType.View:
Expand Down Expand Up @@ -495,7 +527,6 @@ export class LastVisitService {
resourceType: LastVisitResourceType.Base,
resourceId,
parentResourceId,
maxRecords: 10,
});
return;
}
Expand Down
14 changes: 14 additions & 0 deletions apps/nestjs-backend/src/types/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export type I18nTranslations = {
"exit": string;
"next": string;
"previous": string;
"select": string;
"continue": string;
"export": string;
"import": string;
Expand Down Expand Up @@ -837,6 +838,7 @@ export type I18nTranslations = {
};
};
"trash": {
"spaceTrash": string;
"type": string;
"resetTrash": string;
"deletedBy": string;
Expand Down Expand Up @@ -2657,6 +2659,10 @@ export type I18nTranslations = {
"description": string;
"empty": string;
};
"trash": {
"spaceDescription": string;
"baseDescription": string;
};
"integration": {
"title": string;
"description": string;
Expand Down Expand Up @@ -2693,6 +2699,14 @@ export type I18nTranslations = {
"title": string;
"description": string;
};
"baseList": {
"allBases": string;
"owner": string;
"lastOpened": string;
"enter": string;
"noTables": string;
"empty": string;
};
};
"system": {
"notFound": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getBaseNodeChannel } from '@teable/core';
import type { IBaseNodeVo } from '@teable/openapi';
import type { IBaseNodeTreeVo, IBaseNodeVo } from '@teable/openapi';
import { getBaseNodeTree } from '@teable/openapi';
import { ReactQueryKeys } from '@teable/sdk/config';
import { useConnection } from '@teable/sdk/hooks';
Expand All @@ -16,10 +16,21 @@ export const useBaseNode = (baseId: string) => {
const channel = getBaseNodeChannel(baseId);
const presence = connection?.getPresence(channel);
const [nodes, setNodes] = useState<IBaseNodeVo[]>([]);
const [treeItems, setTreeItems] = useState<Record<string, TreeItemData>>({});
const [shouldInvalidate, setShouldInvalidate] = useState(0);

const queryClient = useQueryClient();

// Initialize treeItems from cache to avoid flash of empty state on remount
const [treeItems, setTreeItems] = useState<Record<string, TreeItemData>>(() => {
const cachedData = queryClient.getQueryData<IBaseNodeTreeVo>(
ReactQueryKeys.baseNodeTree(baseId)
);
if (cachedData?.nodes && cachedData.nodes.length > 0) {
return buildTreeItems(cachedData.nodes);
}
return {};
});

const { data: queryData, isLoading } = useQuery({
queryKey: ReactQueryKeys.baseNodeTree(baseId),
queryFn: ({ queryKey }) => getBaseNodeTree(queryKey[1]).then((res) => res.data),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ interface ICommonOperationProps extends IBaseNodeMoreProps {

const CommonOperation = (props: ICommonOperationProps) => {
const {
open,
setOpen,
onRename,
onDuplicate,
onDelete,
Expand All @@ -82,7 +84,7 @@ const CommonOperation = (props: ICommonOperationProps) => {

return (
<>
<DropdownMenu>
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<div>
<MoreHorizontal className={className} />
Expand Down
Loading
Loading