Compare commits

...
Author SHA1 Message Date
bosiraphael 12bf71215f add hooks to context api 2026-04-22 18:19:30 +02:00
29 changed files with 470 additions and 73 deletions
@@ -1,5 +1,17 @@
import {
type ContextStorePageType,
type ContextStoreViewType,
type RecordGqlOperationFilter,
} from 'twenty-shared/types';
export type FrontComponentExecutionContext = {
frontComponentId: string;
userId: string | null;
recordId: string | null;
pageType: ContextStorePageType | null;
viewType: ContextStoreViewType | null;
viewId: string | null;
objectMetadataItemId: string | null;
numberOfSelectedRecords: number;
graphqlFilter: RecordGqlOperationFilter | null;
};
@@ -35,6 +35,7 @@ export const HeadlessFrontComponentRendererEngineCommand = () => {
frontComponentId={context.frontComponentId}
commandMenuItemId={commandMenuItemId}
recordId={recordId}
contextStoreInstanceId={context.contextStoreInstanceId}
/>
</Suspense>
);
@@ -1,21 +1,11 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { type RecordShowParentView } from 'twenty-shared/types';
type RecordShowParentViewComponentState = {
parentViewComponentId: string;
parentViewObjectNameSingular: string;
parentViewFilterGroups: RecordFilterGroup[];
parentViewFilters: RecordFilter[];
parentViewSorts: RecordSort[];
};
export type { RecordShowParentView } from 'twenty-shared/types';
export const contextStoreRecordShowParentViewComponentState =
createAtomComponentState<
RecordShowParentViewComponentState | undefined | null
>({
createAtomComponentState<RecordShowParentView | undefined | null>({
key: 'contextStoreRecordShowParentViewComponentState',
defaultValue: undefined,
componentInstanceContext: ContextStoreComponentInstanceContext,
@@ -1,19 +1,8 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { type ContextStoreTargetedRecordsRule } from 'twenty-shared/types';
type ContextStoreTargetedRecordsRuleSelectionMode = {
mode: 'selection';
selectedRecordIds: string[];
};
type ContextStoreTargetedRecordsRuleExclusionMode = {
mode: 'exclusion';
excludedRecordIds: string[];
};
export type ContextStoreTargetedRecordsRule =
| ContextStoreTargetedRecordsRuleSelectionMode
| ContextStoreTargetedRecordsRuleExclusionMode;
export type { ContextStoreTargetedRecordsRule } from 'twenty-shared/types';
export const contextStoreTargetedRecordsRuleComponentState =
createAtomComponentState<ContextStoreTargetedRecordsRule>({
@@ -1,4 +1 @@
export enum ContextStoreViewType {
Table = 'table',
Kanban = 'kanban',
}
export { ContextStoreViewType } from 'twenty-shared/types';
@@ -19,12 +19,14 @@ type FrontComponentRendererProps = {
frontComponentId: string;
commandMenuItemId?: string;
recordId?: string;
contextStoreInstanceId?: string;
};
export const FrontComponentRenderer = ({
frontComponentId,
commandMenuItemId,
recordId,
contextStoreInstanceId,
}: FrontComponentRendererProps) => {
const { colorScheme } = useContext(ThemeContext);
const { enqueueErrorSnackBar } = useSnackBar();
@@ -39,6 +41,7 @@ export const FrontComponentRenderer = ({
frontComponentId,
commandMenuItemId,
recordId,
contextStoreInstanceId,
});
const handleError = useCallback(
@@ -1,6 +1,24 @@
import { act, renderHook } from '@testing-library/react';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import {
contextStoreTargetedRecordsRuleComponentState,
type ContextStoreTargetedRecordsRule,
} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { ViewFilterOperand } from 'twenty-shared/types';
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
const mockNavigateApp = jest.fn();
const mockRequestAccessTokenRefresh = jest.fn();
@@ -16,7 +34,40 @@ const mockEnqueueWarningSnackBar = jest.fn();
const mockCloseSidePanelMenu = jest.fn();
const mockSetCommandMenuItemProgress = jest.fn();
const enrichedObjectMetadataItems = getTestEnrichedObjectMetadataItemsMock();
const personObjectMetadataItem = enrichedObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const personNameFieldId = personObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)!.id;
let mockCurrentUser: { id: string } | null = { id: 'user-123' };
let mockCurrentWorkspaceMember:
| { id: string; timeZone: string }
| null = { id: 'workspace-member-123', timeZone: 'Europe/Paris' };
let mockObjectMetadataItems = enrichedObjectMetadataItems;
const CONTEXT_STORE_DEFAULTS = new Map<unknown, unknown>([
[contextStoreCurrentPageTypeComponentState, null],
[contextStoreCurrentViewTypeComponentState, null],
[contextStoreCurrentViewIdComponentState, undefined],
[contextStoreCurrentObjectMetadataItemIdComponentState, undefined],
[
contextStoreTargetedRecordsRuleComponentState,
{ mode: 'selection', selectedRecordIds: [] },
],
[contextStoreNumberOfSelectedRecordsComponentState, 0],
[contextStoreFiltersComponentState, []],
[contextStoreFilterGroupsComponentState, []],
[contextStoreAnyFieldFilterValueComponentState, ''],
]);
const contextStoreOverrides = new Map<unknown, unknown>();
const setContextStoreValue = (state: unknown, value: unknown) => {
contextStoreOverrides.set(state, value);
};
jest.mock('~/hooks/useNavigateApp', () => ({
useNavigateApp: () => mockNavigateApp,
@@ -72,9 +123,30 @@ jest.mock('twenty-ui/display', () => ({
}));
jest.mock('@/ui/utilities/state/jotai/hooks/useAtomStateValue', () => ({
useAtomStateValue: () => mockCurrentUser,
useAtomStateValue: (atom: unknown) => {
if (atom === currentUserState) {
return mockCurrentUser;
}
if (atom === currentWorkspaceMemberState) {
return mockCurrentWorkspaceMember;
}
if (atom === objectMetadataItemsSelector) {
return mockObjectMetadataItems;
}
return undefined;
},
}));
jest.mock(
'@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue',
() => ({
useAtomComponentStateValue: (componentState: unknown) =>
contextStoreOverrides.has(componentState)
? contextStoreOverrides.get(componentState)
: CONTEXT_STORE_DEFAULTS.get(componentState),
}),
);
jest.mock('@/ui/utilities/state/jotai/hooks/useSetAtomState', () => ({
useSetAtomState: () => mockSetSidePanelSearch,
}));
@@ -90,6 +162,12 @@ describe('useFrontComponentExecutionContext', () => {
beforeEach(() => {
jest.clearAllMocks();
mockCurrentUser = { id: 'user-123' };
mockCurrentWorkspaceMember = {
id: 'workspace-member-123',
timeZone: 'Europe/Paris',
};
mockObjectMetadataItems = enrichedObjectMetadataItems;
contextStoreOverrides.clear();
});
describe('executionContext', () => {
@@ -105,6 +183,12 @@ describe('useFrontComponentExecutionContext', () => {
frontComponentId: FRONT_COMPONENT_ID,
userId: 'user-123',
recordId: 'record-456',
pageType: null,
viewType: null,
viewId: null,
objectMetadataItemId: null,
numberOfSelectedRecords: 0,
graphqlFilter: null,
});
});
@@ -131,6 +215,119 @@ describe('useFrontComponentExecutionContext', () => {
});
});
describe('graphqlFilter', () => {
it('should be null when objectMetadataItem cannot be resolved', () => {
setContextStoreValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
'unknown-object-id',
);
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
}),
);
expect(result.current.executionContext.graphqlFilter).toBeNull();
});
it('should return { id: { in: [] } } for empty selection mode', () => {
setContextStoreValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
personObjectMetadataItem.id,
);
const targetedRecordsRule: ContextStoreTargetedRecordsRule = {
mode: 'selection',
selectedRecordIds: [],
};
setContextStoreValue(
contextStoreTargetedRecordsRuleComponentState,
targetedRecordsRule,
);
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
}),
);
expect(result.current.executionContext.graphqlFilter).toEqual({
id: { in: [] },
});
});
it('should compose selection mode with selected record ids', () => {
setContextStoreValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
personObjectMetadataItem.id,
);
const targetedRecordsRule: ContextStoreTargetedRecordsRule = {
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
};
setContextStoreValue(
contextStoreTargetedRecordsRuleComponentState,
targetedRecordsRule,
);
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
}),
);
expect(result.current.executionContext.graphqlFilter).toEqual({
and: [{}, { id: { in: ['1', '2', '3'] } }, {}],
});
});
it('should compose exclusion mode with raw filters', () => {
setContextStoreValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
personObjectMetadataItem.id,
);
const targetedRecordsRule: ContextStoreTargetedRecordsRule = {
mode: 'exclusion',
excludedRecordIds: ['1', '2', '3'],
};
setContextStoreValue(
contextStoreTargetedRecordsRuleComponentState,
targetedRecordsRule,
);
const filters: RecordFilter[] = [
{
id: 'name-filter',
fieldMetadataId: personNameFieldId,
value: 'John',
displayValue: 'John',
displayAvatarUrl: undefined,
operand: ViewFilterOperand.CONTAINS,
type: 'TEXT',
label: 'Name',
},
];
setContextStoreValue(contextStoreFiltersComponentState, filters);
const { result } = renderHook(() =>
useFrontComponentExecutionContext({
frontComponentId: FRONT_COMPONENT_ID,
}),
);
expect(result.current.executionContext.graphqlFilter).toEqual({
and: [
{},
{
or: [
{ name: { firstName: { ilike: '%John%' } } },
{ name: { lastName: { ilike: '%John%' } } },
],
},
{ not: { id: { in: ['1', '2', '3'] } } },
],
});
});
});
describe('navigate', () => {
it('should call navigateApp with the provided arguments', async () => {
const { result } = renderHook(() =>
@@ -6,14 +6,27 @@ import {
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
import { useUnmountCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/commandMenuItemProgressFamilyState';
import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentPageTypeComponentState } from '@/context-store/states/contextStoreCurrentPageTypeComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
@@ -24,15 +37,19 @@ export const useFrontComponentExecutionContext = ({
frontComponentId,
commandMenuItemId,
recordId,
contextStoreInstanceId,
}: {
frontComponentId: string;
commandMenuItemId?: string;
recordId?: string;
contextStoreInstanceId?: string;
}): {
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
} => {
const currentUser = useAtomStateValue(currentUserState);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const navigateApp = useNavigateApp();
const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({
frontComponentId,
@@ -124,10 +141,79 @@ export const useFrontComponentExecutionContext = ({
}
};
const contextStoreCurrentPageType = useAtomComponentStateValue(
contextStoreCurrentPageTypeComponentState,
contextStoreInstanceId,
);
const contextStoreCurrentViewType = useAtomComponentStateValue(
contextStoreCurrentViewTypeComponentState,
contextStoreInstanceId,
);
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
contextStoreInstanceId,
);
const contextStoreCurrentObjectMetadataItemId =
useAtomComponentStateValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
contextStoreInstanceId,
);
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
contextStoreInstanceId,
);
const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue(
contextStoreNumberOfSelectedRecordsComponentState,
contextStoreInstanceId,
);
const contextStoreFilters = useAtomComponentStateValue(
contextStoreFiltersComponentState,
contextStoreInstanceId,
);
const contextStoreFilterGroups = useAtomComponentStateValue(
contextStoreFilterGroupsComponentState,
contextStoreInstanceId,
);
const contextStoreAnyFieldFilterValue = useAtomComponentStateValue(
contextStoreAnyFieldFilterValueComponentState,
contextStoreInstanceId,
);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === contextStoreCurrentObjectMetadataItemId,
);
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const userTimezone =
currentWorkspaceMember?.timeZone !== 'system'
? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
: systemTimeZone;
const graphqlFilter = isDefined(objectMetadataItem)
? (computeContextStoreFilters({
contextStoreTargetedRecordsRule,
contextStoreFilters,
contextStoreFilterGroups,
objectMetadataItem,
filterValueDependencies: {
currentWorkspaceMemberId: currentWorkspaceMember?.id,
timeZone: userTimezone,
},
contextStoreAnyFieldFilterValue,
}) ?? null)
: null;
const executionContext: FrontComponentExecutionContext = {
frontComponentId,
userId: currentUser?.id ?? null,
recordId: recordId ?? null,
pageType: contextStoreCurrentPageType,
viewType: contextStoreCurrentViewType,
viewId: contextStoreCurrentViewId ?? null,
objectMetadataItemId: contextStoreCurrentObjectMetadataItemId ?? null,
numberOfSelectedRecords: contextStoreNumberOfSelectedRecords,
graphqlFilter,
};
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
@@ -1,8 +1 @@
import { type RecordFilterGroupLogicalOperator } from 'twenty-shared/types';
export type RecordFilterGroup = {
id: string;
parentRecordFilterGroupId?: string | null;
logicalOperator: RecordFilterGroupLogicalOperator;
positionInRecordFilterGroup?: number | null;
};
export type { RecordFilterGroup } from 'twenty-shared/types';
@@ -1,32 +1,7 @@
import { type FILTER_OPERANDS_MAP } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { type CompositeFieldSubFieldName } from '@/settings/data-model/types/CompositeFieldSubFieldName';
import {
type FilterableAndTSVectorFieldType,
type ViewFilterOperand,
} from 'twenty-shared/types';
import { type FilterableAndTSVectorFieldType } from 'twenty-shared/types';
// RLS-specific: references a workspace member field for dynamic "Me" comparisons
export type RLSDynamicValue = {
workspaceMemberFieldMetadataId: string;
workspaceMemberSubFieldName?: string | null;
};
export type RecordFilter = {
id: string;
fieldMetadataId: string;
value: string;
/** @deprecated We shouldn't implement new features with this field and instead try to create utils to obtain the displayValue at runtime */
displayValue: string;
type: FilterableAndTSVectorFieldType;
recordFilterGroupId?: string;
displayAvatarUrl?: string;
operand: ViewFilterOperand;
positionInRecordFilterGroup?: number | null;
label: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
// RLS-specific: when set, filter compares against current user's field value
rlsDynamicValue?: RLSDynamicValue | null;
};
export type { RecordFilter, RLSDynamicValue } from 'twenty-shared/types';
export type RecordFilterToRecordInputOperand<
T extends FilterableAndTSVectorFieldType,
@@ -1,7 +1 @@
import { type ViewSortDirection } from '~/generated-metadata/graphql';
export type RecordSort = {
id: string;
fieldMetadataId: string;
direction: ViewSortDirection;
};
export type { RecordSort } from 'twenty-shared/types';
@@ -3,6 +3,7 @@ import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
@@ -49,6 +50,7 @@ export const FrontComponentWidgetRenderer = ({
<FrontComponentRenderer
frontComponentId={frontComponentId}
recordId={targetRecordIdentifier?.id}
contextStoreInstanceId={MAIN_CONTEXT_STORE_INSTANCE_ID}
/>
</Suspense>
</StyledContainer>
@@ -1,5 +1,6 @@
import { Suspense, lazy } from 'react';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { viewableFrontComponentIdComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentIdComponentState';
import { viewableFrontComponentRecordContextComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -29,6 +30,7 @@ export const SidePanelFrontComponentPage = () => {
<FrontComponentRenderer
frontComponentId={viewableFrontComponentId}
recordId={viewableFrontComponentRecordContext?.recordId}
contextStoreInstanceId={MAIN_CONTEXT_STORE_INSTANCE_ID}
/>
</Suspense>
);
@@ -1,6 +1,8 @@
import { styled } from '@linaria/react';
import { Suspense, lazy } from 'react';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
@@ -25,6 +27,7 @@ export const SettingsApplicationCustomTab = ({
<Suspense fallback={null}>
<FrontComponentRenderer
frontComponentId={settingsCustomTabFrontComponentId}
contextStoreInstanceId={MAIN_CONTEXT_STORE_INSTANCE_ID}
/>
</Suspense>
</StyledContainer>
@@ -0,0 +1,12 @@
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectGraphqlFilter = (
context: FrontComponentExecutionContext,
): RecordGqlOperationFilter | null => context.graphqlFilter;
export const useGraphqlFilter = (): RecordGqlOperationFilter | null => {
return useFrontComponentExecutionContext(selectGraphqlFilter);
};
@@ -0,0 +1,10 @@
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectNumberOfSelectedRecords = (
context: FrontComponentExecutionContext,
): number => context.numberOfSelectedRecords;
export const useNumberOfSelectedRecords = (): number => {
return useFrontComponentExecutionContext(selectNumberOfSelectedRecords);
};
@@ -0,0 +1,10 @@
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectObjectMetadataItemId = (
context: FrontComponentExecutionContext,
): string | null => context.objectMetadataItemId;
export const useObjectMetadataItemId = (): string | null => {
return useFrontComponentExecutionContext(selectObjectMetadataItemId);
};
@@ -0,0 +1,12 @@
import { type ContextStorePageType } from 'twenty-shared/types';
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectPageType = (
context: FrontComponentExecutionContext,
): ContextStorePageType | null => context.pageType;
export const usePageType = (): ContextStorePageType | null => {
return useFrontComponentExecutionContext(selectPageType);
};
@@ -0,0 +1,10 @@
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectViewId = (
context: FrontComponentExecutionContext,
): string | null => context.viewId;
export const useViewId = (): string | null => {
return useFrontComponentExecutionContext(selectViewId);
};
@@ -0,0 +1,12 @@
import { type ContextStoreViewType } from 'twenty-shared/types';
import { type FrontComponentExecutionContext } from '../types/FrontComponentExecutionContext';
import { useFrontComponentExecutionContext } from './useFrontComponentExecutionContext';
const selectViewType = (
context: FrontComponentExecutionContext,
): ContextStoreViewType | null => context.viewType;
export const useViewType = (): ContextStoreViewType | null => {
return useFrontComponentExecutionContext(selectViewType);
};
@@ -35,8 +35,14 @@ export { unmountFrontComponent } from './functions/unmountFrontComponent';
export { updateProgress } from './functions/updateProgress';
export { useFrontComponentExecutionContext } from './hooks/useFrontComponentExecutionContext';
export { useFrontComponentId } from './hooks/useFrontComponentId';
export { useGraphqlFilter } from './hooks/useGraphqlFilter';
export { useNumberOfSelectedRecords } from './hooks/useNumberOfSelectedRecords';
export { useObjectMetadataItemId } from './hooks/useObjectMetadataItemId';
export { usePageType } from './hooks/usePageType';
export { useRecordId } from './hooks/useRecordId';
export { useUserId } from './hooks/useUserId';
export { useViewId } from './hooks/useViewId';
export { useViewType } from './hooks/useViewType';
export type { FrontComponentExecutionContext } from './types/FrontComponentExecutionContext';
export { getFrontComponentCommandErrorDedupeKey } from './utils/getFrontComponentCommandErrorDedupeKey';
export type {
@@ -1,5 +1,17 @@
import {
type ContextStorePageType,
type ContextStoreViewType,
type RecordGqlOperationFilter,
} from 'twenty-shared/types';
export type FrontComponentExecutionContext = {
frontComponentId: string;
userId: string | null;
recordId: string | null;
pageType: ContextStorePageType | null;
viewType: ContextStoreViewType | null;
viewId: string | null;
objectMetadataItemId: string | null;
numberOfSelectedRecords: number;
graphqlFilter: RecordGqlOperationFilter | null;
};
@@ -0,0 +1,13 @@
type ContextStoreTargetedRecordsRuleSelectionMode = {
mode: 'selection';
selectedRecordIds: string[];
};
type ContextStoreTargetedRecordsRuleExclusionMode = {
mode: 'exclusion';
excludedRecordIds: string[];
};
export type ContextStoreTargetedRecordsRule =
| ContextStoreTargetedRecordsRuleSelectionMode
| ContextStoreTargetedRecordsRuleExclusionMode;
@@ -0,0 +1,4 @@
export enum ContextStoreViewType {
Table = 'table',
Kanban = 'kanban',
}
@@ -0,0 +1,22 @@
import { type FilterableAndTSVectorFieldType } from './FilterableFieldType';
import { type ViewFilterOperand } from './ViewFilterOperand';
export type RLSDynamicValue = {
workspaceMemberFieldMetadataId: string;
workspaceMemberSubFieldName?: string | null;
};
export type RecordFilter = {
id: string;
fieldMetadataId: string;
value: string;
displayValue: string;
type: FilterableAndTSVectorFieldType;
recordFilterGroupId?: string;
displayAvatarUrl?: string;
operand: ViewFilterOperand;
positionInRecordFilterGroup?: number | null;
label: string;
subFieldName?: string | null | undefined;
rlsDynamicValue?: RLSDynamicValue | null;
};
@@ -0,0 +1,8 @@
import { type RecordFilterGroupLogicalOperator } from './RecordFilterGroupLogicalOperator';
export type RecordFilterGroup = {
id: string;
parentRecordFilterGroupId?: string | null;
logicalOperator: RecordFilterGroupLogicalOperator;
positionInRecordFilterGroup?: number | null;
};
@@ -0,0 +1,11 @@
import { type RecordFilter } from './RecordFilter';
import { type RecordFilterGroup } from './RecordFilterGroup';
import { type RecordSort } from './RecordSort';
export type RecordShowParentView = {
parentViewComponentId: string;
parentViewObjectNameSingular: string;
parentViewFilterGroups: RecordFilterGroup[];
parentViewFilters: RecordFilter[];
parentViewSorts: RecordSort[];
};
@@ -0,0 +1,5 @@
export type RecordSort = {
id: string;
fieldMetadataId: string;
direction: string;
};
@@ -59,6 +59,8 @@ export type { ConfigVariableValue } from './ConfigVariableValue';
export type { ConfirmationModalCaller } from './ConfirmationModalCaller';
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
export { ContextStorePageType } from './ContextStorePageType';
export type { ContextStoreTargetedRecordsRule } from './ContextStoreTargetedRecordsRule';
export { ContextStoreViewType } from './ContextStoreViewType';
export { CoreObjectNameSingular } from './CoreObjectNameSingular';
export { CrudOperationType } from './CrudOperationType';
export type { EmailAttachment } from './EmailAttachment';
@@ -209,6 +211,8 @@ export type { RatioAggregateConfig } from './page-layout/ratio-aggregate-config.
export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem';
export type { PartialFieldMetadataItemOption } from './PartialFieldMetadataOption';
export type { QueryCursorDirection } from './QueryCursorDirection';
export type { RLSDynamicValue, RecordFilter } from './RecordFilter';
export type { RecordFilterGroup } from './RecordFilterGroup';
export { RecordFilterGroupLogicalOperator } from './RecordFilterGroupLogicalOperator';
export type { RecordFilterValueDependencies } from './RecordFilterValueDependencies';
export type { RecordGqlFieldsDeprecated } from './RecordGqlFieldsDeprecated';
@@ -250,6 +254,8 @@ export type { RecordGqlOperationGqlRecordFields } from './RecordGqlOperationGqlR
export type { RecordGqlOperationOrderBy } from './RecordGqlOperationOrderBy';
export type { RecordGqlOperationSignature } from './RecordGqlOperationSignature';
export type { RecordGqlOperationVariables } from './RecordGqlOperationVariables';
export type { RecordShowParentView } from './RecordShowParentView';
export type { RecordSort } from './RecordSort';
export type { RelationAndMorphRelationFieldMetadataType } from './RelationAndMorphRelationFieldMetadataType';
export type { RelationCreationPayload } from './RelationCreationPayload';
export { RelationOnDeleteAction } from './RelationOnDeleteAction.type';