Feat: group records by many to one relation (#22123)
## Group records by relation (Kanban + Table)
Adds grouping by `MANY_TO_ONE` relation fields on both board and table
views, reusing the existing `ViewGroup` storage (`fieldValue = related
record id`).
- **New group** record picker to create relation-backed groups (board
column + table row)
- Relation-aware group headers (name/avatar), filtering, and drag-drop
(writes the FK join column) — all using one canonical `${name}Id` column
- Sort menu hides alphabetical options when grouping by a relation (no
comparable title)
- A group whose backing record no longer exists renders a "Deleted" chip
instead of a blank header
- **Backend:** allow `MANY_TO_ONE` relations as the Kanban
`mainGroupByField` in the flat-view validator
https://github.com/user-attachments/assets/267077a6-2667-4506-b178-eee420a16f20
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22123?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
+47
-11
@@ -1,11 +1,13 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, ViewType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType, RelationType, ViewType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { ViewExceptionCode } from 'src/engine/metadata-modules/view/exceptions/view.exception';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
|
||||
@@ -14,6 +16,28 @@ import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-man
|
||||
export class FlatViewValidatorService {
|
||||
constructor() {}
|
||||
|
||||
private isAllowedKanbanMainGroupByField({
|
||||
mainGroupByFieldMetadata,
|
||||
}: {
|
||||
mainGroupByFieldMetadata: UniversalFlatFieldMetadata;
|
||||
}): boolean {
|
||||
if (mainGroupByFieldMetadata.type === FieldMetadataType.SELECT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!isMorphOrRelationUniversalFlatFieldMetadata(mainGroupByFieldMetadata)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
mainGroupByFieldMetadata.type === FieldMetadataType.RELATION &&
|
||||
mainGroupByFieldMetadata.universalSettings?.relationType ===
|
||||
RelationType.MANY_TO_ONE
|
||||
);
|
||||
}
|
||||
|
||||
public validateFlatViewUpdate({
|
||||
universalIdentifier,
|
||||
flatEntityUpdate,
|
||||
@@ -104,11 +128,15 @@ export class FlatViewValidatorService {
|
||||
message: t`Kanban main group by field metadata not found`,
|
||||
userFriendlyMessage: msg`Kanban main group by field metadata not found`,
|
||||
});
|
||||
} else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) {
|
||||
} else if (
|
||||
!this.isAllowedKanbanMainGroupByField({
|
||||
mainGroupByFieldMetadata,
|
||||
})
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Kanban main group by field must be a SELECT field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select field`,
|
||||
message: t`Kanban main group by field must be a SELECT or a many-to-one relation field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select or a many-to-one relation field`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -133,11 +161,15 @@ export class FlatViewValidatorService {
|
||||
message: t`Kanban main group by field metadata not found`,
|
||||
userFriendlyMessage: msg`Kanban main group by field metadata not found`,
|
||||
});
|
||||
} else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) {
|
||||
} else if (
|
||||
!this.isAllowedKanbanMainGroupByField({
|
||||
mainGroupByFieldMetadata,
|
||||
})
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Kanban main group by field must be a SELECT field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select field`,
|
||||
message: t`Kanban main group by field must be a SELECT or a many-to-one relation field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select or a many-to-one relation field`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -300,11 +332,15 @@ export class FlatViewValidatorService {
|
||||
message: t`Kanban main group by field metadata not found`,
|
||||
userFriendlyMessage: msg`Kanban main group by field metadata not found`,
|
||||
});
|
||||
} else if (mainGroupByFieldMetadata.type !== FieldMetadataType.SELECT) {
|
||||
} else if (
|
||||
!this.isAllowedKanbanMainGroupByField({
|
||||
mainGroupByFieldMetadata,
|
||||
})
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Kanban main group by field must be a SELECT field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select field`,
|
||||
message: t`Kanban main group by field must be a SELECT or a many-to-one relation field`,
|
||||
userFriendlyMessage: msg`Kanban main group by field must be a select or a many-to-one relation field`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user