mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 10:05:03 +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 { BaseRowRepo } from '@docmost/db/repos/base/base-row.repo';
|
||||||
import { BasePropertyRepo } from '@docmost/db/repos/base/base-property.repo';
|
import { BasePropertyRepo } from '@docmost/db/repos/base/base-property.repo';
|
||||||
import { BaseRepo } from '@docmost/db/repos/base/base.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 { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
import {
|
import {
|
||||||
IBaseCellGcJob,
|
IBaseCellGcJob,
|
||||||
@@ -39,6 +41,7 @@ export class BaseQueueProcessor
|
|||||||
private readonly baseRowRepo: BaseRowRepo,
|
private readonly baseRowRepo: BaseRowRepo,
|
||||||
private readonly basePropertyRepo: BasePropertyRepo,
|
private readonly basePropertyRepo: BasePropertyRepo,
|
||||||
private readonly baseRepo: BaseRepo,
|
private readonly baseRepo: BaseRepo,
|
||||||
|
private readonly basePropertyService: BasePropertyService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly formulaLock: FormulaLockService,
|
private readonly formulaLock: FormulaLockService,
|
||||||
) {
|
) {
|
||||||
@@ -95,6 +98,13 @@ export class BaseQueueProcessor
|
|||||||
schemaVersion,
|
schemaVersion,
|
||||||
data.actorId,
|
data.actorId,
|
||||||
);
|
);
|
||||||
|
await this.basePropertyService.clearKanbanGroupingIfNotGroupable(
|
||||||
|
data.pageId,
|
||||||
|
data.propertyId,
|
||||||
|
data.toType as BasePropertyTypeValue,
|
||||||
|
data.workspaceId,
|
||||||
|
data.actorId ?? null,
|
||||||
|
);
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
case QueueJob.BASE_CELL_GC: {
|
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', () => {
|
describe('stripPropertyFromViewConfig', () => {
|
||||||
it('returns the config unchanged when no references exist', () => {
|
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 { BaseRowRepo } from '@docmost/db/repos/base/base-row.repo';
|
||||||
import { BaseRepo } from '@docmost/db/repos/base/base.repo';
|
import { BaseRepo } from '@docmost/db/repos/base/base.repo';
|
||||||
import { BaseViewRepo } from '@docmost/db/repos/base/base-view.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 { CreatePropertyDto } from '../dto/create-property.dto';
|
||||||
import {
|
import {
|
||||||
UpdatePropertyDto,
|
UpdatePropertyDto,
|
||||||
@@ -374,6 +377,13 @@ export class BasePropertyService {
|
|||||||
schemaVersion,
|
schemaVersion,
|
||||||
};
|
};
|
||||||
this.eventEmitter.emit(EventName.BASE_SCHEMA_BUMPED, bumpEvent);
|
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);
|
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(
|
private async loadAndEmit(
|
||||||
dto: UpdatePropertyDto,
|
dto: UpdatePropertyDto,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -79,3 +79,23 @@ export function stripPropertyFromViewConfig(
|
|||||||
|
|
||||||
return next as ViewConfig;
|
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