Implement full tab widget frontend (#19568)

https://github.com/user-attachments/assets/488e0145-ff49-4205-8fd3-0eb4f614c054
This commit is contained in:
Baptiste Devessier
2026-04-13 15:24:20 +02:00
committed by GitHub
parent 8425afd930
commit 6829fc315a
22 changed files with 601 additions and 192 deletions
@@ -95,7 +95,7 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
title: 'Note',
icon: 'IconNotes',
position: 150,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
pageLayoutId: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -92,7 +92,7 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
title: 'Note',
icon: 'IconNotes',
position: 150,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
pageLayoutId: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -161,6 +161,8 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
variant === 'side-column' &&
!isRecordPageGlobalEditionEnabled;
const isCanvasVariant = variant === 'canvas';
const widgetCard = (
<WidgetCard
headerLess={!showHeader}
@@ -171,8 +173,8 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
isDragging={isDragging}
isResizing={isResizing}
isLastWidget={isLastWidget}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseEnter={isCanvasVariant ? undefined : handleMouseEnter}
onMouseLeave={isCanvasVariant ? undefined : handleMouseLeave}
data-widget-id={widget.id}
data-testid={widget.id}
className="widget"
@@ -106,6 +106,9 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
&:hover {
// border-color shorthand must precede border-bottom-color longhand for correct CSS cascade
border-color: ${(props) => {
if (props.variant === 'canvas') {
return computeBorderColor(props);
}
if (
props.isEditable &&
!props.isDragging &&
@@ -118,6 +121,10 @@ const StyledWidgetCard = styled.div<WidgetCardStyledProps>`
}};
border-bottom-color: ${(props) => {
if (props.variant === 'canvas') {
return computeBorderColor(props);
}
const { variant, isEditable } = props;
if (variant === 'side-column' && !isEditable) {
@@ -16,6 +16,7 @@ import {
type IconComponent,
} from 'twenty-ui/display';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { PageLayoutTabLayoutMode } from '~/generated-metadata/graphql';
type PageLayoutHeaderInfo = {
headerIcon: IconComponent | undefined;
@@ -65,10 +66,12 @@ export const usePageLayoutHeaderInfo = ({
? tab.title
: '';
const isCanvasTab = tab.layoutMode === PageLayoutTabLayoutMode.CANVAS;
return {
headerIcon: IconAppWindow,
headerIconColor: iconColor,
headerType: t`Tab`,
headerType: isCanvasTab ? t`Full tab widget` : t`Tab`,
title,
isReadonly: false,
tab,
@@ -1,17 +1,23 @@
import { useCallback } from 'react';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useIsDashboardPageLayout } from '@/side-panel/pages/page-layout/hooks/useIsDashboardPageLayout';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { t } from '@lingui/core/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { WidgetType } from '~/generated-metadata/graphql';
import {
PageLayoutTabLayoutMode,
WidgetType,
} from '~/generated-metadata/graphql';
export const useOpenWidgetSettingsInSidePanel = (
pageLayoutIdFromProps?: string,
@@ -32,6 +38,16 @@ export const useOpenWidgetSettingsInSidePanel = (
const { closeSidePanelMenu } = useSidePanelMenu();
const setSidePanelPage = useSetAtomState(sidePanelPageState);
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const setPageLayoutTabSettingsOpenTabId = useSetAtomComponentState(
pageLayoutTabSettingsOpenTabIdComponentState,
pageLayoutId,
);
const openWidgetSettingsInSidePanel = useCallback(
({
widgetId,
@@ -112,12 +128,27 @@ export const useOpenWidgetSettingsInSidePanel = (
return;
}
const containingTab = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
);
if (containingTab?.layoutMode === PageLayoutTabLayoutMode.CANVAS) {
setPageLayoutTabSettingsOpenTabId(containingTab.id);
navigatePageLayoutSidePanel({
sidePanelPage: SidePanelPages.PageLayoutTabSettings,
resetNavigationStack: true,
});
return;
}
setSidePanelPage(SidePanelPages.CommandMenuDisplay);
closeSidePanelMenu();
},
[
isDashboardPageLayout,
pageLayoutDraft,
setPageLayoutEditingWidgetId,
setPageLayoutTabSettingsOpenTabId,
navigatePageLayoutSidePanel,
closeSidePanelMenu,
setSidePanelPage,
@@ -0,0 +1,190 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuItemDropdown';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { CanvasTabWidgetVisibilityDropdownContent } from '@/side-panel/pages/page-layout/components/dropdown-content/CanvasTabWidgetVisibilityDropdownContent';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
import { useTranslatedVisibilityLabel } from '@/side-panel/pages/page-layout/hooks/useTranslatedVisibilityLabel';
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';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronLeft,
IconChevronRight,
IconEyeX,
IconPinned,
IconRefreshDot,
IconTrash,
} from 'twenty-ui/display';
const RESET_TAB_TO_DEFAULT_MODAL_ID = 'reset-canvas-tab-to-default-modal';
type CanvasTabSettingsContentProps = {
pageLayoutId: string;
canvasWidget: PageLayoutWidget | undefined;
canSetAsPinned: boolean;
canMoveLeft: boolean;
canMoveRight: boolean;
canShowResetToDefault: boolean;
canDelete: boolean;
onMoveLeft: () => void;
onMoveRight: () => void;
onSetAsPinned: () => void;
onResetToDefault: () => void;
onDelete: () => void;
};
export const CanvasTabSettingsContent = ({
pageLayoutId,
canvasWidget,
canSetAsPinned,
canMoveLeft,
canMoveRight,
canShowResetToDefault,
canDelete,
onMoveLeft,
onMoveRight,
onSetAsPinned,
onResetToDefault,
onDelete,
}: CanvasTabSettingsContentProps) => {
const { t } = useLingui();
const { openModal } = useModal();
const visibilityLabel = useTranslatedVisibilityLabel(
canvasWidget?.conditionalAvailabilityExpression,
);
const handleResetToDefault = () => {
openModal(RESET_TAB_TO_DEFAULT_MODAL_ID);
};
const selectableItemIds = [
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
...(canMoveLeft ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT] : []),
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
...(isDefined(canvasWidget)
? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION]
: []),
...(canShowResetToDefault
? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT]
: []),
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
return (
<>
<SidePanelList selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Placement`}>
{canSetAsPinned && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
onEnter={onSetAsPinned}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
Icon={IconPinned}
label={t`Pin tab`}
onClick={onSetAsPinned}
/>
</SelectableListItem>
)}
{canMoveLeft && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
onEnter={onMoveLeft}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
Icon={IconChevronLeft}
label={t`Move left`}
onClick={onMoveLeft}
/>
</SelectableListItem>
)}
{canMoveRight && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
onEnter={onMoveRight}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
Icon={IconChevronRight}
label={t`Move right`}
onClick={onMoveRight}
/>
</SelectableListItem>
)}
</SidePanelGroup>
<SidePanelGroup heading={t`Manage`}>
{isDefined(canvasWidget) && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION}
>
<CommandMenuItemDropdown
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION}
label={t`Visibility restriction`}
Icon={IconEyeX}
dropdownId={
TAB_SETTINGS_SELECTABLE_ITEM_IDS.VISIBILITY_RESTRICTION
}
dropdownComponents={
<DropdownContent>
<CanvasTabWidgetVisibilityDropdownContent
widgetId={canvasWidget.id}
currentExpression={
canvasWidget.conditionalAvailabilityExpression
}
pageLayoutId={pageLayoutId}
/>
</DropdownContent>
}
dropdownPlacement="bottom-end"
description={visibilityLabel}
contextualTextPosition="right"
/>
</SelectableListItem>
)}
{canShowResetToDefault && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
onEnter={handleResetToDefault}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
Icon={IconRefreshDot}
label={t`Reset to default`}
onClick={handleResetToDefault}
/>
</SelectableListItem>
)}
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
onEnter={onDelete}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
Icon={IconTrash}
label={t`Delete`}
onClick={onDelete}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
<ConfirmationModal
modalInstanceId={RESET_TAB_TO_DEFAULT_MODAL_ID}
title={t`Reset to default`}
subtitle={t`This will cancel all modifications done on the tab and its widgets. Edit mode will be canceled and the page will refresh. This action cannot be undone.`}
onConfirmClick={onResetToDefault}
confirmButtonText={t`Reset`}
confirmButtonAccent="danger"
/>
</>
);
};
@@ -0,0 +1,157 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
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';
import { useLingui } from '@lingui/react/macro';
import {
IconChevronLeft,
IconChevronRight,
IconCopyPlus,
IconPinned,
IconRefreshDot,
IconTrash,
} from 'twenty-ui/display';
const RESET_TAB_TO_DEFAULT_MODAL_ID = 'reset-regular-tab-to-default-modal';
type RegularTabSettingsContentProps = {
canSetAsPinned: boolean;
canMoveLeft: boolean;
canMoveRight: boolean;
canShowResetToDefault: boolean;
canDelete: boolean;
onMoveLeft: () => void;
onMoveRight: () => void;
onSetAsPinned: () => void;
onDuplicate: () => void;
onResetToDefault: () => void;
onDelete: () => void;
};
export const RegularTabSettingsContent = ({
canSetAsPinned,
canMoveLeft,
canMoveRight,
canShowResetToDefault,
canDelete,
onMoveLeft,
onMoveRight,
onSetAsPinned,
onDuplicate,
onResetToDefault,
onDelete,
}: RegularTabSettingsContentProps) => {
const { t } = useLingui();
const { openModal } = useModal();
const handleResetToDefault = () => {
openModal(RESET_TAB_TO_DEFAULT_MODAL_ID);
};
const selectableItemIds = [
...(canMoveLeft ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT] : []),
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
...(canShowResetToDefault
? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT]
: []),
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
return (
<>
<SidePanelList selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Settings`}>
{canMoveLeft && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
onEnter={onMoveLeft}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
Icon={IconChevronLeft}
label={t`Move left`}
onClick={onMoveLeft}
/>
</SelectableListItem>
)}
{canMoveRight && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
onEnter={onMoveRight}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
Icon={IconChevronRight}
label={t`Move right`}
onClick={onMoveRight}
/>
</SelectableListItem>
)}
{canSetAsPinned && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
onEnter={onSetAsPinned}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
Icon={IconPinned}
label={t`Set as pinned tab`}
onClick={onSetAsPinned}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
onEnter={onDuplicate}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
Icon={IconCopyPlus}
label={t`Duplicate`}
onClick={onDuplicate}
/>
</SelectableListItem>
{canShowResetToDefault && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
onEnter={handleResetToDefault}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
Icon={IconRefreshDot}
label={t`Reset to default`}
onClick={handleResetToDefault}
/>
</SelectableListItem>
)}
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
onEnter={onDelete}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
Icon={IconTrash}
label={t`Delete`}
onClick={onDelete}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
<ConfirmationModal
modalInstanceId={RESET_TAB_TO_DEFAULT_MODAL_ID}
title={t`Reset to default`}
subtitle={t`This will cancel all modifications done on the tab and its widgets. Edit mode will be canceled and the page will refresh. This action cannot be undone.`}
onConfirmClick={onResetToDefault}
confirmButtonText={t`Reset`}
confirmButtonAccent="danger"
/>
</>
);
};
@@ -1,4 +1,3 @@
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
@@ -9,40 +8,27 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
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';
import { CanvasTabSettingsContent } from '@/side-panel/pages/page-layout/components/CanvasTabSettingsContent';
import { RegularTabSettingsContent } from '@/side-panel/pages/page-layout/components/RegularTabSettingsContent';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronLeft,
IconChevronRight,
IconCopyPlus,
IconPinned,
IconRefreshDot,
IconTrash,
} from 'twenty-ui/display';
import { PageLayoutType } from '~/generated-metadata/graphql';
PageLayoutTabLayoutMode,
PageLayoutType,
} from '~/generated-metadata/graphql';
type SidePanelPageLayoutTabSettingsContentProps = {
pageLayoutId: string;
recordId: string;
};
const RESET_TAB_TO_DEFAULT_MODAL_ID = 'reset-tab-to-default-modal';
export const SidePanelPageLayoutTabSettingsContent = ({
pageLayoutId,
recordId,
}: SidePanelPageLayoutTabSettingsContentProps) => {
const { t } = useLingui();
const { closeSidePanelMenu } = useSidePanelMenu();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
@@ -76,7 +62,6 @@ export const SidePanelPageLayoutTabSettingsContent = ({
const { setAsPinnedTab } = useSetAsPinnedTab(pageLayoutId);
const { resetPageLayoutTabToDefault } =
useResetPageLayoutTabToDefault(pageLayoutId);
const { openModal } = useModal();
if (!isDefined(pageLayoutTabSettingsOpenTabId)) {
return null;
@@ -110,114 +95,43 @@ export const SidePanelPageLayoutTabSettingsContent = ({
};
const handleResetToDefault = () => {
openModal(RESET_TAB_TO_DEFAULT_MODAL_ID);
};
const handleConfirmReset = () => {
resetPageLayoutTabToDefault(tab.id);
};
const selectableItemIds = [
...(canMoveLeft ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT] : []),
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
...(canShowResetToDefault
? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT]
: []),
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
];
const isCanvasTab = tab.layoutMode === PageLayoutTabLayoutMode.CANVAS;
if (isCanvasTab) {
return (
<CanvasTabSettingsContent
pageLayoutId={pageLayoutId}
canvasWidget={tab.widgets.at(0)}
canSetAsPinned={canSetAsPinned}
canMoveLeft={canMoveLeft}
canMoveRight={canMoveRight}
canShowResetToDefault={canShowResetToDefault}
canDelete={canDelete}
onMoveLeft={() => moveLeft(tab.id)}
onMoveRight={() => moveRight(tab.id)}
onSetAsPinned={() => setAsPinnedTab(tab.id)}
onResetToDefault={handleResetToDefault}
onDelete={handleDelete}
/>
);
}
return (
<>
<SidePanelList selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Settings`}>
{canMoveLeft && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
onEnter={() => moveLeft(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_LEFT}
Icon={IconChevronLeft}
label={t`Move left`}
onClick={() => moveLeft(tab.id)}
/>
</SelectableListItem>
)}
{canMoveRight && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
onEnter={() => moveRight(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT}
Icon={IconChevronRight}
label={t`Move right`}
onClick={() => moveRight(tab.id)}
/>
</SelectableListItem>
)}
{canSetAsPinned && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
onEnter={() => setAsPinnedTab(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED}
Icon={IconPinned}
label={t`Set as pinned tab`}
onClick={() => setAsPinnedTab(tab.id)}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
onEnter={() => duplicateTab(tab.id)}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE}
Icon={IconCopyPlus}
label={t`Duplicate`}
onClick={() => duplicateTab(tab.id)}
/>
</SelectableListItem>
{canShowResetToDefault && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
onEnter={handleResetToDefault}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
Icon={IconRefreshDot}
label={t`Reset to default`}
onClick={handleResetToDefault}
/>
</SelectableListItem>
)}
{canDelete && (
<SelectableListItem
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
onEnter={handleDelete}
>
<CommandMenuItem
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
Icon={IconTrash}
label={t`Delete`}
onClick={handleDelete}
/>
</SelectableListItem>
)}
</SidePanelGroup>
</SidePanelList>
<ConfirmationModal
modalInstanceId={RESET_TAB_TO_DEFAULT_MODAL_ID}
title={t`Reset to default`}
subtitle={t`This will cancel all modifications done on the tab and its widgets. Edit mode will be canceled and the page will refresh. This action cannot be undone.`}
onConfirmClick={handleConfirmReset}
confirmButtonText={t`Reset`}
confirmButtonAccent="danger"
/>
</>
<RegularTabSettingsContent
canSetAsPinned={canSetAsPinned}
canMoveLeft={canMoveLeft}
canMoveRight={canMoveRight}
canShowResetToDefault={canShowResetToDefault}
canDelete={canDelete}
onMoveLeft={() => moveLeft(tab.id)}
onMoveRight={() => moveRight(tab.id)}
onSetAsPinned={() => setAsPinnedTab(tab.id)}
onDuplicate={() => duplicateTab(tab.id)}
onResetToDefault={handleResetToDefault}
onDelete={handleDelete}
/>
);
};
@@ -7,8 +7,8 @@ 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 { useTranslatedVisibilityLabel } from '@/side-panel/pages/page-layout/hooks/useTranslatedVisibilityLabel';
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';
@@ -51,19 +51,14 @@ export const WidgetSettingsManageSection = ({
const { openModal } = useModal();
const visibilityLabel = useTranslatedVisibilityLabel(
widgetInEditMode?.conditionalAvailabilityExpression,
);
if (!isDefined(pageLayoutEditingWidgetId)) {
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);
};
@@ -0,0 +1,75 @@
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { VISIBILITY_OPTIONS } from '@/side-panel/pages/page-layout/constants/VisibilityOptions';
import { useVisibilityLabels } from '@/side-panel/pages/page-layout/hooks/useVisibilityLabels';
import { expressionToOptionId } from '@/side-panel/pages/page-layout/utils/expressionToOptionId';
import { optionIdToExpression } from '@/side-panel/pages/page-layout/utils/optionIdToExpression';
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 { MenuItemSelect } from 'twenty-ui/navigation';
type CanvasTabWidgetVisibilityDropdownContentProps = {
widgetId: string;
currentExpression: string | null | undefined;
pageLayoutId: string;
};
export const CanvasTabWidgetVisibilityDropdownContent = ({
widgetId,
currentExpression,
pageLayoutId,
}: CanvasTabWidgetVisibilityDropdownContentProps) => {
const currentOptionId = expressionToOptionId(currentExpression);
const dropdownId = useAvailableComponentInstanceIdOrThrow(
DropdownComponentInstanceContext,
);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
dropdownId,
);
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const { closeDropdown } = useCloseDropdown();
const handleSelectVisibility = (optionId: string) => {
updatePageLayoutWidget(widgetId, {
conditionalAvailabilityExpression: optionIdToExpression(optionId),
});
closeDropdown();
};
const visibilityLabels = useVisibilityLabels();
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] ?? option.id}
selected={currentOptionId === option.id}
focused={selectedItemId === option.id}
onClick={() => handleSelectVisibility(option.id)}
/>
</SelectableListItem>
))}
</SelectableList>
</DropdownMenuItemsContainer>
);
};
@@ -1,8 +1,10 @@
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 { VISIBILITY_OPTIONS } from '@/side-panel/pages/page-layout/constants/VisibilityOptions';
import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useVisibilityLabels } from '@/side-panel/pages/page-layout/hooks/useVisibilityLabels';
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
import { expressionToOptionId } from '@/side-panel/pages/page-layout/utils/expressionToOptionId';
import { optionIdToExpression } from '@/side-panel/pages/page-layout/utils/optionIdToExpression';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
@@ -11,51 +13,9 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
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);
@@ -85,11 +45,7 @@ export const WidgetVisibilityDropdownContent = () => {
closeDropdown();
};
const visibilityLabels: Record<string, string> = {
[VISIBILITY_ANY_DEVICE]: t`Any device`,
[VISIBILITY_MOBILE]: t`Mobile`,
[VISIBILITY_DESKTOP]: t`Desktop`,
};
const visibilityLabels = useVisibilityLabels();
return (
<DropdownMenuItemsContainer>
@@ -0,0 +1 @@
export const VISIBILITY_ANY_DEVICE = 'any-device';
@@ -0,0 +1 @@
export const VISIBILITY_DESKTOP = 'desktop';
@@ -0,0 +1 @@
export const VISIBILITY_MOBILE = 'mobile';
@@ -0,0 +1,9 @@
import { VISIBILITY_ANY_DEVICE } from '@/side-panel/pages/page-layout/constants/VisibilityAnyDevice';
import { VISIBILITY_DESKTOP } from '@/side-panel/pages/page-layout/constants/VisibilityDesktop';
import { VISIBILITY_MOBILE } from '@/side-panel/pages/page-layout/constants/VisibilityMobile';
export const VISIBILITY_OPTIONS = [
{ id: VISIBILITY_ANY_DEVICE },
{ id: VISIBILITY_MOBILE },
{ id: VISIBILITY_DESKTOP },
] as const;
@@ -3,6 +3,7 @@ export const TAB_SETTINGS_SELECTABLE_ITEM_IDS = {
MOVE_RIGHT: 'tab-move-right',
SET_AS_PINNED: 'tab-set-as-pinned',
DUPLICATE: 'tab-duplicate',
VISIBILITY_RESTRICTION: 'tab-visibility-restriction',
RESET_TO_DEFAULT: 'tab-reset-to-default',
DELETE: 'tab-delete',
} as const;
@@ -0,0 +1,14 @@
import { getVisibilityLabel } from '@/side-panel/pages/page-layout/utils/getVisibilityLabel';
import { useLingui } from '@lingui/react/macro';
export const useTranslatedVisibilityLabel = (
expression: string | null | undefined,
): string => {
const { t } = useLingui();
return getVisibilityLabel(expression, {
anyDevice: t`Any device`,
mobile: t`Mobile`,
desktop: t`Desktop`,
});
};
@@ -0,0 +1,14 @@
import { VISIBILITY_ANY_DEVICE } from '@/side-panel/pages/page-layout/constants/VisibilityAnyDevice';
import { VISIBILITY_DESKTOP } from '@/side-panel/pages/page-layout/constants/VisibilityDesktop';
import { VISIBILITY_MOBILE } from '@/side-panel/pages/page-layout/constants/VisibilityMobile';
import { useLingui } from '@lingui/react/macro';
export const useVisibilityLabels = (): Record<string, string> => {
const { t } = useLingui();
return {
[VISIBILITY_ANY_DEVICE]: t`Any device`,
[VISIBILITY_MOBILE]: t`Mobile`,
[VISIBILITY_DESKTOP]: t`Desktop`,
};
};
@@ -0,0 +1,23 @@
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 { VISIBILITY_ANY_DEVICE } from '@/side-panel/pages/page-layout/constants/VisibilityAnyDevice';
import { VISIBILITY_DESKTOP } from '@/side-panel/pages/page-layout/constants/VisibilityDesktop';
import { VISIBILITY_MOBILE } from '@/side-panel/pages/page-layout/constants/VisibilityMobile';
export 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;
};
@@ -0,0 +1,15 @@
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 { VISIBILITY_DESKTOP } from '@/side-panel/pages/page-layout/constants/VisibilityDesktop';
import { VISIBILITY_MOBILE } from '@/side-panel/pages/page-layout/constants/VisibilityMobile';
export 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;
}
};
@@ -140,7 +140,7 @@ export const TAB_PROPS = {
title: 'Note',
position: 15,
icon: 'IconNotes',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
},
flow: {
title: 'Flow',
@@ -203,13 +203,13 @@ export const WIDGET_PROPS = {
title: 'Note',
type: WidgetType.FIELD_RICH_TEXT,
gridPosition: GRID_POSITIONS.RICH_TEXT,
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FIRST,
},
taskRichText: {
title: 'Task',
type: WidgetType.FIELD_RICH_TEXT,
gridPosition: GRID_POSITIONS.RICH_TEXT,
position: CANVAS_LAYOUT_POSITIONS.DEFAULT,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FIRST,
},
workflow: {
title: 'Flow',