Add per-user saved column visibility for tables

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 11:56:49 +03:30
co-authored by Cursor
parent 75ff2c36c4
commit 23c83ffbee
29 changed files with 624 additions and 23 deletions
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE IF NOT EXISTS "UserTablePreference" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"tableKey" TEXT NOT NULL,
"hiddenColumns" TEXT[] DEFAULT ARRAY[]::TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UserTablePreference_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_tableKey_key" ON "UserTablePreference"("userId", "organizationId", "tableKey");
CREATE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_idx" ON "UserTablePreference"("userId", "organizationId");
-- AddForeignKey
DO $$ BEGIN
ALTER TABLE "UserTablePreference" ADD CONSTRAINT "UserTablePreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
+17
View File
@@ -264,6 +264,7 @@ model User {
billingAccount BillingAccount?
auditLogs AuditLog[] @relation("AuditLogActor")
savedViews SavedView[] @relation("SavedViewOwner")
tablePreferences UserTablePreference[]
assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees")
teamMemberships TeamMember[]
reservationAssignments ReservationAppointment[] @relation("ReservationAssignee")
@@ -1712,6 +1713,22 @@ model AuditLog {
}
/// فیلترهای ذخیره‌شده برای لیست‌ها و کاریزها
/// ستون‌های مخفی هر جدول برای هر کاربر (به‌ازای هر سازمان)
model UserTablePreference {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organizationId String
tableKey String
/// ستون جدیدی که بعداً اضافه شود به‌صورت پیش‌فرض نمایش داده می‌شود
hiddenColumns String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, organizationId, tableKey])
@@index([userId, organizationId])
}
model SavedView {
id String @id @default(cuid())
organizationId String
+2
View File
@@ -37,6 +37,7 @@ import { PlatformModule } from './platform/platform.module';
import { BillingModule } from './billing/billing.module';
import { AuditLogsModule } from './audit-logs/audit-logs.module';
import { SavedViewsModule } from './saved-views/saved-views.module';
import { TablePrefsModule } from './table-prefs/table-prefs.module';
import { PublicApiModule } from './public-api/public-api.module';
import { SmsCampaignsModule } from './sms-campaigns/sms-campaigns.module';
import { TeamsModule } from './teams/teams.module';
@@ -89,6 +90,7 @@ import { ConversationsModule } from './conversations/conversations.module';
TahesabModule,
AuditLogsModule,
SavedViewsModule,
TablePrefsModule,
PublicApiModule,
SmsCampaignsModule,
TrackingModule,
@@ -0,0 +1,92 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Put,
UseGuards,
} from '@nestjs/common';
import {
ArrayMaxSize,
IsArray,
IsString,
Matches,
MaxLength,
} from 'class-validator';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { PrismaService } from '../prisma/prisma.service';
const TABLE_KEY_RE = /^[a-z0-9_.-]{1,80}$/i;
class SaveTablePrefDto {
@IsArray()
@ArrayMaxSize(80)
@IsString({ each: true })
@MaxLength(80, { each: true })
hiddenColumns!: string[];
}
class TableKeyParam {
@Matches(TABLE_KEY_RE)
key!: string;
}
type Actor = { userId: string; organizationId: string };
@Controller('table-prefs')
@UseGuards(JwtAuthGuard)
export class TablePrefsController {
constructor(private readonly prisma: PrismaService) {}
@Get()
async list(@CurrentUser() user: Actor) {
const rows = await this.prisma.userTablePreference.findMany({
where: { userId: user.userId, organizationId: user.organizationId },
select: { tableKey: true, hiddenColumns: true },
});
const out: Record<string, string[]> = {};
for (const row of rows) out[row.tableKey] = row.hiddenColumns;
return out;
}
@Put(':key')
async save(
@CurrentUser() user: Actor,
@Param() params: TableKeyParam,
@Body() dto: SaveTablePrefDto,
) {
const hiddenColumns = [...new Set(dto.hiddenColumns.map((c) => c.trim()).filter(Boolean))];
const row = await this.prisma.userTablePreference.upsert({
where: {
userId_organizationId_tableKey: {
userId: user.userId,
organizationId: user.organizationId,
tableKey: params.key,
},
},
create: {
userId: user.userId,
organizationId: user.organizationId,
tableKey: params.key,
hiddenColumns,
},
update: { hiddenColumns },
select: { tableKey: true, hiddenColumns: true },
});
return row;
}
@Delete(':key')
async reset(@CurrentUser() user: Actor, @Param() params: TableKeyParam) {
await this.prisma.userTablePreference.deleteMany({
where: {
userId: user.userId,
organizationId: user.organizationId,
tableKey: params.key,
},
});
return { ok: true };
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { TablePrefsController } from './table-prefs.controller';
@Module({
imports: [AuthModule],
controllers: [TablePrefsController],
})
export class TablePrefsModule {}