We previously used classnames to exclude elements from the click outside listener. With this PR we can now use `data-click-outside-id` instead of `classNames` to target the elements we want to exclude from the click outside listener. We can also add `data-globally-prevent-click-outside` to a component to globally prevent triggering click outside listeners for other components. This attribute is especially useful for confirmation modals and snackbar items. Fixes #11785: https://github.com/user-attachments/assets/318baa7e-0f82-4e3a-a447-bf981328462d
268 lines
9.6 KiB
TypeScript
268 lines
9.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
matchPath,
|
|
useLocation,
|
|
useNavigate,
|
|
useParams,
|
|
} from 'react-router-dom';
|
|
import { useRecoilValue } from 'recoil';
|
|
|
|
import {
|
|
setSessionId,
|
|
useEventTracker,
|
|
} from '@/analytics/hooks/useEventTracker';
|
|
import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange';
|
|
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
|
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
|
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
|
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
|
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
|
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
|
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
|
|
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
|
import { RecordIndexHotkeyScope } from '@/object-record/record-index/types/RecordIndexHotkeyScope';
|
|
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
|
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
|
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
|
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
|
import { AppBasePath } from '@/types/AppBasePath';
|
|
import { AppPath } from '@/types/AppPath';
|
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
|
import { SettingsPath } from '@/types/SettingsPath';
|
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { AnalyticsType } from '~/generated/graphql';
|
|
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
|
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
|
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
|
// TODO: break down into smaller functions and / or hooks
|
|
// - moved usePageChangeEffectNavigateLocation into dedicated hook
|
|
export const PageChangeEffect = () => {
|
|
const navigate = useNavigate();
|
|
|
|
const [previousLocation, setPreviousLocation] = useState('');
|
|
|
|
const setHotkeyScope = useSetHotkeyScope();
|
|
|
|
const location = useLocation();
|
|
|
|
const pageChangeEffectNavigateLocation =
|
|
usePageChangeEffectNavigateLocation();
|
|
|
|
const eventTracker = useEventTracker();
|
|
|
|
const { initializeQueryParamState } = useInitializeQueryParamState();
|
|
|
|
//TODO: refactor useResetTableRowSelection hook to not throw when the argument `recordTableId` is an empty string
|
|
// - replace CoreObjectNamePlural.Person
|
|
const objectNamePlural =
|
|
useParams().objectNamePlural ?? CoreObjectNamePlural.Person;
|
|
|
|
const contextStoreCurrentViewId = useRecoilComponentValueV2(
|
|
contextStoreCurrentViewIdComponentState,
|
|
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
|
);
|
|
|
|
const contextStoreCurrentViewType = useRecoilComponentValueV2(
|
|
contextStoreCurrentViewTypeComponentState,
|
|
MAIN_CONTEXT_STORE_INSTANCE_ID,
|
|
);
|
|
|
|
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
|
objectNamePlural,
|
|
contextStoreCurrentViewId || '',
|
|
);
|
|
|
|
const resetTableSelections = useResetTableRowSelection(recordIndexId);
|
|
const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId);
|
|
const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId);
|
|
|
|
const { resetRecordSelection } = useRecordBoardSelection(recordIndexId);
|
|
const { deactivateBoardCard } = useActiveRecordBoardCard(recordIndexId);
|
|
const { unfocusBoardCard } = useFocusedRecordBoardCard(recordIndexId);
|
|
|
|
const { executeTasksOnAnyLocationChange } =
|
|
useExecuteTasksOnAnyLocationChange();
|
|
|
|
const { closeCommandMenu } = useCommandMenu();
|
|
|
|
useEffect(() => {
|
|
closeCommandMenu();
|
|
}, [location.pathname, closeCommandMenu]);
|
|
|
|
useEffect(() => {
|
|
if (!previousLocation || previousLocation !== location.pathname) {
|
|
setPreviousLocation(location.pathname);
|
|
executeTasksOnAnyLocationChange();
|
|
} else {
|
|
return;
|
|
}
|
|
}, [location, previousLocation, executeTasksOnAnyLocationChange]);
|
|
|
|
useEffect(() => {
|
|
initializeQueryParamState();
|
|
|
|
if (isDefined(pageChangeEffectNavigateLocation)) {
|
|
navigate(pageChangeEffectNavigateLocation);
|
|
}
|
|
}, [navigate, pageChangeEffectNavigateLocation, initializeQueryParamState]);
|
|
|
|
useEffect(() => {
|
|
const isLeavingRecordIndexPage = !!matchPath(
|
|
AppPath.RecordIndexPage,
|
|
previousLocation,
|
|
);
|
|
|
|
if (isLeavingRecordIndexPage) {
|
|
if (contextStoreCurrentViewType === ContextStoreViewType.Table) {
|
|
resetTableSelections();
|
|
unfocusRecordTableRow();
|
|
deactivateRecordTableRow();
|
|
}
|
|
if (contextStoreCurrentViewType === ContextStoreViewType.Kanban) {
|
|
resetRecordSelection();
|
|
deactivateBoardCard();
|
|
unfocusBoardCard();
|
|
}
|
|
}
|
|
}, [
|
|
previousLocation,
|
|
resetTableSelections,
|
|
unfocusRecordTableRow,
|
|
deactivateRecordTableRow,
|
|
contextStoreCurrentViewType,
|
|
resetRecordSelection,
|
|
deactivateBoardCard,
|
|
unfocusBoardCard,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
switch (true) {
|
|
case isMatchingLocation(location, AppPath.RecordIndexPage): {
|
|
setHotkeyScope(RecordIndexHotkeyScope.RecordIndex, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.RecordShowPage): {
|
|
setHotkeyScope(PageHotkeyScope.CompanyShowPage, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.OpportunitiesPage): {
|
|
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.TasksPage): {
|
|
setHotkeyScope(PageHotkeyScope.TaskPage, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
|
|
case isMatchingLocation(location, AppPath.SignInUp): {
|
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.Invite): {
|
|
setHotkeyScope(PageHotkeyScope.SignInUp);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.CreateProfile): {
|
|
setHotkeyScope(PageHotkeyScope.CreateProfile);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.CreateWorkspace): {
|
|
setHotkeyScope(PageHotkeyScope.CreateWorkspace);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.SyncEmails): {
|
|
setHotkeyScope(PageHotkeyScope.SyncEmail);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.InviteTeam): {
|
|
setHotkeyScope(PageHotkeyScope.InviteTeam);
|
|
break;
|
|
}
|
|
case isMatchingLocation(location, AppPath.PlanRequired): {
|
|
setHotkeyScope(PageHotkeyScope.PlanRequired);
|
|
break;
|
|
}
|
|
case isMatchingLocation(
|
|
location,
|
|
SettingsPath.ProfilePage,
|
|
AppBasePath.Settings,
|
|
): {
|
|
setHotkeyScope(PageHotkeyScope.ProfilePage, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
case isMatchingLocation(
|
|
location,
|
|
SettingsPath.Domain,
|
|
AppBasePath.Settings,
|
|
): {
|
|
setHotkeyScope(PageHotkeyScope.Settings, {
|
|
goto: false,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
case isMatchingLocation(
|
|
location,
|
|
SettingsPath.WorkspaceMembersPage,
|
|
AppBasePath.Settings,
|
|
): {
|
|
setHotkeyScope(PageHotkeyScope.WorkspaceMemberPage, {
|
|
goto: true,
|
|
keyboardShortcutMenu: true,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}, [location, setHotkeyScope]);
|
|
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
setSessionId();
|
|
eventTracker(AnalyticsType['PAGEVIEW'], {
|
|
name: getPageTitleFromPath(location.pathname),
|
|
properties: {
|
|
pathname: location.pathname,
|
|
locale: navigator.language,
|
|
userAgent: window.navigator.userAgent,
|
|
href: window.location.href,
|
|
referrer: document.referrer,
|
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
},
|
|
});
|
|
}, 500);
|
|
}, [eventTracker, location.pathname]);
|
|
|
|
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
|
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
|
|
|
|
useEffect(() => {
|
|
if (isCaptchaScriptLoaded && isCaptchaRequiredForPath(location.pathname)) {
|
|
requestFreshCaptchaToken();
|
|
}
|
|
}, [isCaptchaScriptLoaded, location.pathname, requestFreshCaptchaToken]);
|
|
|
|
return <></>;
|
|
};
|