Merge branch 'main' into fix/operand-dropdown

This commit is contained in:
Lucas Bordeau 2024-10-31 15:37:06 +01:00
commit d61abf9564
46 changed files with 335 additions and 277 deletions

View File

@ -33,3 +33,7 @@ Your turn 👇
» 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-crm) » 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-crm)
» 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/is-twenty-crm-the-right-tool-for-your-business-heres-my-honest-review-0d41e9d8a7eb) » 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/is-twenty-crm-the-right-tool-for-your-business-heres-my-honest-review-0d41e9d8a7eb)
» 30-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) blog Link: [blog](https://dev.to/adityadeshlahre/transform-your-customer-relationships-with-the-leading-open-source-crm-twenty-161d)
» 30-October-2024 by [RajuGangitla](https://oss.gg/RajuGangitla) blog Link: [blog](https://zedblock.com/blog/twenty-crm)

View File

@ -31,3 +31,7 @@ Your turn 👇
» 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-self-host) » 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-self-host)
» 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/comprehensive-guide-to-self-hosting-twenty-crm-26e7fa36c846) » 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/comprehensive-guide-to-self-hosting-twenty-crm-26e7fa36c846)
» 30-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) blog Link: [blog](https://dev.to/adityadeshlahre/complete-guide-to-self-hosting-twenty-crm-2h08)
» 30-October-2024 by [RajuGangitla](https://oss.gg/RajuGangitla) blog Link: [blog](https://dev.to/raju_gangitla_91920e1427f/self-hosting-twenty-crm-a-complete-guide-559n)

View File

@ -18,11 +18,13 @@ Your turn 👇
//////////////////////////// ////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/)
» 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) Figma Link: [Figma](https://www.figma.com/design/XE21QdkFuy0IJHtmW7TURa/Twenty-(rajeevDewangan)?node-id=0-1&node-type=canvas&t=BYBulCT6hpJu6E8G-0) » 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) Figma Link: [Figma](https://www.figma.com/design/XE21QdkFuy0IJHtmW7TURa/Twenty-(rajeevDewangan)?node-id=0-1&node-type=canvas&t=BYBulCT6hpJu6E8G-0)
» 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) Figma Link: [Figma](https://www.figma.com/design/HqYQrzel3e2TjzujwfdCXZ/Twenty-(Copy)---Khaan25?node-id=478-19796&t=QTB8gzKTudbVNeNs-1) » 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) Figma Link: [Figma](https://www.figma.com/design/HqYQrzel3e2TjzujwfdCXZ/Twenty-(Copy)---Khaan25?node-id=478-19796&t=QTB8gzKTudbVNeNs-1)
» 24-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Figma Link: [Figma](https://www.figma.com/design/2qlAPS3llwf8jrWKGHEf6O/Twenty-(sateshcharan)?node-id=1633-94880&t=GIceWxqyY0ajWXnZ-1) » 24-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Figma Link: [Figma](https://www.figma.com/design/2qlAPS3llwf8jrWKGHEf6O/Twenty-(sateshcharan)?node-id=1633-94880&t=GIceWxqyY0ajWXnZ-1)
» 28-October-2024 by [Vanshdeep Singh](https://oss.gg/Vanshdeepsingh-2232) Figma Link:[Figma](https://www.figma.com/design/akgDOb37YLUW9iWLB155EV/Twenty-(Copy)?node-id=478-19796&t=8Gz1yqls2Q3dsN9h-1) » 28-October-2024 by [Vanshdeep Singh](https://oss.gg/Vanshdeepsingh-2232) Figma Link:[Figma](https://www.figma.com/design/akgDOb37YLUW9iWLB155EV/Twenty-(Copy)?node-id=478-19796&t=8Gz1yqls2Q3dsN9h-1)
» 30-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) Figma Link:[Figma](https://www.figma.com/design/4hlpS6LOIaJqDbd6T6SlXc/CustomTwentyByAdityDeshlahre?node-id=478-19796&t=Dp8EBpl0FxVjiGHT-1) Figma Preview Link:[Prototype](https://www.figma.com/proto/4hlpS6LOIaJqDbd6T6SlXc/CustomTwentyByAdityDeshlahre?node-id=478-19796&t=Dp8EBpl0FxVjiGHT-1)

View File

@ -20,4 +20,9 @@ Your turn 👇
» 22-October-2024 by [FaheemOnHub](https://oss.gg/FaheemOnHub) video Link: [video](https://drive.google.com/file/d/1bR59Q5gqoqHjzgdrF6K68U2hloexkQYM/view) » 22-October-2024 by [FaheemOnHub](https://oss.gg/FaheemOnHub) video Link: [video](https://drive.google.com/file/d/1bR59Q5gqoqHjzgdrF6K68U2hloexkQYM/view)
» 24-October-2024 by [chrisdadev13](https://oss.gg/chrisdadev13) video Link: [video](https://www.loom.com/share/f98f34f8d9b34728998847d3b97a16b7)
» 27-October-2024 by [Khaan25](https://oss.gg/Khaan25) video Link: [video](https://drive.google.com/file/d/1-wgzofJaWmnMcFgZZV5uYNNgtbJKJ_1G/view?usp=sharing/) » 27-October-2024 by [Khaan25](https://oss.gg/Khaan25) video Link: [video](https://drive.google.com/file/d/1-wgzofJaWmnMcFgZZV5uYNNgtbJKJ_1G/view?usp=sharing/)
» 30-October-2024 by [RajuGangitla](https://oss.gg/RajuGangitla) video Link: [video](https://www.loom.com/share/f072bf31fb46449d98d6826a3a824fe9?sid=21f2c3f4-f286-43a2-98aa-d1fb92c3a86e)

View File

@ -20,4 +20,5 @@ Your turn 👇
» 25-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) template Link: [template]() » 25-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) template Link: [template]()
» 30-October-2024 by [RajuGangitla](https://oss.gg/RajuGangitla) video Link: [video](https://www.loom.com/share/89f86ef895e946fbbbbae3cc90559bb7?sid=dbebe60b-3ece-4ac8-acb2-184043c6f87b)
--- ---

View File

@ -20,4 +20,6 @@ Your turn 👇
» 26-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) guide Link: [guide](https://dev.to/sateshcharan/supercharge-your-marketing-with-twentycrm-n8n-1hfd) » 26-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) guide Link: [guide](https://dev.to/sateshcharan/supercharge-your-marketing-with-twentycrm-n8n-1hfd)
» 31-October-2024 by [RajuGangitla](https://oss.gg/RajuGangitla) guide Link: [guide](https://dev.to/raju_gangitla_91920e1427f/automating-people-data-sync-n8n-workflow-for-twenty-crm-and-google-sheets-5789
)
--- ---

View File

@ -102,6 +102,7 @@ export const DeleteRecordsActionEffect = ({
position, position,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true,
onClick: () => { onClick: () => {
setIsDeleteRecordsModalOpen(true); setIsDeleteRecordsModalOpen(true);
}, },

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
@ -30,7 +31,9 @@ export const RecordIndexActionMenuBar = () => {
actionMenuEntriesComponentSelector, actionMenuEntriesComponentSelector,
); );
if (actionMenuEntries.length === 0) { const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (pinnedEntries.length === 0) {
return null; return null;
} }
@ -42,9 +45,10 @@ export const RecordIndexActionMenuBar = () => {
}} }}
> >
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel> <StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{actionMenuEntries.map((entry, index) => ( {pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} /> <RecordIndexActionMenuBarEntry key={index} entry={entry} />
))} ))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar> </BottomBar>
); );
}; };

View File

@ -0,0 +1,53 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconLayoutSidebarRightExpand } from 'twenty-ui';
const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none;
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledButtonLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledShortcutLabel = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
background: ${({ theme }) => theme.border.color.light};
height: ${({ theme, size }) => theme.spacing(size === 'sm' ? 4 : 8)};
margin: 0 ${({ theme }) => theme.spacing(1)};
width: 1px;
`;
export const RecordIndexActionMenuBarAllActionsButton = () => {
const theme = useTheme();
const { openCommandMenu } = useCommandMenu();
return (
<>
<StyledSeparator size="md" />
<StyledButton onClick={() => openCommandMenu()}>
<IconLayoutSidebarRightExpand size={theme.icon.size.md} />
<StyledButtonLabel>All Actions</StyledButtonLabel>
<StyledSeparator size="sm" />
<StyledShortcutLabel>K</StyledShortcutLabel>
</StyledButton>
</>
);
};

View File

@ -2,31 +2,24 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
type RecordIndexActionMenuBarEntryProps = { type RecordIndexActionMenuBarEntryProps = {
entry: ActionMenuEntry; entry: ActionMenuEntry;
}; };
const StyledButton = styled.div<{ accent: MenuItemAccent }>` const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) => color: ${({ theme }) => theme.font.color.secondary};
props.accent === 'danger'
? props.theme.color.red
: props.theme.font.color.secondary};
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
transition: background 0.1s ease; transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none; user-select: none;
&:hover { &:hover {
background: ${({ theme, accent }) => background: ${({ theme }) => theme.background.tertiary};
accent === 'danger'
? theme.background.danger
: theme.background.tertiary};
} }
`; `;
@ -40,10 +33,7 @@ export const RecordIndexActionMenuBarEntry = ({
}: RecordIndexActionMenuBarEntryProps) => { }: RecordIndexActionMenuBarEntryProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<StyledButton <StyledButton onClick={() => entry.onClick?.()}>
accent={entry.accent ?? 'default'}
onClick={() => entry.onClick?.()}
>
{entry.Icon && <entry.Icon size={theme.icon.size.md} />} {entry.Icon && <entry.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{entry.label}</StyledButtonLabel> <StyledButtonLabel>{entry.label}</StyledButtonLabel>
</StyledButton> </StyledButton>

View File

@ -10,15 +10,15 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test'; import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui'; import { IconTrash, RouterDecorator } from 'twenty-ui';
const deleteMock = jest.fn(); const deleteMock = jest.fn();
const markAsDoneMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = { const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar', title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar, component: RecordIndexActionMenuBar,
decorators: [ decorators: [
RouterDecorator,
(Story) => ( (Story) => (
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }} value={{ instanceId: 'story-action-menu' }}
@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
[ [
'delete', 'delete',
{ {
isPinned: true,
key: 'delete', key: 'delete',
label: 'Delete', label: 'Delete',
position: 0, position: 0,
@ -55,16 +56,6 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
onClick: deleteMock, onClick: deleteMock,
}, },
], ],
[
'markAsDone',
{
key: 'markAsDone',
label: 'Mark as done',
position: 1,
Icon: IconCheckbox,
onClick: markAsDoneMock,
},
],
]), ]),
); );
set( set(
@ -120,12 +111,8 @@ export const WithButtonClicks: Story = {
const deleteButton = await canvas.findByText('Delete'); const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton); await userEvent.click(deleteButton);
const markAsDoneButton = await canvas.findByText('Mark as done');
await userEvent.click(markAsDoneButton);
await waitFor(() => { await waitFor(() => {
expect(deleteMock).toHaveBeenCalled(); expect(deleteMock).toHaveBeenCalled();
expect(markAsDoneMock).toHaveBeenCalled();
}); });
}, },
}; };

View File

@ -8,6 +8,7 @@ export type ActionMenuEntry = {
label: string; label: string;
position: number; position: number;
Icon: IconComponent; Icon: IconComponent;
isPinned?: boolean;
accent?: MenuItemAccent; accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void; onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode; ConfirmationModal?: ReactNode;

View File

@ -19,8 +19,6 @@ export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSig
__typename: true, __typename: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
activity: true,
activityId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems), ...generateActivityTargetMorphFieldKeys(objectMetadataItems),
}, },
}); });

View File

@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
@ -16,7 +17,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useScrollRestoration } from '~/hooks/useScrollRestoration'; import { useScrollRestoration } from '~/hooks/useScrollRestoration';
@ -69,9 +70,15 @@ export const RecordBoard = () => {
const { resetRecordSelection, setRecordAsSelected } = const { resetRecordSelection, setRecordAsSelected } =
useRecordBoardSelection(recordBoardId); useRecordBoardSelection(recordBoardId);
useListenClickOutsideByClassName({ useListenClickOutsideV2({
classNames: ['record-board-card'], excludeClassNames: [
excludeClassNames: ['bottom-bar', 'action-menu-dropdown', 'command-menu'], 'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
refs: [boardRef],
callback: resetRecordSelection, callback: resetRecordSelection,
}); });

View File

@ -0,0 +1 @@
export const RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID = 'record-board';

View File

@ -84,9 +84,9 @@ export const MultiItemFieldInput = <T,>({
setInputValue(value); setInputValue(value);
if (!validateInput) return; if (!validateInput) return;
if (errorData.isValid) { setErrorData(
setErrorData(errorData); errorData.isValid ? errorData : { isValid: true, errorMessage: '' },
} );
}; };
const handleAddButtonClick = () => { const handleAddButtonClick = () => {

View File

@ -7,6 +7,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
@ -81,12 +82,14 @@ export const RecordIndexTableContainerEffect = () => {
const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector()); const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector());
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
useEffect(() => { useEffect(() => {
if (hasUserSelectedAllRows) { if (hasUserSelectedAllRows) {
setContextStoreTargetedRecords({ setContextStoreTargetedRecords({
mode: 'exclusion', mode: 'exclusion',
excludedRecordIds: unselectedRowIds, excludedRecordIds: unselectedRowIds,
filters: [], filters: recordIndexFilters,
}); });
} else { } else {
setContextStoreTargetedRecords({ setContextStoreTargetedRecords({
@ -103,6 +106,7 @@ export const RecordIndexTableContainerEffect = () => {
}; };
}, [ }, [
hasUserSelectedAllRows, hasUserSelectedAllRows,
recordIndexFilters,
selectedRowIds, selectedRowIds,
setContextStoreTargetedRecords, setContextStoreTargetedRecords,
unselectedRowIds, unselectedRowIds,

View File

@ -1,19 +1,26 @@
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
type RecordTableInternalEffectProps = { type RecordTableInternalEffectProps = {
recordTableId: string; recordTableId: string;
tableBodyRef: React.RefObject<HTMLDivElement>;
}; };
export const RecordTableInternalEffect = ({ export const RecordTableInternalEffect = ({
recordTableId, recordTableId,
tableBodyRef,
}: RecordTableInternalEffectProps) => { }: RecordTableInternalEffectProps) => {
const { leaveTableFocus, resetTableRowSelection, useMapKeyboardToSoftFocus } = const leaveTableFocus = useLeaveTableFocus(recordTableId);
useRecordTable({ recordTableId });
const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({
recordTableId,
});
useMapKeyboardToSoftFocus(); useMapKeyboardToSoftFocus();
@ -25,9 +32,15 @@ export const RecordTableInternalEffect = ({
TableHotkeyScope.Table, TableHotkeyScope.Table,
); );
useListenClickOutsideByClassName({ useListenClickOutsideV2({
classNames: ['entity-table-cell'], excludeClassNames: [
excludeClassNames: ['bottom-bar', 'action-menu-dropdown', 'command-menu'], 'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
refs: [tableBodyRef],
callback: () => { callback: () => {
leaveTableFocus(); leaveTableFocus();
}, },

View File

@ -87,7 +87,10 @@ export const RecordTableWithWrappers = ({
onDragSelectionChange={setRowSelected} onDragSelectionChange={setRowSelected}
/> />
</StyledTableInternalContainer> </StyledTableInternalContainer>
<RecordTableInternalEffect recordTableId={recordTableId} /> <RecordTableInternalEffect
tableBodyRef={tableBodyRef}
recordTableId={recordTableId}
/>
</StyledTableContainer> </StyledTableContainer>
</StyledTableWithHeader> </StyledTableWithHeader>
</RecordUpdateContext.Provider> </RecordUpdateContext.Provider>

View File

@ -0,0 +1 @@
export const RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID = 'record-table';

View File

@ -1,19 +1,13 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
import { useDisableSoftFocus } from './useDisableSoftFocus'; import { useDisableSoftFocus } from './useDisableSoftFocus';
export const useLeaveTableFocus = (recordTableId?: string) => { export const useLeaveTableFocus = (recordTableId?: string) => {
const disableSoftFocus = useDisableSoftFocus(recordTableId); const disableSoftFocus = useDisableSoftFocus(recordTableId);
const closeCurrentCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId);
const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); const { isSoftFocusActiveState } = useRecordTableStates(recordTableId);
@ -27,28 +21,14 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
isSoftFocusActiveState, isSoftFocusActiveState,
); );
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.getValue();
resetTableRowSelection(); resetTableRowSelection();
if (!isSoftFocusActive) { if (!isSoftFocusActive) {
return; return;
} }
if (currentHotkeyScope?.scope === TableHotkeyScope.Table) {
return;
}
closeCurrentCellInEditMode();
disableSoftFocus(); disableSoftFocus();
}, },
[ [disableSoftFocus, isSoftFocusActiveState, resetTableRowSelection],
closeCurrentCellInEditMode,
disableSoftFocus,
isSoftFocusActiveState,
resetTableRowSelection,
],
); );
}; };

View File

@ -4,10 +4,19 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly'; import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { useSetRecoilState } from 'recoil';
export const RecordTableCellFieldInput = () => { export const RecordTableCellFieldInput = () => {
const { getClickOutsideListenerIsActivatedState } =
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
const setClickOutsideListenerIsActivated = useSetRecoilState(
getClickOutsideListenerIsActivatedState,
);
const { onUpsertRecord, onMoveFocus, onCloseTableCell } = const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
useContext(RecordTableContext); useContext(RecordTableContext);
@ -40,6 +49,8 @@ export const RecordTableCellFieldInput = () => {
}; };
const handleClickOutside: FieldInputEvent = (persistField) => { const handleClickOutside: FieldInputEvent = (persistField) => {
setClickOutsideListenerIsActivated(false);
onUpsertRecord({ onUpsertRecord({
persistField, persistField,
recordId, recordId,

View File

@ -21,6 +21,8 @@ import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { useContext } from 'react'; import { useContext } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@ -42,6 +44,9 @@ export type OpenTableCellArgs = {
}; };
export const useOpenRecordTableCellV2 = (tableScopeId: string) => { export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const { getClickOutsideListenerIsActivatedState } =
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext); const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
const moveEditModeToTableCellPosition = const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId); useMoveEditModeToTableCellPosition(tableScopeId);
@ -65,7 +70,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const navigate = useNavigate(); const navigate = useNavigate();
const openTableCell = useRecoilCallback( const openTableCell = useRecoilCallback(
({ snapshot }) => ({ snapshot, set }) =>
({ ({
initialValue, initialValue,
cellPosition, cellPosition,
@ -80,6 +85,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
return; return;
} }
set(getClickOutsideListenerIsActivatedState, false);
const isFirstColumnCell = cellPosition.column === 0; const isFirstColumnCell = cellPosition.column === 0;
const fieldValue = getSnapshotValue( const fieldValue = getSnapshotValue(
@ -137,17 +144,18 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
} }
}, },
[ [
getClickOutsideListenerIsActivatedState,
setDragSelectionStartEnabled, setDragSelectionStartEnabled,
moveEditModeToTableCellPosition,
initDraftValue,
toggleClickOutsideListener, toggleClickOutsideListener,
leaveTableFocus, leaveTableFocus,
setHotkeyScope, navigate,
initDraftValue, indexIdentifierUrl,
moveEditModeToTableCellPosition,
openRightDrawer,
setViewableRecordId, setViewableRecordId,
setViewableRecordNameSingular, setViewableRecordNameSingular,
indexIdentifierUrl, openRightDrawer,
navigate, setHotkeyScope,
], ],
); );

View File

@ -23,7 +23,6 @@ export const SingleEntitySelect = ({
onEntitySelected, onEntitySelected,
relationObjectNameSingular, relationObjectNameSingular,
relationPickerScopeId, relationPickerScopeId,
selectedEntity,
selectedRelationRecordIds, selectedRelationRecordIds,
width = 200, width = 200,
}: SingleEntitySelectProps) => { }: SingleEntitySelectProps) => {
@ -61,7 +60,6 @@ export const SingleEntitySelect = ({
onEntitySelected, onEntitySelected,
relationObjectNameSingular, relationObjectNameSingular,
relationPickerScopeId, relationPickerScopeId,
selectedEntity,
selectedRelationRecordIds, selectedRelationRecordIds,
}} }}
/> />

View File

@ -37,6 +37,7 @@ export type SingleEntitySelectMenuItemsProps = {
onAllEntitySelected?: () => void; onAllEntitySelected?: () => void;
hotkeyScope?: string; hotkeyScope?: string;
isFiltered: boolean; isFiltered: boolean;
shouldSelectEmptyOption?: boolean;
}; };
export const SingleEntitySelectMenuItems = ({ export const SingleEntitySelectMenuItems = ({
@ -56,6 +57,7 @@ export const SingleEntitySelectMenuItems = ({
onAllEntitySelected, onAllEntitySelected,
hotkeyScope = RelationPickerHotkeyScope.RelationPicker, hotkeyScope = RelationPickerHotkeyScope.RelationPicker,
isFiltered, isFiltered,
shouldSelectEmptyOption,
}: SingleEntitySelectMenuItemsProps) => { }: SingleEntitySelectMenuItemsProps) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -181,7 +183,7 @@ export const SingleEntitySelectMenuItems = ({
onClick={() => onEntitySelected()} onClick={() => onEntitySelected()}
LeftIcon={EmptyIcon} LeftIcon={EmptyIcon}
text={emptyLabel} text={emptyLabel}
selected={!selectedEntity} selected={shouldSelectEmptyOption === true}
hovered={isSelectedSelectNoneButton} hovered={isSelectedSelectNoneButton}
/> />
) )

View File

@ -35,7 +35,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
onEntitySelected, onEntitySelected,
relationObjectNameSingular, relationObjectNameSingular,
relationPickerScopeId = 'relation-picker', relationPickerScopeId = 'relation-picker',
selectedEntity,
selectedRelationRecordIds, selectedRelationRecordIds,
dropdownPlacement, dropdownPlacement,
}: SingleEntitySelectMenuItemsWithSearchProps) => { }: SingleEntitySelectMenuItemsWithSearchProps) => {
@ -71,11 +70,11 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
entitiesToSelect={entities.entitiesToSelect} entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading} loading={entities.loading}
selectedEntity={ selectedEntity={
selectedEntity ?? entities.selectedEntities.length === 1
(entities.selectedEntities.length === 1
? entities.selectedEntities[0] ? entities.selectedEntities[0]
: undefined) : undefined
} }
shouldSelectEmptyOption={selectedRelationRecordIds?.length === 0}
hotkeyScope={relationPickerScopeId} hotkeyScope={relationPickerScopeId}
onCreate={onCreateWithInput} onCreate={onCreateWithInput}
isFiltered={!!relationPickerSearchFilter} isFiltered={!!relationPickerSearchFilter}

View File

@ -37,7 +37,7 @@ export const sanitizeRecordInput = ({
(field) => field.name === relationIdFieldName, (field) => field.name === relationIdFieldName,
); );
return relationIdFieldMetadataItem && fieldValue?.id return relationIdFieldMetadataItem
? [relationIdFieldName, fieldValue?.id ?? null] ? [relationIdFieldName, fieldValue?.id ?? null]
: undefined; : undefined;
} }

View File

@ -100,7 +100,6 @@ type SettingsDataModelFieldSettingsFormCardProps = {
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>; } & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
display: grid;
flex: 1 1 100%; flex: 1 1 100%;
`; `;

View File

@ -2,6 +2,8 @@ import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar'; import { ViewBar } from '@/views/components/ViewBar';
@ -20,36 +22,54 @@ export const SignInBackgroundMockContainer = () => {
const recordIndexId = 'sign-up-mock-record-table-id'; const recordIndexId = 'sign-up-mock-record-table-id';
const viewBarId = 'companies-mock'; const viewBarId = 'companies-mock';
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
return ( return (
<StyledContainer> <StyledContainer>
<ViewComponentInstanceContext.Provider value={{ instanceId: viewBarId }}> <RecordIndexRootPropsContext.Provider
<ContextStoreComponentInstanceContext.Provider value={{
value={{ recordIndexId,
instanceId: recordIndexId, objectNamePlural,
}} objectNameSingular,
objectMetadataItem,
onIndexRecordsLoaded: () => {},
indexIdentifierUrl: () => '',
onCreateRecord: () => {},
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
> >
<ActionMenuComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }} value={{
instanceId: recordIndexId,
}}
> >
<ViewBar <ActionMenuComponentInstanceContext.Provider
viewBarId={viewBarId} value={{ instanceId: recordIndexId }}
onCurrentViewChange={async () => {}} >
optionsDropdownButton={<></>} <ViewBar
/> viewBarId={viewBarId}
<SignInBackgroundMockContainerEffect onCurrentViewChange={() => {}}
objectNamePlural={objectNamePlural} optionsDropdownButton={<></>}
recordTableId={recordIndexId} />
viewId={viewBarId} <SignInBackgroundMockContainerEffect
/> objectNamePlural={objectNamePlural}
<RecordTableWithWrappers recordTableId={recordIndexId}
objectNameSingular={objectNameSingular} viewId={viewBarId}
recordTableId={recordIndexId} />
viewBarId={viewBarId} <RecordTableWithWrappers
updateRecordMutation={() => {}} objectNameSingular={objectNameSingular}
/> recordTableId={recordIndexId}
</ActionMenuComponentInstanceContext.Provider> viewBarId={viewBarId}
</ContextStoreComponentInstanceContext.Provider> updateRecordMutation={() => {}}
</ViewComponentInstanceContext.Provider> />
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</RecordIndexRootPropsContext.Provider>
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -26,6 +26,7 @@ const StyledControlContainer = styled.div<{
color: ${({ disabled, theme }) => color: ${({ disabled, theme }) =>
disabled ? theme.font.color.tertiary : theme.font.color.primary}; disabled ? theme.font.color.tertiary : theme.font.color.primary};
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
text-align: left;
`; `;
const StyledIconChevronDown = styled(IconChevronDown)<{ const StyledIconChevronDown = styled(IconChevronDown)<{

View File

@ -126,7 +126,6 @@ const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
} }
& .react-datepicker__month-dropdown { & .react-datepicker__month-dropdown {
left: ${({ theme }) => theme.spacing(2)}; left: ${({ theme }) => theme.spacing(2)};
width: 160px;
height: 260px; height: 260px;
} }

View File

@ -22,9 +22,9 @@ export type NavigationDrawerProps = {
title?: string; title?: string;
}; };
const StyledAnimatedContainer = styled(motion.div)` const StyledAnimatedContainer = styled(motion.div)<{ isSettings?: boolean }>`
max-height: 100vh; max-height: 100vh;
overflow: hidden; overflow: ${({ isSettings }) => (isSettings ? 'visible' : 'hidden')};
`; `;
const StyledContainer = styled.div<{ const StyledContainer = styled.div<{
@ -50,11 +50,12 @@ const StyledContainer = styled.div<{
padding-right: 20px; padding-right: 20px;
} }
`; `;
const StyledItemsContainer = styled.div`
const StyledItemsContainer = styled.div<{ isSettings?: boolean }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: auto; margin-bottom: auto;
overflow: hidden; overflow: ${({ isSettings }) => (isSettings ? 'visible' : 'hidden')};
flex: 1; flex: 1;
`; `;
@ -102,6 +103,7 @@ export const NavigationDrawer = ({
transition={{ transition={{
duration: theme.animation.duration.normal, duration: theme.animation.duration.normal,
}} }}
isSettings={isSettingsDrawer}
> >
<StyledContainer <StyledContainer
isSettings={isSettingsDrawer} isSettings={isSettingsDrawer}
@ -118,7 +120,9 @@ export const NavigationDrawer = ({
showCollapseButton={isHovered} showCollapseButton={isHovered}
/> />
)} )}
<StyledItemsContainer>{children}</StyledItemsContainer> <StyledItemsContainer isSettings={isSettingsDrawer}>
{children}
</StyledItemsContainer>
{footer} {footer}
</StyledContainer> </StyledContainer>
</StyledAnimatedContainer> </StyledAnimatedContainer>

View File

@ -1,13 +1,12 @@
import { fireEvent, renderHook } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { fireEvent, render, renderHook } from '@testing-library/react';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { import {
ClickOutsideMode, ClickOutsideMode,
useListenClickOutside, useListenClickOutside,
useListenClickOutsideByClassName,
} from '../useListenClickOutside'; } from '../useListenClickOutside';
const containerRef = React.createRef<HTMLDivElement>(); const containerRef = React.createRef<HTMLDivElement>();
@ -77,59 +76,3 @@ describe('useListenClickOutside', () => {
expect(callback).toHaveBeenCalled(); expect(callback).toHaveBeenCalled();
}); });
}); });
describe('useListenClickOutsideByClassName', () => {
it('should trigger the callback when clicking outside the specified class names', () => {
const callback = jest.fn();
const { container } = render(
<div>
<div className="wont-trigger other-class">Inside</div>
<div className="will-trigger">Outside</div>
</div>,
);
renderHook(() =>
useListenClickOutsideByClassName({
classNames: ['wont-trigger'],
callback,
}),
);
act(() => {
const notClickableElement = container.querySelector('.will-trigger');
if (isDefined(notClickableElement)) {
fireEvent.mouseDown(notClickableElement);
fireEvent.click(notClickableElement);
}
});
expect(callback).toHaveBeenCalled();
});
it('should not trigger the callback when clicking inside the specified class names', () => {
const callback = jest.fn();
const { container } = render(
<div>
<div className="wont-trigger other-class">Inside</div>
<div className="will-trigger">Outside</div>
</div>,
);
renderHook(() =>
useListenClickOutsideByClassName({
classNames: ['wont-trigger'],
callback,
}),
);
act(() => {
const notClickableElement = container.querySelector('.wont-trigger');
if (isDefined(notClickableElement)) {
fireEvent.mouseDown(notClickableElement);
fireEvent.click(notClickableElement);
}
});
expect(callback).not.toHaveBeenCalled();
});
});

View File

@ -138,58 +138,3 @@ export const useListenClickOutside = <T extends Element>({
} }
}, [refs, callback, mode, enabled, isMouseDownInside]); }, [refs, callback, mode, enabled, isMouseDownInside]);
}; };
export const useListenClickOutsideByClassName = ({
classNames,
excludeClassNames,
callback,
}: {
classNames: string[];
excludeClassNames?: string[];
callback: () => void;
}) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (!(event.target instanceof Node)) return;
const clickedElement = event.target as HTMLElement;
let isClickedInside = false;
let isClickedOnExcluded = false;
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
isClickedInside = classNames.some((className) =>
currentClassList.contains(className),
);
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
if (isClickedInside || isClickedOnExcluded) {
break;
}
currentElement = currentElement.parentElement;
}
if (!isClickedInside && !isClickedOnExcluded) {
callback();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchend', handleClickOutside, {
capture: true,
});
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchend', handleClickOutside, {
capture: true,
});
};
}, [callback, classNames, excludeClassNames]);
};

View File

@ -10,6 +10,7 @@ export enum ClickOutsideMode {
export type ClickOutsideListenerProps<T extends Element> = { export type ClickOutsideListenerProps<T extends Element> = {
refs: Array<React.RefObject<T>>; refs: Array<React.RefObject<T>>;
excludeClassNames?: string[];
callback: (event: MouseEvent | TouchEvent) => void; callback: (event: MouseEvent | TouchEvent) => void;
mode?: ClickOutsideMode; mode?: ClickOutsideMode;
listenerId: string; listenerId: string;
@ -18,6 +19,7 @@ export type ClickOutsideListenerProps<T extends Element> = {
export const useListenClickOutsideV2 = <T extends Element>({ export const useListenClickOutsideV2 = <T extends Element>({
refs, refs,
excludeClassNames,
callback, callback,
mode = ClickOutsideMode.compareHTMLRef, mode = ClickOutsideMode.compareHTMLRef,
listenerId, listenerId,
@ -106,11 +108,34 @@ export const useListenClickOutsideV2 = <T extends Element>({
.getValue(); .getValue();
if (mode === ClickOutsideMode.compareHTMLRef) { if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedElement = event.target as HTMLElement;
let isClickedOnExcluded = false;
let currentElement: HTMLElement | null = clickedElement;
while (currentElement) {
const currentClassList = currentElement.classList;
isClickedOnExcluded =
excludeClassNames?.some((className) =>
currentClassList.contains(className),
) ?? false;
if (isClickedOnExcluded) {
break;
}
currentElement = currentElement.parentElement;
}
const clickedOnAtLeastOneRef = refs const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current) .filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node)); .some((ref) => ref.current?.contains(event.target as Node));
if (!clickedOnAtLeastOneRef && !isMouseDownInside) { if (
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
) {
callback(event); callback(event);
} }
} }
@ -151,7 +176,13 @@ export const useListenClickOutsideV2 = <T extends Element>({
} }
} }
}, },
[mode, refs, callback, getClickOutsideListenerIsMouseDownInsideState], [
getClickOutsideListenerIsMouseDownInsideState,
mode,
refs,
excludeClassNames,
callback,
],
); );
useEffect(() => { useEffect(() => {

View File

@ -87,9 +87,9 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
} }
// Here we might instead want to get view from unsaved filters ? // Here we might instead want to get view from unsaved filters ?
const view = await getViewFromCache(currentViewId); const sourceView = await getViewFromCache(currentViewId);
if (!isDefined(view)) { if (!isDefined(sourceView)) {
return; return;
} }
@ -97,23 +97,23 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const newView = await createOneRecord({ const newView = await createOneRecord({
id: id ?? v4(), id: id ?? v4(),
name: name ?? view.name, name: name ?? sourceView.name,
icon: icon ?? view.icon, icon: icon ?? sourceView.icon,
key: null, key: null,
kanbanFieldMetadataId: kanbanFieldMetadataId:
kanbanFieldMetadataId ?? view.kanbanFieldMetadataId, kanbanFieldMetadataId ?? sourceView.kanbanFieldMetadataId,
type: type ?? view.type, type: type ?? sourceView.type,
objectMetadataId: view.objectMetadataId, objectMetadataId: sourceView.objectMetadataId,
}); });
if (isUndefinedOrNull(newView)) { if (isUndefinedOrNull(newView)) {
throw new Error('Failed to create view'); throw new Error('Failed to create view');
} }
await createViewFieldRecords(view.viewFields, newView); await createViewFieldRecords(sourceView.viewFields, newView);
if (type === ViewType.Kanban) { if (type === ViewType.Kanban) {
if (!isNonEmptyArray(view.viewGroups)) { if (!isNonEmptyArray(sourceView.viewGroups)) {
if (!isDefined(kanbanFieldMetadataId)) { if (!isDefined(kanbanFieldMetadataId)) {
throw new Error('Kanban view must have a kanban field'); throw new Error('Kanban view must have a kanban field');
} }
@ -144,22 +144,24 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
await createViewGroupRecords(viewGroupsToCreate, newView); await createViewGroupRecords(viewGroupsToCreate, newView);
} else { } else {
await createViewGroupRecords(view.viewGroups, newView); await createViewGroupRecords(sourceView.viewGroups, newView);
} }
} }
if (shouldCopyFiltersAndSorts === true) { if (shouldCopyFiltersAndSorts === true) {
const sourceViewCombinedFilterGroups = getViewFilterGroupsCombined( const sourceViewCombinedFilterGroups = getViewFilterGroupsCombined(
view.id, sourceView.id,
); );
const sourceViewCombinedFilters = getViewFiltersCombined(view.id); const sourceViewCombinedFilters = getViewFiltersCombined(
const sourceViewCombinedSorts = getViewSortsCombined(view.id); sourceView.id,
);
const sourceViewCombinedSorts = getViewSortsCombined(sourceView.id);
await createViewSortRecords(sourceViewCombinedSorts, view); await createViewSortRecords(sourceViewCombinedSorts, newView);
await createViewFilterRecords(sourceViewCombinedFilters, view); await createViewFilterRecords(sourceViewCombinedFilters, newView);
await createViewFilterGroupRecords( await createViewFilterGroupRecords(
sourceViewCombinedFilterGroups, sourceViewCombinedFilterGroups,
view, newView,
); );
} }

View File

@ -113,6 +113,33 @@ export const graphqlMocks = {
}, },
}); });
}), }),
graphql.query('FindManyViews', ({ variables }) => {
const objectMetadataId = variables.filter?.objectMetadataId?.eq;
const viewType = variables.filter?.type?.eq;
return HttpResponse.json({
data: {
views: {
edges: mockedViewsData
.filter(
(view) =>
view?.objectMetadataId === objectMetadataId &&
view?.type === viewType,
)
.map((view) => ({
node: view,
cursor: null,
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
});
}),
graphql.query('SearchWorkspaceMembers', () => { graphql.query('SearchWorkspaceMembers', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {

View File

@ -25,7 +25,8 @@ export const mockedViewsData = [
createdAt: '2021-09-01T00:00:00.000Z', createdAt: '2021-09-01T00:00:00.000Z',
updatedAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z',
isCompact: false, isCompact: false,
viewFilterGroups: [],
viewGroups: [],
__typename: 'View', __typename: 'View',
}, },
{ {
@ -40,6 +41,8 @@ export const mockedViewsData = [
createdAt: '2021-09-01T00:00:00.000Z', createdAt: '2021-09-01T00:00:00.000Z',
updatedAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z',
isCompact: false, isCompact: false,
viewFilterGroups: [],
viewGroups: [],
__typename: 'View', __typename: 'View',
}, },
{ {
@ -54,6 +57,8 @@ export const mockedViewsData = [
createdAt: '2021-09-01T00:00:00.000Z', createdAt: '2021-09-01T00:00:00.000Z',
updatedAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z',
isCompact: false, isCompact: false,
viewFilterGroups: [],
viewGroups: [],
__typename: 'View', __typename: 'View',
}, },
{ {
@ -68,6 +73,8 @@ export const mockedViewsData = [
createdAt: '2021-09-01T00:00:00.000Z', createdAt: '2021-09-01T00:00:00.000Z',
updatedAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z',
isCompact: false, isCompact: false,
viewFilterGroups: [],
viewGroups: [],
__typename: 'View', __typename: 'View',
}, },
]; ];

View File

@ -1,14 +1,13 @@
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { ObjectLiteral, Repository } from 'typeorm';
import chalk from 'chalk'; import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@Command({ @Command({
name: 'upgrade-0.32:copy-webhook-operation-into-operations', name: 'upgrade-0.32:copy-webhook-operation-into-operations',

View File

@ -79,11 +79,10 @@ export class SimplifySearchVectorExpressionCommand extends ActiveWorkspacesComma
fieldsUsedForSearch = SEARCH_FIELDS_FOR_OPPORTUNITY; fieldsUsedForSearch = SEARCH_FIELDS_FOR_OPPORTUNITY;
break; break;
} }
default: { }
throw new Error(
`search vector has unexpected standardId: ${searchVectorField.standardId}`, if (fieldsUsedForSearch.length === 0) {
); continue;
}
} }
await this.searchService.updateSearchVector( await this.searchService.updateSearchVector(

View File

@ -4,6 +4,7 @@ import { Command } from 'nest-commander';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { CopyWebhookOperationIntoOperationsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command';
import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression'; import { SimplifySearchVectorExpressionCommand } from 'src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
@ -25,6 +26,7 @@ export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner {
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand, private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand,
private readonly simplifySearchVectorExpressionCommand: SimplifySearchVectorExpressionCommand, private readonly simplifySearchVectorExpressionCommand: SimplifySearchVectorExpressionCommand,
private readonly copyWebhookOperationIntoOperationsCommand: CopyWebhookOperationIntoOperationsCommand,
) { ) {
super(workspaceRepository); super(workspaceRepository);
} }
@ -54,5 +56,11 @@ export class UpgradeTo0_32Command extends ActiveWorkspacesCommandRunner {
options, options,
workspaceIds, workspaceIds,
); );
await this.copyWebhookOperationIntoOperationsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
} }
} }

View File

@ -88,12 +88,12 @@ export class GraphqlQuerySearchResolverService
qb.where( qb.where(
searchTerms === '' searchTerms === ''
? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL` ? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL`
: `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', :searchTerms)`,
searchTerms === '' ? {} : { searchTerms }, searchTerms === '' ? {} : { searchTerms },
).orWhere( ).orWhere(
searchTermsOr === '' searchTermsOr === ''
? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL` ? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL`
: `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTermsOr)`, : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery('simple', :searchTermsOr)`,
searchTermsOr === '' ? {} : { searchTermsOr }, searchTermsOr === '' ? {} : { searchTermsOr },
); );
}), }),

View File

@ -25,7 +25,6 @@ const ATTACHMENT_GQL_FIELDS = `
updatedAt updatedAt
deletedAt deletedAt
authorId authorId
activityId
taskId taskId
noteId noteId
personId personId
@ -70,7 +69,6 @@ describe('attachments resolvers (integration)', () => {
expect(attachment).toHaveProperty('updatedAt'); expect(attachment).toHaveProperty('updatedAt');
expect(attachment).toHaveProperty('deletedAt'); expect(attachment).toHaveProperty('deletedAt');
expect(attachment).toHaveProperty('authorId'); expect(attachment).toHaveProperty('authorId');
expect(attachment).toHaveProperty('activityId');
expect(attachment).toHaveProperty('taskId'); expect(attachment).toHaveProperty('taskId');
expect(attachment).toHaveProperty('noteId'); expect(attachment).toHaveProperty('noteId');
expect(attachment).toHaveProperty('personId'); expect(attachment).toHaveProperty('personId');
@ -104,7 +102,6 @@ describe('attachments resolvers (integration)', () => {
expect(createdAttachment).toHaveProperty('updatedAt'); expect(createdAttachment).toHaveProperty('updatedAt');
expect(createdAttachment).toHaveProperty('deletedAt'); expect(createdAttachment).toHaveProperty('deletedAt');
expect(createdAttachment).toHaveProperty('authorId'); expect(createdAttachment).toHaveProperty('authorId');
expect(createdAttachment).toHaveProperty('activityId');
expect(createdAttachment).toHaveProperty('taskId'); expect(createdAttachment).toHaveProperty('taskId');
expect(createdAttachment).toHaveProperty('noteId'); expect(createdAttachment).toHaveProperty('noteId');
expect(createdAttachment).toHaveProperty('personId'); expect(createdAttachment).toHaveProperty('personId');
@ -137,7 +134,6 @@ describe('attachments resolvers (integration)', () => {
expect(attachments).toHaveProperty('updatedAt'); expect(attachments).toHaveProperty('updatedAt');
expect(attachments).toHaveProperty('deletedAt'); expect(attachments).toHaveProperty('deletedAt');
expect(attachments).toHaveProperty('authorId'); expect(attachments).toHaveProperty('authorId');
expect(attachments).toHaveProperty('activityId');
expect(attachments).toHaveProperty('taskId'); expect(attachments).toHaveProperty('taskId');
expect(attachments).toHaveProperty('noteId'); expect(attachments).toHaveProperty('noteId');
expect(attachments).toHaveProperty('personId'); expect(attachments).toHaveProperty('personId');
@ -169,7 +165,6 @@ describe('attachments resolvers (integration)', () => {
expect(attachment).toHaveProperty('updatedAt'); expect(attachment).toHaveProperty('updatedAt');
expect(attachment).toHaveProperty('deletedAt'); expect(attachment).toHaveProperty('deletedAt');
expect(attachment).toHaveProperty('authorId'); expect(attachment).toHaveProperty('authorId');
expect(attachment).toHaveProperty('activityId');
expect(attachment).toHaveProperty('taskId'); expect(attachment).toHaveProperty('taskId');
expect(attachment).toHaveProperty('noteId'); expect(attachment).toHaveProperty('noteId');
expect(attachment).toHaveProperty('personId'); expect(attachment).toHaveProperty('personId');

View File

@ -14,8 +14,8 @@ import { generateRecordName } from 'test/integration/utils/generate-record-name'
const NOTE_TARGET_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; const NOTE_TARGET_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987';
const NOTE_TARGET_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; const NOTE_TARGET_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988';
const NOTE_TARGET_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; const NOTE_TARGET_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989';
const PERSON_1_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; const PERSON_1_ID = 'f875ff46-b881-41ba-973a-b9cd5345e8f0';
const PERSON_2_ID = '777a8457-eb2d-40ac-a707-331b615b6989'; const PERSON_2_ID = '1fe0f78c-8c59-4ce6-ae02-56571331b252';
const NOTE_TARGET_GQL_FIELDS = ` const NOTE_TARGET_GQL_FIELDS = `
id id
createdAt createdAt

View File

@ -14,8 +14,8 @@ import { generateRecordName } from 'test/integration/utils/generate-record-name'
const TASK_TARGET_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; const TASK_TARGET_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987';
const TASK_TARGET_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; const TASK_TARGET_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988';
const TASK_TARGET_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; const TASK_TARGET_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989';
const PERSON_1_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; const PERSON_1_ID = 'f875ff46-b881-41ba-973a-b9cd5345e8f0';
const PERSON_2_ID = '777a8457-eb2d-40ac-a707-331b615b6989'; const PERSON_2_ID = '1fe0f78c-8c59-4ce6-ae02-56571331b252';
const TASK_TARGET_GQL_FIELDS = ` const TASK_TARGET_GQL_FIELDS = `
id id
createdAt createdAt

View File

@ -42,7 +42,7 @@ const StyledTag = styled.h3<{
padding: 0 ${spacing2}; padding: 0 ${spacing2};
border: ${({ variant, theme }) => border: ${({ variant, theme }) =>
variant === 'outline' || variant === 'border' variant === 'outline' || variant === 'border'
? `1px ${variant === 'border' ? 'solid' : 'dash'} ${theme.border.color.strong}` ? `1px ${variant === 'border' ? 'solid' : 'dashed'} ${theme.border.color.strong}`
: ''}; : ''};
gap: ${spacing1}; gap: ${spacing1};