Baptiste Devessier
2026-04-09 21:33:15 +02:00
committed by GitHub
parent 8a10071253
commit e3077691d1
40 changed files with 450 additions and 23 deletions
@@ -896,6 +896,7 @@ type PageLayoutWidget {
position: PageLayoutWidgetPosition
configuration: WidgetConfiguration!
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
createdAt: DateTime!
updatedAt: DateTime!
deletedAt: DateTime
@@ -4012,6 +4013,7 @@ input UpdatePageLayoutWidgetWithIdInput {
position: JSON
configuration: JSON
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
}
input GridPositionInput {
@@ -4040,6 +4042,7 @@ input UpdatePageLayoutWidgetInput {
position: JSON
configuration: JSON
conditionalDisplay: JSON
conditionalAvailabilityExpression: String
}
input CreateLogicFunctionFromSourceInput {
@@ -671,6 +671,7 @@ export interface PageLayoutWidget {
position?: PageLayoutWidgetPosition
configuration: WidgetConfiguration
conditionalDisplay?: Scalars['JSON']
conditionalAvailabilityExpression?: Scalars['String']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
deletedAt?: Scalars['DateTime']
@@ -3811,6 +3812,7 @@ export interface PageLayoutWidgetGenqlSelection{
position?: PageLayoutWidgetPositionGenqlSelection
configuration?: WidgetConfigurationGenqlSelection
conditionalDisplay?: boolean | number
conditionalAvailabilityExpression?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
deletedAt?: boolean | number
@@ -6553,13 +6555,13 @@ export interface UpdatePageLayoutWithTabsInput {name: Scalars['String'],type: Pa
export interface UpdatePageLayoutTabWithWidgetsInput {id: Scalars['UUID'],title: Scalars['String'],position: Scalars['Float'],icon?: (Scalars['String'] | null),layoutMode?: (PageLayoutTabLayoutMode | null),widgets: UpdatePageLayoutWidgetWithIdInput[]}
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface UpdatePageLayoutWidgetWithIdInput {id: Scalars['UUID'],pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float'],rowSpan: Scalars['Float'],columnSpan: Scalars['Float']}
export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']}
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null)}
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
@@ -1893,6 +1893,9 @@ export default {
"conditionalDisplay": [
15
],
"conditionalAvailabilityExpression": [
1
],
"createdAt": [
4
],
@@ -10194,6 +10197,9 @@ export default {
"conditionalDisplay": [
15
],
"conditionalAvailabilityExpression": [
1
],
"__typename": [
1
]
@@ -10266,6 +10272,9 @@ export default {
"conditionalDisplay": [
15
],
"conditionalAvailabilityExpression": [
1
],
"__typename": [
1
]
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
updatedAt
deletedAt
conditionalDisplay
conditionalAvailabilityExpression
gridPosition {
column
columnSpan
@@ -12,4 +12,5 @@ export type PageLayoutWidget = Omit<
> & {
objectMetadataId?: Nullable<string>;
configuration: WidgetConfiguration | FieldsConfiguration | FieldConfiguration;
conditionalAvailabilityExpression?: Nullable<string>;
};
@@ -5,6 +5,7 @@ import { evaluateWidgetVisibility } from '@/page-layout/utils/evaluateWidgetVisi
describe('evaluateWidgetVisibility', () => {
it('should return true (visible) when no conditionalDisplay is provided', () => {
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay: undefined,
context: {
device: 'DESKTOP',
@@ -24,6 +25,7 @@ describe('evaluateWidgetVisibility', () => {
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'MOBILE',
@@ -43,6 +45,7 @@ describe('evaluateWidgetVisibility', () => {
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'DESKTOP',
@@ -62,6 +65,7 @@ describe('evaluateWidgetVisibility', () => {
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'DESKTOP',
@@ -81,6 +85,7 @@ describe('evaluateWidgetVisibility', () => {
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'MOBILE',
@@ -103,6 +108,7 @@ describe('evaluateWidgetVisibility', () => {
};
const resultMobile = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'MOBILE',
@@ -110,6 +116,7 @@ describe('evaluateWidgetVisibility', () => {
});
const resultDesktop = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'DESKTOP',
@@ -127,6 +134,7 @@ describe('evaluateWidgetVisibility', () => {
expect(() => {
evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay: invalidConditionalDisplay,
context: {
device: 'DESKTOP',
@@ -134,4 +142,106 @@ describe('evaluateWidgetVisibility', () => {
});
}).toThrow();
});
describe('conditionalAvailabilityExpression', () => {
it('should return true when expression matches MOBILE device', () => {
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: 'device == "MOBILE"',
conditionalDisplay: undefined,
context: {
device: 'MOBILE',
},
});
expect(result).toBe(true);
});
it('should return false when expression does not match MOBILE device', () => {
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: 'device == "MOBILE"',
conditionalDisplay: undefined,
context: {
device: 'DESKTOP',
},
});
expect(result).toBe(false);
});
it('should return true when expression matches DESKTOP device', () => {
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: 'device == "DESKTOP"',
conditionalDisplay: undefined,
context: {
device: 'DESKTOP',
},
});
expect(result).toBe(true);
});
it('should take priority over conditionalDisplay when both are set', () => {
const conditionalDisplay: RulesLogic = {
and: [
{
'===': [{ var: 'device' }, 'DESKTOP'],
},
],
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: 'device == "MOBILE"',
conditionalDisplay,
context: {
device: 'DESKTOP',
},
});
// Expression says MOBILE only, so DESKTOP should be hidden
// even though conditionalDisplay says DESKTOP is visible
expect(result).toBe(false);
});
it('should fall through to conditionalDisplay when expression is null', () => {
const conditionalDisplay: RulesLogic = {
and: [
{
'===': [{ var: 'device' }, 'MOBILE'],
},
],
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: null,
conditionalDisplay,
context: {
device: 'DESKTOP',
},
});
// Expression is null, so conditionalDisplay takes over
// conditionalDisplay says MOBILE only → DESKTOP hidden
expect(result).toBe(false);
});
it('should fall through to conditionalDisplay when expression is undefined', () => {
const conditionalDisplay: RulesLogic = {
and: [
{
'===': [{ var: 'device' }, 'DESKTOP'],
},
],
};
const result = evaluateWidgetVisibility({
conditionalAvailabilityExpression: undefined,
conditionalDisplay,
context: {
device: 'DESKTOP',
},
});
expect(result).toBe(true);
});
});
});
@@ -76,6 +76,8 @@ export const convertPageLayoutDraftToUpdateInput = (
tab.layoutMode ?? PageLayoutTabLayoutMode.GRID,
),
configuration: widget.configuration ?? null,
conditionalAvailabilityExpression:
widget.conditionalAvailabilityExpression ?? null,
})),
};
}),
@@ -1,17 +1,29 @@
import { isDefined } from 'twenty-shared/utils';
import jsonLogic, { type RulesLogic } from 'json-logic-js';
import {
evaluateConditionalAvailabilityExpression,
isDefined,
} from 'twenty-shared/utils';
import { type WidgetVisibilityContext } from '@/page-layout/types/WidgetVisibilityContext';
type EvaluateWidgetVisibilityParams = {
conditionalAvailabilityExpression: string | null | undefined;
conditionalDisplay: RulesLogic | undefined;
context: WidgetVisibilityContext;
};
export const evaluateWidgetVisibility = ({
conditionalAvailabilityExpression,
conditionalDisplay,
context,
}: EvaluateWidgetVisibilityParams): boolean => {
if (isDefined(conditionalAvailabilityExpression)) {
return evaluateConditionalAvailabilityExpression(
conditionalAvailabilityExpression,
context,
);
}
if (!isDefined(conditionalDisplay)) {
return true;
}
@@ -13,6 +13,8 @@ export const filterVisibleWidgets = ({
}: FilterVisibleWidgetsParams): PageLayoutTab['widgets'] => {
return widgets.filter((widget) => {
return evaluateWidgetVisibility({
conditionalAvailabilityExpression:
widget.conditionalAvailabilityExpression,
conditionalDisplay: widget.conditionalDisplay,
context,
});
@@ -1,10 +1,15 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useResetPageLayoutWidgetToDefault } from '@/page-layout/hooks/useResetPageLayoutWidgetToDefault';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { WidgetVisibilityDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/WidgetVisibilityDropdownContent';
import { WIDGET_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/WidgetSettingsSelectableItemIds';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { getVisibilityLabel } from '@/side-panel/pages/page-layout/utils/getVisibilityLabel';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -13,6 +18,7 @@ import { useLingui } from '@lingui/react/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconEyeX,
IconRefreshDot,
IconSwitchHorizontal,
IconTrash,
@@ -29,6 +35,8 @@ export const WidgetSettingsManageSection = ({
}: WidgetSettingsManageSectionProps) => {
const { t } = useLingui();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const pageLayoutEditingWidgetId = useAtomComponentStateValue(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
@@ -47,6 +55,15 @@ export const WidgetSettingsManageSection = ({
return null;
}
const visibilityLabel = getVisibilityLabel(
widgetInEditMode?.conditionalAvailabilityExpression,
{
anyDevice: t`Any device`,
mobile: t`Mobile`,
desktop: t`Desktop`,
},
);
const handleResetToDefault = () => {
openModal(RESET_WIDGET_TO_DEFAULT_MODAL_ID);
};
@@ -68,6 +85,26 @@ export const WidgetSettingsManageSection = ({
return (
<>
<SidePanelGroup heading={t`Manage`}>
<SelectableListItem
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION}
>
<CommandMenuItemDropdown
id={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION}
label={t`Visibility Restriction`}
Icon={IconEyeX}
dropdownId={
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION
}
dropdownComponents={
<DropdownContent>
<WidgetVisibilityDropdownContent />
</DropdownContent>
}
dropdownPlacement="bottom-end"
description={visibilityLabel}
contextualTextPosition="right"
/>
</SelectableListItem>
<SelectableListItem
itemId={WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
onEnter={handleResetToDefault}
@@ -0,0 +1,122 @@
import { EXPRESSION_DEVICE_DESKTOP } from '@/side-panel/pages/page-layout/constants/ExpressionDeviceDesktop';
import { EXPRESSION_DEVICE_MOBILE } from '@/side-panel/pages/page-layout/constants/ExpressionDeviceMobile';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLingui } from '@lingui/react/macro';
import { MenuItemSelect } from 'twenty-ui/navigation';
const VISIBILITY_ANY_DEVICE = 'any-device';
const VISIBILITY_MOBILE = 'mobile';
const VISIBILITY_DESKTOP = 'desktop';
const VISIBILITY_OPTIONS = [
{ id: VISIBILITY_ANY_DEVICE },
{ id: VISIBILITY_MOBILE },
{ id: VISIBILITY_DESKTOP },
] as const;
const expressionToOptionId = (
expression: string | null | undefined,
): string => {
if (!expression) {
return VISIBILITY_ANY_DEVICE;
}
if (expression === EXPRESSION_DEVICE_MOBILE) {
return VISIBILITY_MOBILE;
}
if (expression === EXPRESSION_DEVICE_DESKTOP) {
return VISIBILITY_DESKTOP;
}
return VISIBILITY_ANY_DEVICE;
};
const optionIdToExpression = (optionId: string): string | null => {
switch (optionId) {
case VISIBILITY_MOBILE:
return EXPRESSION_DEVICE_MOBILE;
case VISIBILITY_DESKTOP:
return EXPRESSION_DEVICE_DESKTOP;
default:
return null;
}
};
export const WidgetVisibilityDropdownContent = () => {
const { t } = useLingui();
const { pageLayoutId } = usePageLayoutIdFromContextStore();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const currentOptionId = expressionToOptionId(
widgetInEditMode?.conditionalAvailabilityExpression,
);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
dropdownId,
);
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const handleSelectVisibility = (optionId: string) => {
updateCurrentWidgetConfig({
conditionalAvailabilityExpression: optionIdToExpression(optionId),
});
closeDropdown();
};
const visibilityLabels: Record<string, string> = {
[VISIBILITY_ANY_DEVICE]: t`Any device`,
[VISIBILITY_MOBILE]: t`Mobile`,
[VISIBILITY_DESKTOP]: t`Desktop`,
};
return (
<DropdownMenuItemsContainer>
<SelectableList
selectableListInstanceId={dropdownId}
focusId={dropdownId}
selectableItemIdArray={VISIBILITY_OPTIONS.map((option) => option.id)}
>
{VISIBILITY_OPTIONS.map((option) => (
<SelectableListItem
key={option.id}
itemId={option.id}
onEnter={() => {
handleSelectVisibility(option.id);
}}
>
<MenuItemSelect
text={visibilityLabels[option.id]}
selected={currentOptionId === option.id}
focused={selectedItemId === option.id}
onClick={() => {
handleSelectVisibility(option.id);
}}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
);
};
@@ -72,6 +72,7 @@ export const SidePanelRecordPageFieldSettings = () => {
const selectableItemIds = [
'field',
'layout',
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET,
@@ -86,6 +86,7 @@ export const SidePanelRecordPageFieldsSettings = () => {
'new-field-default-visibility',
'display-more-fields-button',
'action-button',
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.REPLACE_WIDGET,
WIDGET_SETTINGS_SELECTABLE_ITEM_IDS.DELETE_WIDGET,
@@ -0,0 +1 @@
export const EXPRESSION_DEVICE_DESKTOP = 'device == "DESKTOP"';
@@ -0,0 +1 @@
export const EXPRESSION_DEVICE_MOBILE = 'device == "MOBILE"';
@@ -1,4 +1,5 @@
export const WIDGET_SETTINGS_SELECTABLE_ITEM_IDS = {
VISIBILITY_RESTRICTION: 'visibility-restriction',
MOVE_DOWN: 'widget-move-down',
MOVE_UP: 'widget-move-up',
MOVE_TO_TAB: 'widget-move-to-tab',
@@ -23,9 +23,11 @@ export const useUpdateCurrentWidgetConfig = (pageLayoutIdFromProps: string) => {
({
objectMetadataId,
configToUpdate,
conditionalAvailabilityExpression,
}: {
objectMetadataId?: string | null;
configToUpdate?: Partial<PageLayoutWidget['configuration']>;
conditionalAvailabilityExpression?: string | null;
}) => {
const prev = store.get(pageLayoutDraft);
store.set(pageLayoutDraft, {
@@ -41,6 +43,9 @@ export const useUpdateCurrentWidgetConfig = (pageLayoutIdFromProps: string) => {
...(widget.configuration ?? {}),
...configToUpdate,
} as PageLayoutWidget['configuration'],
...(conditionalAvailabilityExpression !== undefined
? { conditionalAvailabilityExpression }
: {}),
}
: widget,
),
@@ -0,0 +1,23 @@
import { isDefined } from 'twenty-shared/utils';
import { EXPRESSION_DEVICE_DESKTOP } from '@/side-panel/pages/page-layout/constants/ExpressionDeviceDesktop';
import { EXPRESSION_DEVICE_MOBILE } from '@/side-panel/pages/page-layout/constants/ExpressionDeviceMobile';
export const getVisibilityLabel = (
expression: string | null | undefined,
labels: { anyDevice: string; mobile: string; desktop: string },
): string => {
if (!isDefined(expression)) {
return labels.anyDevice;
}
if (expression === EXPRESSION_DEVICE_MOBILE) {
return labels.mobile;
}
if (expression === EXPRESSION_DEVICE_DESKTOP) {
return labels.desktop;
}
return labels.anyDevice;
};
@@ -778,6 +778,7 @@ export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveO
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
overrides: null,
universalOverrides: null,
};
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddConditionalAvailabilityExpressionToPageLayoutWidget1775654781000
implements MigrationInterface
{
name = 'AddConditionalAvailabilityExpressionToPageLayoutWidget1775654781000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."pageLayoutWidget" ADD "conditionalAvailabilityExpression" varchar`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."pageLayoutWidget" DROP COLUMN "conditionalAvailabilityExpression"`,
);
}
}
@@ -36,6 +36,7 @@ export const fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget = ({
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalAvailabilityExpression: null,
universalOverrides: null,
};
};
@@ -194,6 +194,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"universalConfiguration",
"deletedAt",
"conditionalDisplay",
"conditionalAvailabilityExpression",
"isActive",
"universalOverrides",
],
@@ -929,6 +929,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
isOverridable: true,
},
conditionalAvailabilityExpression: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
isOverridable: true,
},
isActive: {
toCompare: true,
toStringify: false,
@@ -8,5 +8,6 @@ export const FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES = [
'position',
'configuration',
'conditionalDisplay',
'conditionalAvailabilityExpression',
'pageLayoutTabId',
] as const satisfies MetadataEntityPropertyName<'pageLayoutWidget'>[];
@@ -74,6 +74,7 @@ export const fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate = ({
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
overrides: null,
universalOverrides: null,
universalConfiguration:
@@ -68,4 +68,9 @@ export class UpdatePageLayoutWidgetWithIdInput {
@IsObject()
@IsOptional()
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
conditionalAvailabilityExpression?: string | null;
}
@@ -62,4 +62,9 @@ export class UpdatePageLayoutWidgetInput {
@IsObject()
@IsOptional()
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
conditionalAvailabilityExpression?: string | null;
}
@@ -13,9 +13,9 @@ import {
} from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutWidgetPositionUnion } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget-position.union';
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
@@ -65,6 +65,9 @@ export class PageLayoutWidgetDTO {
@Field(() => GraphQLJSON, { nullable: true })
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
@Field(() => String, { nullable: true })
conditionalAvailabilityExpression?: string | null;
@Field()
createdAt: Date;
@@ -31,6 +31,7 @@ export type PageLayoutWidgetOverrides = {
title?: string;
position?: PageLayoutWidgetPosition | null;
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
conditionalAvailabilityExpression?: string | null;
pageLayoutTabId?: SerializedRelation;
};
@@ -85,6 +86,9 @@ export class PageLayoutWidgetEntity<
@Column({ type: 'jsonb', nullable: true })
conditionalDisplay: JsonbProperty<PageLayoutWidgetConditionalDisplay | null>;
@Column({ type: 'varchar', nullable: true })
conditionalAvailabilityExpression: string | null;
@Column({ type: 'jsonb', nullable: false })
gridPosition: JsonbProperty<GridPosition>;
@@ -598,7 +598,9 @@ export class PageLayoutUpdateService {
applicationId: workspaceCustomApplicationId,
applicationUniversalIdentifier:
workspaceCustomApplicationUniversalIdentifier,
conditionalDisplay: null,
conditionalDisplay: widgetInput.conditionalDisplay ?? null,
conditionalAvailabilityExpression:
widgetInput.conditionalAvailabilityExpression ?? null,
overrides: null,
universalOverrides: null,
isActive: true,
@@ -737,6 +739,11 @@ export class PageLayoutUpdateService {
widgetInput.conditionalDisplay ?? null;
}
if (widgetInput.conditionalAvailabilityExpression !== undefined) {
editableProperties.conditionalAvailabilityExpression =
widgetInput.conditionalAvailabilityExpression ?? null;
}
const { overrides, updatedEditableProperties } =
sanitizeOverridableEntityInput({
metadataName: 'pageLayoutWidget',
@@ -17,6 +17,12 @@ export const CONDITIONAL_DISPLAY_DEVICE_DESKTOP = {
and: [{ '===': [{ var: 'device' }, 'DESKTOP'] }],
} as const satisfies PageLayoutWidgetConditionalDisplay;
export const CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_MOBILE =
'device == "MOBILE"';
export const CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_DESKTOP =
'device == "DESKTOP"';
export const GRID_POSITIONS = {
FULL_WIDTH: {
row: 0,
@@ -2,6 +2,8 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_DESKTOP,
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_MOBILE,
CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
CONDITIONAL_DISPLAY_DEVICE_MOBILE,
TAB_PROPS,
@@ -28,6 +30,8 @@ const NOTE_PAGE_TABS = {
gridPosition: WIDGET_PROPS.noteRichText.gridPosition,
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_MOBILE,
conditionalAvailabilityExpression:
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_MOBILE,
},
},
},
@@ -39,6 +43,8 @@ const NOTE_PAGE_TABS = {
universalIdentifier: '20202020-ac04-4004-8004-a0be5a11a421',
...WIDGET_PROPS.noteRichText,
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
conditionalAvailabilityExpression:
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_DESKTOP,
},
},
},
@@ -15,6 +15,7 @@ export type StandardPageLayoutWidgetConfig = {
gridPosition?: GridPosition;
position?: PageLayoutWidgetPosition;
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
conditionalAvailabilityExpression?: string | null;
fieldUniversalIdentifier?: string;
};
@@ -43,6 +44,7 @@ export type StandardRecordPageWidgetConfig = {
gridPosition: GridPosition;
position?: PageLayoutWidgetPosition;
conditionalDisplay?: PageLayoutWidgetConditionalDisplay | null;
conditionalAvailabilityExpression?: string | null;
fieldUniversalIdentifier?: string;
};
@@ -2,6 +2,8 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_DESKTOP,
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_MOBILE,
CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
CONDITIONAL_DISPLAY_DEVICE_MOBILE,
TAB_PROPS,
@@ -28,6 +30,8 @@ const TASK_PAGE_TABS = {
gridPosition: WIDGET_PROPS.taskRichText.gridPosition,
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_MOBILE,
conditionalAvailabilityExpression:
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_MOBILE,
},
},
},
@@ -39,6 +43,8 @@ const TASK_PAGE_TABS = {
universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5521',
...WIDGET_PROPS.taskRichText,
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
conditionalAvailabilityExpression:
CONDITIONAL_AVAILABILITY_EXPRESSION_DEVICE_DESKTOP,
},
},
},
@@ -321,6 +321,8 @@ const computeRecordPageWidgets = ({
universalConfiguration,
objectMetadataId,
conditionalDisplay: widget.conditionalDisplay ?? null,
conditionalAvailabilityExpression:
widget.conditionalAvailabilityExpression ?? null,
},
}),
);
@@ -242,6 +242,7 @@ const createWelcomeRichText = ({
universalConfiguration: configuration,
objectMetadataId: null,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -306,6 +307,7 @@ const createDealsByCompany = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -383,6 +385,7 @@ const createPipelineValueByStage = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -449,6 +452,7 @@ const createRevenueTimeline = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -530,6 +534,7 @@ const createOpportunitiesByOwner = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -565,6 +570,7 @@ const createStockMarketIframe = ({
universalConfiguration: configuration,
objectMetadataId: null,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -633,6 +639,7 @@ const createDealsCreatedThisMonth = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -701,6 +708,7 @@ const createDealValueCreatedThisMonth = ({
},
objectMetadataId: opportunityObjectId,
conditionalDisplay: null,
conditionalAvailabilityExpression: null,
},
});
};
@@ -30,6 +30,7 @@ export type CreateStandardPageLayoutWidgetContext = {
universalConfiguration: MetadataUniversalFlatEntity<'pageLayoutWidget'>['universalConfiguration'];
objectMetadataId: string | null;
conditionalDisplay: PageLayoutWidgetConditionalDisplay | null;
conditionalAvailabilityExpression: string | null;
};
export type CreateStandardPageLayoutWidgetArgs = {
@@ -54,6 +55,7 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({
universalConfiguration,
objectMetadataId,
conditionalDisplay,
conditionalAvailabilityExpression,
},
workspaceId,
twentyStandardApplicationId,
@@ -109,6 +111,8 @@ export const createStandardPageLayoutWidgetFlatMetadata = ({
updatedAt: now,
deletedAt: null,
conditionalDisplay: conditionalDisplay ?? null,
conditionalAvailabilityExpression:
conditionalAvailabilityExpression ?? null,
overrides: null,
universalOverrides: null,
};
@@ -171,6 +171,7 @@ export {
IconEye,
IconEyeOff,
IconEyeShare,
IconEyeX,
IconFile,
IconFileCheck,
IconFileExport,
+1
View File
@@ -249,6 +249,7 @@ export {
IconEye,
IconEyeOff,
IconEyeShare,
IconEyeX,
IconFile,
IconFileCheck,
IconFileExport,