Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7338 refactor actionbar and contextmenu to use the context store #7462

Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0fa87af
create states
bosiraphael Oct 1, 2024
fdd4028
create hooks
bosiraphael Oct 1, 2024
7b96e35
remove hooks
bosiraphael Oct 1, 2024
d84ccd8
setContextStoreCurrentViewId
bosiraphael Oct 1, 2024
b24fa66
setContextStoreCurrentViewId on page change effect
bosiraphael Oct 1, 2024
ad6947e
setContextStoreCurrentObjectMetadataId inside PageChangeEffect
bosiraphael Oct 1, 2024
dd5b1be
set contextStoreTargetedRecordIdsState
bosiraphael Oct 1, 2024
5cf71b3
introduce RecordShowPageEffect to set contextStoreTargetedRecordIds
bosiraphael Oct 1, 2024
1f329d2
remove line breaks
bosiraphael Oct 1, 2024
fc04ff2
refactor action bar
bosiraphael Oct 2, 2024
bca53ed
create ActionMenu component
bosiraphael Oct 2, 2024
accf0dc
refactoring ActionMenu components
bosiraphael Oct 2, 2024
f582a5c
fix ActionMenuDropdown behavior
bosiraphael Oct 2, 2024
a6b0137
renaming
bosiraphael Oct 2, 2024
dc9f787
Merge branch 'main' into 7338-refactor-actionbar-and-contextmenu-to-u…
bosiraphael Oct 3, 2024
7eae28c
move components outside of ui folder
bosiraphael Oct 3, 2024
bcaf61e
create bottom bar component
bosiraphael Oct 3, 2024
deafb91
refactor ActionMenu and BottomBar component
bosiraphael Oct 4, 2024
96091ce
scope component states
bosiraphael Oct 4, 2024
347284c
create useActionMenu hook
bosiraphael Oct 4, 2024
4ca377d
Merge branch 'main' into 7338-refactor-actionbar-and-contextmenu-to-u…
bosiraphael Oct 4, 2024
b851f29
add ActionMenuEffect
bosiraphael Oct 4, 2024
e542d0c
set state in useRecoilCallback
bosiraphael Oct 7, 2024
a2d85c5
set ActionMenuDropdown open state inside recoil callback when resetti…
bosiraphael Oct 7, 2024
fc6b859
remove empty component
bosiraphael Oct 7, 2024
1baffb6
remove unused dependency from dependency array
bosiraphael Oct 7, 2024
588d1aa
remove unused dependency from dependency array
bosiraphael Oct 7, 2024
f18d74f
create bottom bar stories
bosiraphael Oct 7, 2024
75630ad
add stories for ActionBar
bosiraphael Oct 7, 2024
e83d2f1
create ActionMenuDropdown stories
bosiraphael Oct 7, 2024
9ac48f4
make actionMenuDropdownPositionState a component state
bosiraphael Oct 7, 2024
31804a8
Merge branch 'main' into 7338-refactor-actionbar-and-contextmenu-to-u…
bosiraphael Oct 8, 2024
f2571fc
merge main
bosiraphael Oct 8, 2024
c0749cd
fix ActionMenuDropdown position
bosiraphael Oct 9, 2024
5253de4
remove unused type
bosiraphael Oct 9, 2024
98352d7
rename ActionBar
bosiraphael Oct 9, 2024
40f3c05
rename ActionMenuConfirmationModals
bosiraphael Oct 9, 2024
c746207
make actionMenuEntriesState a component state
bosiraphael Oct 9, 2024
77b6155
create ActionMenuEntriesEffect
bosiraphael Oct 9, 2024
8ebbd57
remove legacy code
bosiraphael Oct 9, 2024
8ba4a59
rename action to entry
bosiraphael Oct 9, 2024
42e42bf
rename item to entry
bosiraphael Oct 9, 2024
9c58739
Merge branch '7338-refactor-actionbar-and-contextmenu-to-use-the-cont…
bosiraphael Oct 9, 2024
a94c972
remove useless style
bosiraphael Oct 9, 2024
036df02
fix stories
bosiraphael Oct 9, 2024
203d648
remove subactions
bosiraphael Oct 9, 2024
1f9fcf7
modify bottomBar states to use the new api
bosiraphael Oct 9, 2024
3ab21bd
fixes
bosiraphael Oct 9, 2024
09dc4fc
Merge branch 'main' into 7338-refactor-actionbar-and-contextmenu-to-u…
bosiraphael Oct 9, 2024
26a9197
fix stories
bosiraphael Oct 9, 2024
928087e
add tests
bosiraphael Oct 10, 2024
56e232f
remove objectMetadataItemFamilySelectorById
bosiraphael Oct 10, 2024
66f0d31
add tests
bosiraphael Oct 10, 2024
8508f49
add tests for useActionMenu
bosiraphael Oct 10, 2024
c7bb78c
remove logs
bosiraphael Oct 10, 2024
6ab5d31
replace enum folder to types
bosiraphael Oct 10, 2024
b83bae6
increase coverage
bosiraphael Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions packages/twenty-front/src/effect-components/PageChangeEffect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import {
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconCheckbox } from 'twenty-ui';

import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
Expand All @@ -12,6 +17,10 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandType } from '@/command-menu/types/Command';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreCurrentViewIdState } from '@/context-store/states/contextStoreCurrentViewIdState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { AppBasePath } from '@/types/AppBasePath';
Expand Down Expand Up @@ -49,6 +58,29 @@ export const PageChangeEffect = () => {
activityObjectNameSingular: CoreObjectNameSingular.Task,
});

const [searchParams] = useSearchParams();
const { objectNameSingular, objectNamePlural } = useParams();
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

const objectMetadataItem = useRecoilValue(
objectMetadataItemFamilySelector(
objectNameSingular
? { objectName: objectNameSingular, objectNameType: 'singular' }
: objectNamePlural
? { objectName: objectNamePlural, objectNameType: 'plural' }
: { objectName: '', objectNameType: 'singular' },
),
);

const setContextStoreCurrentViewId = useSetRecoilState(
contextStoreCurrentViewIdState,
);
const setContextStoreCurrentObjectMetadataId = useSetRecoilState(
contextStoreCurrentObjectMetadataIdState,
);
const setContextStoreTargetedRecordIds = useSetRecoilState(
contextStoreTargetedRecordIdsState,
);

useEffect(() => {
cleanRecoilState();
}, [cleanRecoilState]);
Expand All @@ -67,6 +99,20 @@ export const PageChangeEffect = () => {
}
}, [navigate, pageChangeEffectNavigateLocation]);

useEffect(() => {
const viewId = searchParams.get('view');
setContextStoreCurrentViewId(viewId);
setContextStoreTargetedRecordIds([]);
}, [
searchParams,
setContextStoreCurrentViewId,
setContextStoreTargetedRecordIds,
]);

useEffect(() => {
setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id ?? null);
}, [objectMetadataItem, setContextStoreCurrentObjectMetadataId]);

useEffect(() => {
switch (true) {
case isMatchingLocation(AppPath.RecordIndexPage): {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import styled from '@emotion/styled';

import { actionMenuEntriesState } from '@/action-menu/states/actionMenuEntriesState';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { useRecoilValue } from 'recoil';
import { ActionBarItem } from './ActionBarItem';

const StyledLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;

type ActionBarProps = {
actionMenuId: string;
};

export const ActionBar = ({ actionMenuId }: ActionBarProps) => {
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);

const actionMenuEntries = useRecoilValue(actionMenuEntriesState);

return (
<BottomBar
bottomBarId={`action-bar-${actionMenuId}`}
bottomBarHotkeyScopeFromParent={{
scope: ActionBarHotkeyScope.ActionBar,
}}
>
<StyledLabel>
{contextStoreTargetedRecordIds.length} selected:
</StyledLabel>
{actionMenuEntries.map((item, index) => (
<ActionBarItem key={index} item={item} />
))}
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
</BottomBar>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChevronDown } from 'twenty-ui';

import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';

type ActionBarItemProps = {
item: ActionBarEntry;
item: ActionMenuEntry;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
};

const StyledButton = styled.div<{ accent: MenuItemAccent }>`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';

import { PositionType } from '../types/PositionType';

import { actionMenuDropdownPositionState } from '@/action-menu/states/actionMenuDropdownPositionState';
import { actionMenuEntriesState } from '@/action-menu/states/actionMenuEntriesState';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';

type StyledContainerProps = {
position: PositionType;
};

const StyledContainerActionMenuDropdown = styled.div<StyledContainerProps>`
align-items: flex-start;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex;
flex-direction: column;
gap: 1px;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

left: ${(props) => `${props.position.x}px`};
position: fixed;
top: ${(props) => `${props.position.y}px`};

transform: translateX(-50%);
width: auto;
z-index: 2;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
`;

export type ActionMenuDropdownProps = {
actionMenuId: string;
};

export const ActionMenuDropdown = ({
actionMenuId,
}: ActionMenuDropdownProps) => {
const actionMenuEntries = useRecoilValue(actionMenuEntriesState);

const actionMenuDropdownPosition = useRecoilValue(
actionMenuDropdownPositionState,
);

//TODO: remove this
const width = actionMenuEntries.some(
(actionMenuEntry) => actionMenuEntry.label === 'Remove from favorites',
)
? 200
: undefined;

return (
<StyledContainerActionMenuDropdown
position={actionMenuDropdownPosition}
className="context-menu"
>
<Dropdown
dropdownId={`action-menu-dropdown-${actionMenuId}`}
dropdownHotkeyScope={{
scope: ActionMenuDropdownHotkeyScope.ActionMenuDropdown,
}}
data-select-disable
dropdownMenuWidth={width}
dropdownComponents={actionMenuEntries.map((item, index) => (
<MenuItem
key={index}
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
LeftIcon={item.Icon}
onClick={item.onClick}
accent={item.accent}
text={item.label}
/>
))}
/>
</StyledContainerActionMenuDropdown>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';

type ActionMenuEffectProps = {
actionMenuId: string;
};

export const ActionMenuEffect = ({ actionMenuId }: ActionMenuEffectProps) => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);

const { openActionBar, closeActionMenuDropdown, closeActionBar } =
useActionMenu(actionMenuId);

const isDropdownOpen = useRecoilValue(
extractComponentState(
isDropdownOpenComponentState,
`action-menu-dropdown-${actionMenuId}`,
),
);

useEffect(() => {
if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) {
openActionBar();
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
}
if (contextStoreTargetedRecordIds.length === 0) {
closeActionBar();
}
}, [
contextStoreTargetedRecordIds,
openActionBar,
closeActionMenuDropdown,
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
closeActionBar,
isDropdownOpen,
]);

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { actionMenuEntriesState } from '@/action-menu/states/actionMenuEntriesState';
import { useRecoilValue } from 'recoil';

export const ActionMenuNavigationModal = () => {
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
const actionMenuEntries = useRecoilValue(actionMenuEntriesState);

return (
<div data-select-disable>
{actionMenuEntries.map((actionMenuEntry, index) =>
actionMenuEntry.ConfirmationModal ? (
<div key={index}>{actionMenuEntry.ConfirmationModal}</div>
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
) : null,
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useBottomBar } from '@/ui/layout/bottom-bar/hooks/useBottomBar';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';

export const useActionMenu = (actionMenuId: string) => {
const { openDropdown, closeDropdown } = useDropdownV2();
const { openBottomBar, closeBottomBar } = useBottomBar();

const openActionMenuDropdown = () => {
closeBottomBar(`action-bar-${actionMenuId}`);
openDropdown(`action-menu-dropdown-${actionMenuId}`);
};

const openActionBar = () => {
closeDropdown(`action-menu-dropdown-${actionMenuId}`);
openBottomBar(`action-bar-${actionMenuId}`);
};

const closeActionMenuDropdown = () => {
closeDropdown(`action-menu-dropdown-${actionMenuId}`);
};

const closeActionBar = () => {
closeBottomBar(`action-bar-${actionMenuId}`);
};

return {
openActionMenuDropdown,
openActionBar,
closeActionBar,
closeActionMenuDropdown,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const RightDrawerActionMenu = () => {
return;
};
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PositionType } from '@/action-menu/types/PositionType';
import { createState } from 'twenty-ui';

export const actionMenuDropdownPositionState = createState<PositionType>({
key: 'actionMenuDropdownPositionState',
defaultValue: {
x: null,
y: null,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createState } from 'twenty-ui';

import { ActionMenuEntry } from '../types/ActionMenuEntry';

export const actionMenuEntriesState = createState<ActionMenuEntry[]>({
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
key: 'actionMenuEntriesState',
defaultValue: [],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ActionBarHotkeyScope {
ActionBar = 'action-bar',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ActionMenuDisplay {
NONE = 'NONE',
DROPDOWN = 'DROPDOWN',
ACTION_BAR = 'ACTION_BAR',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ActionMenuDropdownHotkeyScope {
ActionMenuDropdown = 'action-menu-dropdown',
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { IconComponent } from 'twenty-ui';

import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';

export type ContextMenuEntry = {
export type ActionMenuEntry = {
label: string;
Icon: IconComponent;
accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode;
subActions?: ActionMenuEntry[];
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createState } from 'twenty-ui';

export const contextStoreCurrentObjectMetadataIdState = createState<
string | null
>({
key: 'contextStoreCurrentObjectMetadataIdState',
defaultValue: null,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';

export const contextStoreCurrentViewIdState = createState<string | null>({
key: 'contextStoreCurrentViewIdState',
defaultValue: null,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';

export const contextStoreTargetedRecordIdsState = createState<string[]>({
key: 'contextStoreTargetedRecordIdsState',
defaultValue: [],
});
Loading
Loading