Skip to content

Commit

Permalink
feat: pin space and base (#628)
Browse files Browse the repository at this point in the history
* feat: optimize ui display

* feat: change password to trigger browser autosave

* feat: pin space and base

* chore: remove console code
  • Loading branch information
boris-w committed May 29, 2024
1 parent ba07d6c commit b95cd79
Show file tree
Hide file tree
Showing 47 changed files with 1,014 additions and 100 deletions.
2 changes: 2 additions & 0 deletions apps/nestjs-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ImportOpenApiModule } from './features/import/open-api/import-open-api.
import { InvitationModule } from './features/invitation/invitation.module';
import { NextModule } from './features/next/next.module';
import { NotificationModule } from './features/notification/notification.module';
import { PinModule } from './features/pin/pin.module';
import { SelectionModule } from './features/selection/selection.module';
import { ShareModule } from './features/share/share.module';
import { SpaceModule } from './features/space/space.module';
Expand Down Expand Up @@ -45,6 +46,7 @@ export const appModules = {
AccessTokenModule,
ImportOpenApiModule,
ExportOpenApiModule,
PinModule,
],
providers: [InitBootstrapProvider],
};
Expand Down
2 changes: 2 additions & 0 deletions apps/nestjs-backend/src/event-emitter/event-emitter.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ActionTriggerListener } from './listeners/action-trigger.listener';
import { AttachmentListener } from './listeners/attachment.listener';
import { BasePermissionUpdateListener } from './listeners/base-permission-update.listener';
import { CollaboratorNotificationListener } from './listeners/collaborator-notification.listener';
import { PinListener } from './listeners/pin.listener';

export interface EventEmitterModuleOptions {
global?: boolean;
Expand Down Expand Up @@ -38,6 +39,7 @@ export class EventEmitterModule extends EventEmitterModuleClass {
CollaboratorNotificationListener,
AttachmentListener,
BasePermissionUpdateListener,
PinListener,
],
exports: [EventEmitterService],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export class BasePermissionUpdateListener {
payload: { baseId },
} = listenerEvent;
const channel = getBasePermissionUpdateChannel(baseId);
console.log('basePermissionUpdateListener trigger');
const presence = this.shareDbService.connect().getPresence(channel);
const localPresence = presence.create();

Expand Down
34 changes: 34 additions & 0 deletions apps/nestjs-backend/src/event-emitter/listeners/pin.listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PrismaService } from '@teable/db-main-prisma';
import type { SpaceDeleteEvent, BaseDeleteEvent } from '../events';
import { Events } from '../events';

@Injectable()
export class PinListener {
private readonly logger = new Logger(PinListener.name);

constructor(private readonly prismaService: PrismaService) {}

@OnEvent(Events.BASE_DELETE, { async: true })
@OnEvent(Events.SPACE_DELETE, { async: true })
async spaceAndBaseDelete(listenerEvent: SpaceDeleteEvent | BaseDeleteEvent) {
let id: string = '';
if (listenerEvent.name === Events.SPACE_DELETE) {
id = listenerEvent.payload.spaceId;
}
if (listenerEvent.name === Events.BASE_DELETE) {
id = listenerEvent.payload.baseId;
}

if (!id) {
return;
}

await this.prismaService.pinResource.deleteMany({
where: {
resourceId: id,
},
});
}
}
2 changes: 1 addition & 1 deletion apps/nestjs-backend/src/features/base/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class BaseService {
});

await updateOrder({
parentId: base.spaceId,
query: base.spaceId,
position,
item: base,
anchorItem: anchorBase,
Expand Down
37 changes: 37 additions & 0 deletions apps/nestjs-backend/src/features/pin/pin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Body, Controller, Delete, Get, Post, Put, Query } from '@nestjs/common';
import type { GetPinListVo } from '@teable/openapi';
import {
AddPinRo,
DeletePinRo,
addPinRoSchema,
deletePinRoSchema,
UpdatePinOrderRo,
updatePinOrderRoSchema,
} from '@teable/openapi';
import { ZodValidationPipe } from '../../zod.validation.pipe';
import { PinService } from './pin.service';

@Controller('api/pin')
export class PinController {
constructor(private readonly pinService: PinService) {}

@Post()
async add(@Body(new ZodValidationPipe(addPinRoSchema)) query: AddPinRo) {
return this.pinService.addPin(query);
}

@Delete()
async delete(@Query(new ZodValidationPipe(deletePinRoSchema)) query: DeletePinRo) {
return this.pinService.deletePin(query);
}

@Get('list')
async getList(): Promise<GetPinListVo> {
return this.pinService.getList();
}

@Put('order')
async updateOrder(@Body(new ZodValidationPipe(updatePinOrderRoSchema)) body: UpdatePinOrderRo) {
return this.pinService.updateOrder(body);
}
}
9 changes: 9 additions & 0 deletions apps/nestjs-backend/src/features/pin/pin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PinController } from './pin.controller';
import { PinService } from './pin.service';

@Module({
providers: [PinService],
controllers: [PinController],
})
export class PinModule {}
154 changes: 154 additions & 0 deletions apps/nestjs-backend/src/features/pin/pin.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import type { Prisma } from '@teable/db-main-prisma';
import { PrismaService } from '@teable/db-main-prisma';
import type {
PinType,
GetPinListVo,
AddPinRo,
DeletePinRo,
UpdatePinOrderRo,
} from '@teable/openapi';
import { ClsService } from 'nestjs-cls';
import type { IClsStore } from '../../types/cls';
import { updateOrder } from '../../utils/update-order';

@Injectable()
export class PinService {
constructor(
private readonly prismaService: PrismaService,
private readonly cls: ClsService<IClsStore>
) {}

private async getMaxOrder(where: Prisma.PinResourceWhereInput) {
const aggregate = await this.prismaService.pinResource.aggregate({
where,
_max: { order: true },
});
return aggregate._max.order || 0;
}

async addPin(query: AddPinRo) {
const { type, id } = query;
const maxOrder = await this.getMaxOrder({
createdBy: this.cls.get('user.id'),
});
return this.prismaService.pinResource
.create({
data: {
type,
resourceId: id,
createdBy: this.cls.get('user.id'),
order: maxOrder + 1,
},
})
.catch(() => {
throw new BadRequestException('Pin already exists');
});
}

async deletePin(query: DeletePinRo) {
const { id, type } = query;
return this.prismaService.pinResource
.delete({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
createdBy_resourceId: {
resourceId: id,
createdBy: this.cls.get('user.id'),
},
type,
},
})
.catch(() => {
throw new NotFoundException('Pin not found');
});
}

async getList(): Promise<GetPinListVo> {
const list = await this.prismaService.pinResource.findMany({
where: {
createdBy: this.cls.get('user.id'),
},
select: {
resourceId: true,
type: true,
order: true,
},
orderBy: {
order: 'asc',
},
});
return list.map((item) => ({
id: item.resourceId,
type: item.type as PinType,
order: item.order,
}));
}

async updateOrder(data: UpdatePinOrderRo) {
const { id, type, position, anchorId, anchorType } = data;

const item = await this.prismaService.pinResource
.findFirstOrThrow({
select: { order: true, id: true },
where: {
resourceId: id,
type,
createdBy: this.cls.get('user.id'),
},
})
.catch(() => {
throw new NotFoundException('Pin not found');
});

const anchorItem = await this.prismaService.pinResource
.findFirstOrThrow({
select: { order: true, id: true },
where: {
resourceId: anchorId,
type: anchorType,
createdBy: this.cls.get('user.id'),
},
})
.catch(() => {
throw new NotFoundException('Pin Anchor not found');
});

await updateOrder({
query: undefined,
position,
item,
anchorItem,
getNextItem: async (whereOrder, align) => {
return this.prismaService.pinResource.findFirst({
select: { order: true, id: true },
where: {
resourceId: id,
type: type,
order: whereOrder,
},
orderBy: { order: align },
});
},
update: async (_, id, data) => {
await this.prismaService.pinResource.update({
data: { order: data.newOrder },
where: { id },
});
},
shuffle: async () => {
const orderKey = position === 'before' ? 'lt' : 'gt';
const dataOrderKey = position === 'before' ? 'decrement' : 'increment';
await this.prismaService.pinResource.updateMany({
data: { order: { [dataOrderKey]: 1 } },
where: {
createdBy: this.cls.get('user.id'),
order: {
[orderKey]: anchorItem.order,
},
},
});
},
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ export class TableOpenApiService {
});

await updateOrder({
parentId: baseId,
query: baseId,
position,
item: table,
anchorItem: anchorTable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export class ViewOpenApiService {
});

await updateOrder({
parentId: tableId,
query: tableId,
position,
item: view,
anchorItem: anchorView,
Expand Down
8 changes: 4 additions & 4 deletions apps/nestjs-backend/src/utils/update-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('updateOrder', () => {
// Setup for case 1
getNextItemMock.mockResolvedValueOnce({ id: '2', order: 2 });
const params = {
parentId: 'parent1',
query: 'parent1',
position: 'before' as const,
item: { id: 'item1', order: 4 },
anchorItem: { id: 'anchor', order: 3 },
Expand All @@ -40,7 +40,7 @@ describe('updateOrder', () => {
// Setup for case 2
getNextItemMock.mockResolvedValueOnce({ id: '4', order: 4 });
const params = {
parentId: 'parent1',
query: 'parent1',
position: 'after' as const,
item: { id: 'item1', order: 2 },
anchorItem: { id: 'anchor', order: 3 },
Expand All @@ -66,7 +66,7 @@ describe('updateOrder', () => {
// Setup: getNextItem returns null
getNextItemMock.mockResolvedValueOnce(null);
const params = {
parentId: 'parent1',
query: 'parent1',
position: 'after' as const, // Can test 'before' in a similar manner with adjusted logic
item: { id: 'item1', order: 4 },
anchorItem: { id: 'anchor', order: 5 },
Expand All @@ -86,7 +86,7 @@ describe('updateOrder', () => {
// Setup: getNextItem returns a value that would cause a shuffle due to close orders
getNextItemMock.mockResolvedValueOnce({ id: 'anchor', order: 3 - Number.EPSILON });
const params = {
parentId: 'parent1',
query: 'parent1',
position: 'before' as const,
item: { id: 'item1', order: 4 },
anchorItem: { id: 'anchor', order: 3 },
Expand Down
18 changes: 7 additions & 11 deletions apps/nestjs-backend/src/utils/update-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,19 @@
* orderBy asc, we have [4, 5]
* pick the first one, we have 4
*/
export async function updateOrder(params: {
parentId: string;
export async function updateOrder<T>(params: {
query: T;
position: 'before' | 'after';
item: { id: string; order: number };
anchorItem: { id: string; order: number };
getNextItem: (
whereOrder: { lt?: number; gt?: number },
align: 'desc' | 'asc'
) => Promise<{ id: string; order: number } | null>;
update: (
parentId: string,
id: string,
data: { newOrder: number; oldOrder: number }
) => Promise<void>;
shuffle: (parentId: string) => Promise<void>;
update: (query: T, id: string, data: { newOrder: number; oldOrder: number }) => Promise<void>;
shuffle: (query: T) => Promise<void>;
}) {
const { parentId, position, item, anchorItem, getNextItem, update, shuffle } = params;
const { query, position, item, anchorItem, getNextItem, update, shuffle } = params;
const nextView = await getNextItem(
{ [position === 'before' ? 'lt' : 'gt']: anchorItem.order },
position === 'before' ? 'desc' : 'asc'
Expand All @@ -42,12 +38,12 @@ export async function updateOrder(params: {
const { id, order: oldOrder } = item;

if (Math.abs(order - anchorItem.order) < Number.EPSILON * 2) {
await shuffle(parentId);
await shuffle(query);
// recursive call
await updateOrder(params);
return;
}
await update(parentId, id, { newOrder: order, oldOrder });
await update(query, id, { newOrder: order, oldOrder });
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export const SideBarHeader = (props: ISideBarInteractionProps) => {

const backSpace = () => {
router.push({
pathname: '/space',
pathname: '/space/[spaceId]',
query: { spaceId: base.spaceId },
});
};

Expand Down
Loading

0 comments on commit b95cd79

Please sign in to comment.