mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 01:52:43 +08:00
feat(bases): clear kanban grouping when its property changes to a non-groupable type
This commit is contained in:
@@ -8,6 +8,8 @@ import { executeTx } from '@docmost/db/utils';
|
||||
import { BaseRowRepo } from '@docmost/db/repos/base/base-row.repo';
|
||||
import { BasePropertyRepo } from '@docmost/db/repos/base/base-property.repo';
|
||||
import { BaseRepo } from '@docmost/db/repos/base/base.repo';
|
||||
import { BasePropertyService } from '../services/base-property.service';
|
||||
import { BasePropertyTypeValue } from '../base.schemas';
|
||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||
import {
|
||||
IBaseCellGcJob,
|
||||
@@ -39,6 +41,7 @@ export class BaseQueueProcessor
|
||||
private readonly baseRowRepo: BaseRowRepo,
|
||||
private readonly basePropertyRepo: BasePropertyRepo,
|
||||
private readonly baseRepo: BaseRepo,
|
||||
private readonly basePropertyService: BasePropertyService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly formulaLock: FormulaLockService,
|
||||
) {
|
||||
@@ -95,6 +98,13 @@ export class BaseQueueProcessor
|
||||
schemaVersion,
|
||||
data.actorId,
|
||||
);
|
||||
await this.basePropertyService.clearKanbanGroupingIfNotGroupable(
|
||||
data.pageId,
|
||||
data.propertyId,
|
||||
data.toType as BasePropertyTypeValue,
|
||||
data.workspaceId,
|
||||
data.actorId ?? null,
|
||||
);
|
||||
return summary;
|
||||
}
|
||||
case QueueJob.BASE_CELL_GC: {
|
||||
|
||||
+46
-1
@@ -1,4 +1,7 @@
|
||||
import { stripPropertyFromViewConfig } from '../strip-property-from-view-config';
|
||||
import {
|
||||
clearKanbanGroupingFromViewConfig,
|
||||
stripPropertyFromViewConfig,
|
||||
} from '../strip-property-from-view-config';
|
||||
|
||||
describe('stripPropertyFromViewConfig', () => {
|
||||
it('returns the config unchanged when no references exist', () => {
|
||||
@@ -124,3 +127,45 @@ describe('stripPropertyFromViewConfig', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearKanbanGroupingFromViewConfig', () => {
|
||||
it("returns the config unchanged (by reference) when groupByPropertyId doesn't match", () => {
|
||||
const config = { groupByPropertyId: 'p-other' };
|
||||
expect(clearKanbanGroupingFromViewConfig(config, 'p-here')).toBe(config);
|
||||
});
|
||||
|
||||
it('returns {} when config is null or undefined', () => {
|
||||
expect(clearKanbanGroupingFromViewConfig(null, 'p1')).toEqual({});
|
||||
expect(clearKanbanGroupingFromViewConfig(undefined, 'p1')).toEqual({});
|
||||
});
|
||||
|
||||
it('clears kanban fields when groupByPropertyId matches', () => {
|
||||
const result = clearKanbanGroupingFromViewConfig(
|
||||
{
|
||||
groupByPropertyId: 'p1',
|
||||
hiddenChoiceIds: ['c1'],
|
||||
choiceOrder: ['c1'],
|
||||
sorts: [{ propertyId: 'p2', direction: 'asc' as const }],
|
||||
},
|
||||
'p1',
|
||||
);
|
||||
expect(result).toEqual({
|
||||
sorts: [{ propertyId: 'p2', direction: 'asc' as const }],
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves non-kanban fields untouched when clearing', () => {
|
||||
const result = clearKanbanGroupingFromViewConfig(
|
||||
{
|
||||
groupByPropertyId: 'p1',
|
||||
filter: { op: 'and' as const, children: [] },
|
||||
visiblePropertyIds: ['p2'],
|
||||
},
|
||||
'p1',
|
||||
);
|
||||
expect(result).toEqual({
|
||||
filter: { op: 'and' as const, children: [] },
|
||||
visiblePropertyIds: ['p2'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,10 @@ import { BasePropertyRepo } from '@docmost/db/repos/base/base-property.repo';
|
||||
import { BaseRowRepo } from '@docmost/db/repos/base/base-row.repo';
|
||||
import { BaseRepo } from '@docmost/db/repos/base/base.repo';
|
||||
import { BaseViewRepo } from '@docmost/db/repos/base/base-view.repo';
|
||||
import { stripPropertyFromViewConfig } from './strip-property-from-view-config';
|
||||
import {
|
||||
clearKanbanGroupingFromViewConfig,
|
||||
stripPropertyFromViewConfig,
|
||||
} from './strip-property-from-view-config';
|
||||
import { CreatePropertyDto } from '../dto/create-property.dto';
|
||||
import {
|
||||
UpdatePropertyDto,
|
||||
@@ -374,6 +377,13 @@ export class BasePropertyService {
|
||||
schemaVersion,
|
||||
};
|
||||
this.eventEmitter.emit(EventName.BASE_SCHEMA_BUMPED, bumpEvent);
|
||||
await this.clearKanbanGroupingIfNotGroupable(
|
||||
dto.pageId,
|
||||
dto.propertyId,
|
||||
newType,
|
||||
workspaceId,
|
||||
actorId ?? null,
|
||||
);
|
||||
return this.loadAndEmit(dto, workspaceId, actorId, null);
|
||||
}
|
||||
|
||||
@@ -452,6 +462,55 @@ export class BasePropertyService {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Post-type-conversion cleanup: when a property's type flips to something
|
||||
* not groupable (anything other than select/status), drop the kanban
|
||||
* grouping fields from any view rooted on it. Called from BOTH the inline
|
||||
* path here and the BullMQ processor after the pending→live swap, so
|
||||
* `view.config` stays coherent with the property's actual type. Each
|
||||
* touched view re-emits `base.view.updated` so other clients see it.
|
||||
*/
|
||||
async clearKanbanGroupingIfNotGroupable(
|
||||
pageId: string,
|
||||
propertyId: string,
|
||||
newType: BasePropertyTypeValue,
|
||||
workspaceId: string,
|
||||
actorId: string | null,
|
||||
): Promise<void> {
|
||||
if (
|
||||
newType === BasePropertyType.SELECT ||
|
||||
newType === BasePropertyType.STATUS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const views = await this.baseViewRepo.findByPageId(pageId, {
|
||||
workspaceId,
|
||||
});
|
||||
for (const view of views) {
|
||||
const before = (view.config ?? {}) as ViewConfig;
|
||||
const after = clearKanbanGroupingFromViewConfig(before, propertyId);
|
||||
if (after === before) continue;
|
||||
await this.baseViewRepo.updateView(
|
||||
view.id,
|
||||
// `config` column is typed `Json` by Kysely; ViewConfig is a Zod
|
||||
// inferred shape that isn't structurally assignable to `Json`.
|
||||
{ config: after as any },
|
||||
{ workspaceId },
|
||||
);
|
||||
const fresh = await this.baseViewRepo.findById(view.id, { workspaceId });
|
||||
if (fresh) {
|
||||
const event: BaseViewUpdatedEvent = {
|
||||
pageId,
|
||||
workspaceId,
|
||||
actorId,
|
||||
requestId: null,
|
||||
view: fresh,
|
||||
};
|
||||
this.eventEmitter.emit(EventName.BASE_VIEW_UPDATED, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAndEmit(
|
||||
dto: UpdatePropertyDto,
|
||||
workspaceId: string,
|
||||
|
||||
@@ -79,3 +79,23 @@ export function stripPropertyFromViewConfig(
|
||||
|
||||
return next as ViewConfig;
|
||||
}
|
||||
|
||||
/*
|
||||
* Narrower sibling of `stripPropertyFromViewConfig`. Used after a property's
|
||||
* type changes to something no longer groupable (e.g. select → text): the
|
||||
* property still exists, so we leave sorts/filters/visibility alone, and only
|
||||
* clear the kanban grouping fields rooted on it. Returns the input by
|
||||
* reference when nothing matches so callers can skip the write.
|
||||
*/
|
||||
export function clearKanbanGroupingFromViewConfig(
|
||||
config: ViewConfig | undefined | null,
|
||||
propertyId: string,
|
||||
): ViewConfig {
|
||||
if (!config) return {};
|
||||
if (config.groupByPropertyId !== propertyId) return config;
|
||||
const next: Record<string, unknown> = { ...config };
|
||||
delete next.groupByPropertyId;
|
||||
delete next.hiddenChoiceIds;
|
||||
delete next.choiceOrder;
|
||||
return next as ViewConfig;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user