Refactor command menu items deprecated code (#19508)

- Removes the intermediate `CommandMenuItemConfig` /
`CommandConfigContext` / `CommandMenuItemDisplay` abstraction layers,
replacing them with a single `CommandMenuItemRenderer` that renders
directly from the command menu items from the backend
- Eliminates the server-items/ subdirectory by moving its contents
(hooks/, contexts/, states/, display/, edit/) up into the parent
command-menu-item/ module, removing an unnecessary nesting level.
This commit is contained in:
Raphaël Bosi
2026-04-10 11:28:27 +02:00
committed by GitHub
parent f13e7e01fe
commit 4b3a46d953
132 changed files with 1376 additions and 2723 deletions
@@ -0,0 +1,39 @@
import { computeInsertPositionFromBounds } from '@/command-menu-item/edit/utils/computeInsertPositionFromBounds';
describe('computeInsertPositionFromBounds', () => {
it('returns midpoint when both bounds are defined', () => {
expect(computeInsertPositionFromBounds(2, 4)).toBe(3);
});
it('returns midpoint for non-integer result', () => {
expect(computeInsertPositionFromBounds(1, 2)).toBe(1.5);
});
it('returns previous - 1 when only next is undefined', () => {
expect(computeInsertPositionFromBounds(5, undefined)).toBe(6);
});
it('returns next - 1 when only previous is undefined', () => {
expect(computeInsertPositionFromBounds(undefined, 3)).toBe(2);
});
it('returns 0 when both bounds are undefined', () => {
expect(computeInsertPositionFromBounds(undefined, undefined)).toBe(0);
});
it('returns previous - 1 when bounds are equal', () => {
expect(computeInsertPositionFromBounds(5, 5)).toBe(4);
});
it('handles negative positions', () => {
expect(computeInsertPositionFromBounds(-4, -2)).toBe(-3);
});
it('handles zero as previous position', () => {
expect(computeInsertPositionFromBounds(0, 2)).toBe(1);
});
it('handles zero as next position', () => {
expect(computeInsertPositionFromBounds(-2, 0)).toBe(-1);
});
});
@@ -0,0 +1,105 @@
import { getPositionBoundsAtInsertionPoint } from '@/command-menu-item/edit/utils/getPositionBoundsAtInsertionPoint';
const makeItems = (positions: number[]) =>
positions.map((position, index) => ({
id: `item-${index}`,
position,
}));
describe('getPositionBoundsAtInsertionPoint', () => {
const items = makeItems([10, 20, 30, 40, 50]);
describe('insert before', () => {
it('returns previous=undefined and next=10 when inserting before the first item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-0', 'before', items),
).toEqual({
previousPosition: undefined,
nextPosition: 10,
});
});
it('returns previous=10 and next=20 when inserting before the second item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-1', 'before', items),
).toEqual({
previousPosition: 10,
nextPosition: 20,
});
});
it('returns previous=40 and next=50 when inserting before the last item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-4', 'before', items),
).toEqual({
previousPosition: 40,
nextPosition: 50,
});
});
});
describe('insert after', () => {
it('returns previous=10 and next=20 when inserting after the first item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-0', 'after', items),
).toEqual({
previousPosition: 10,
nextPosition: 20,
});
});
it('returns previous=50 and next=undefined when inserting after the last item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-4', 'after', items),
).toEqual({
previousPosition: 50,
nextPosition: undefined,
});
});
it('returns previous=30 and next=40 when inserting after the middle item', () => {
expect(
getPositionBoundsAtInsertionPoint('item-2', 'after', items),
).toEqual({
previousPosition: 30,
nextPosition: 40,
});
});
});
describe('edge cases', () => {
it('returns undefined when anchor item is not found', () => {
expect(
getPositionBoundsAtInsertionPoint('nonexistent', 'before', items),
).toBeUndefined();
});
it('handles single-item list inserting before', () => {
const singleItem = makeItems([5]);
expect(
getPositionBoundsAtInsertionPoint('item-0', 'before', singleItem),
).toEqual({
previousPosition: undefined,
nextPosition: 5,
});
});
it('handles single-item list inserting after', () => {
const singleItem = makeItems([5]);
expect(
getPositionBoundsAtInsertionPoint('item-0', 'after', singleItem),
).toEqual({
previousPosition: 5,
nextPosition: undefined,
});
});
it('returns undefined for empty list', () => {
expect(
getPositionBoundsAtInsertionPoint('item-0', 'before', []),
).toBeUndefined();
});
});
});
@@ -0,0 +1,24 @@
import { isDefined } from 'twenty-shared/utils';
export const computeInsertPositionFromBounds = (
previousPosition: number | undefined,
nextPosition: number | undefined,
): number => {
if (!isDefined(previousPosition) && isDefined(nextPosition)) {
return nextPosition - 1;
}
if (isDefined(previousPosition) && !isDefined(nextPosition)) {
return previousPosition + 1;
}
if (isDefined(previousPosition) && isDefined(nextPosition)) {
if (previousPosition === nextPosition) {
return previousPosition - 1;
}
return (previousPosition + nextPosition) / 2;
}
return 0;
};
@@ -0,0 +1,23 @@
type PositionedItem = { id: string; position: number };
export const getPositionBoundsAtInsertionPoint = (
anchorItemId: string,
insertionSide: 'before' | 'after',
sectionItems: PositionedItem[],
) => {
const anchorIndex = sectionItems.findIndex(
(item) => item.id === anchorItemId,
);
if (anchorIndex === -1) {
return undefined;
}
const insertIndex =
insertionSide === 'before' ? anchorIndex : anchorIndex + 1;
return {
previousPosition: sectionItems[insertIndex - 1]?.position,
nextPosition: sectionItems[insertIndex]?.position,
};
};
@@ -0,0 +1,42 @@
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import type { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
export const getRecordIndexId = (
store: ReturnType<typeof useStore>,
): string | null => {
const objectMetadataItemId = store.get(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
);
const viewId = store.get(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
);
if (!isDefined(objectMetadataItemId) || !isDefined(viewId)) {
return null;
}
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
return getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
viewId,
);
};