Clean and re-organize post table refactoring (#1000)

* Clean and re-organize post table refactoring

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-30 18:26:32 -07:00
committed by GitHub
parent 86a2d67efd
commit ade5e52e55
336 changed files with 638 additions and 2757 deletions

View File

@ -0,0 +1,23 @@
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
export const INITIAL_HOTKEYS_SCOPES: string[] = [AppHotkeyScope.App];
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
AppHotkeyScope.CommandMenu,
AppHotkeyScope.App,
];
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
commandMenu: true,
goto: false,
};
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
scope: AppHotkeyScope.App,
customScopes: {
commandMenu: true,
goto: true,
},
};

View File

@ -0,0 +1,103 @@
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../../states/internal/internalHotkeysEnabledScopesState';
export function useHotkeyScopes() {
const { disableScope, enableScope } = useHotkeysContext();
const disableAllHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async () => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
for (const enabledScope of enabledScopes) {
disableScope(enabledScope);
}
set(internalHotkeysEnabledScopesState, []);
};
},
[disableScope],
);
const enableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToEnable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
if (!enabledScopes.includes(scopeToEnable)) {
enableScope(scopeToEnable);
set(internalHotkeysEnabledScopesState, [
...enabledScopes,
scopeToEnable,
]);
}
};
},
[enableScope],
);
const disableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToDisable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopeToRemoveIndex = enabledScopes.findIndex(
(scope) => scope === scopeToDisable,
);
if (scopeToRemoveIndex > -1) {
disableScope(scopeToDisable);
enabledScopes.splice(scopeToRemoveIndex);
set(internalHotkeysEnabledScopesState, enabledScopes);
}
};
},
[disableScope],
);
const setHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async (scopesToSet: string[]) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopesToDisable = enabledScopes.filter(
(enabledScope) => !scopesToSet.includes(enabledScope),
);
const scopesToEnable = scopesToSet.filter(
(scopeToSet) => !enabledScopes.includes(scopeToSet),
);
for (const scopeToDisable of scopesToDisable) {
disableScope(scopeToDisable);
}
for (const scopeToEnable of scopesToEnable) {
enableScope(scopeToEnable);
}
set(internalHotkeysEnabledScopesState, scopesToSet);
};
},
[disableScope, enableScope],
);
return {
disableAllHotkeyScopes,
enableHotkeyScope,
disableHotkeyScope,
setHotkeyScopes,
};
}

View File

@ -0,0 +1,25 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useNavigate } from 'react-router-dom';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
export function useGoToHotkeys(key: Keys, location: string) {
const navigate = useNavigate();
useSequenceHotkeys(
'g',
key,
() => {
navigate(location);
},
AppHotkeyScope.Goto,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[navigate],
);
}

View File

@ -0,0 +1,43 @@
import { useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
import { useSetHotkeyScope } from './useSetHotkeyScope';
export function usePreviousHotkeyScope() {
const [previousHotkeyScope, setPreviousHotkeyScope] =
useState<HotkeyScope | null>();
const setHotkeyScope = useSetHotkeyScope();
function goBackToPreviousHotkeyScope() {
if (previousHotkeyScope) {
setHotkeyScope(
previousHotkeyScope.scope,
previousHotkeyScope.customScopes,
);
}
}
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
({ snapshot }) =>
(scope: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
setHotkeyScope(scope, customScopes);
setPreviousHotkeyScope(currentHotkeyScope);
},
[setPreviousHotkeyScope],
);
return {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
};
}

View File

@ -0,0 +1,64 @@
import { Hotkey } from 'react-hotkeys-hook/dist/types';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
const DEBUG_HOTKEY_SCOPE = true;
export function useScopedHotkeyCallback() {
return useRecoilCallback(
({ snapshot }) =>
({
callback,
hotkeysEvent,
keyboardEvent,
scope,
preventDefault = true,
}: {
keyboardEvent: KeyboardEvent;
hotkeysEvent: Hotkey;
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
scope: string;
preventDefault?: boolean;
}) => {
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.valueOrThrow();
if (!currentHotkeyScopes.includes(scope)) {
if (DEBUG_HOTKEY_SCOPE) {
console.debug(
`%cI can't call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: gray; ',
);
}
return;
}
if (DEBUG_HOTKEY_SCOPE) {
console.debug(
`%cI can call hotkey (${
hotkeysEvent.keys
}) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
', ',
)}]`,
'color: green;',
);
}
if (preventDefault) {
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
keyboardEvent.stopImmediatePropagation();
}
return callback(keyboardEvent, hotkeysEvent);
},
[],
);
}

View File

@ -0,0 +1,52 @@
import { useHotkeys } from 'react-hotkeys-hook';
import {
HotkeyCallback,
Keys,
Options,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useScopedHotkeys(
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: OptionsOrDependencyArray,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback();
return useHotkeys(
keys,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
},
scope,
preventDefault: !!options.preventDefault,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
dependencies,
);
}

View File

@ -0,0 +1,76 @@
import { Options, useHotkeys } from 'react-hotkeys-hook';
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useSequenceHotkeys(
firstKey: Keys,
secondKey: Keys,
sequenceCallback: () => void,
scope: string,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
deps: any[] = [],
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback();
useHotkeys(
firstKey,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
setPendingHotkey(firstKey);
},
scope,
preventDefault: !!options.preventDefault,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[setPendingHotkey, scope],
);
useHotkeys(
secondKey,
(keyboardEvent, hotkeysEvent) => {
callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (pendingHotkey !== firstKey) {
return;
}
setPendingHotkey(null);
if (!!options.preventDefault) {
keyboardEvent.stopImmediatePropagation();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
sequenceCallback();
},
scope,
preventDefault: false,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[pendingHotkey, setPendingHotkey, scope, ...deps],
);
}

View File

@ -0,0 +1,76 @@
import { useRecoilCallback } from 'recoil';
import { isDefined } from '~/utils/isDefined';
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
function isCustomScopesEqual(
customScopesA: CustomHotkeyScopes | undefined,
customScopesB: CustomHotkeyScopes | undefined,
) {
return (
customScopesA?.commandMenu === customScopesB?.commandMenu &&
customScopesA?.goto === customScopesB?.goto
);
}
export function useSetHotkeyScope() {
return useRecoilCallback(
({ snapshot, set }) =>
async (hotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = await snapshot.getPromise(
currentHotkeyScopeState,
);
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
if (!isDefined(customScopes)) {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES,
)
) {
return;
}
} else {
if (
isCustomScopesEqual(
currentHotkeyScope?.customScopes,
customScopes,
)
) {
return;
}
}
}
const newHotkeyScope: HotkeyScope = {
scope: hotkeyScopeToSet,
customScopes: {
commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false,
},
};
const scopesToSet: string[] = [];
if (newHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (newHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(newHotkeyScope.scope);
set(internalHotkeysEnabledScopesState, scopesToSet);
},
[],
);
}

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants';
import { HotkeyScope } from '../../types/HotkeyScope';
export const currentHotkeyScopeState = atom<HotkeyScope>({
key: 'currentHotkeyScopeState',
default: INITIAL_HOTKEYS_SCOPE,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const internalHotkeysEnabledScopesState = atom<string[]>({
key: 'internalHotkeysEnabledScopesState',
default: [],
});

View File

@ -0,0 +1,7 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { atom } from 'recoil';
export const pendingHotkeyState = atom<Keys | null>({
key: 'pendingHotkeyState',
default: null,
});

View File

@ -0,0 +1,5 @@
export enum AppHotkeyScope {
App = 'app',
Goto = 'goto',
CommandMenu = 'command-menu',
}

View File

@ -0,0 +1,4 @@
export type CustomHotkeyScopes = {
goto?: boolean;
commandMenu?: boolean;
};

View File

@ -0,0 +1,6 @@
import { CustomHotkeyScopes } from './CustomHotkeyScope';
export type HotkeyScope = {
scope: string;
customScopes?: CustomHotkeyScopes;
};

View File

@ -0,0 +1,57 @@
export function isNonTextWritingKey(key: string) {
const nonTextWritingKeys = [
'Enter',
'Tab',
'Shift',
'Escape',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Delete',
'Backspace',
'F1',
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'F10',
'F11',
'F12',
'Meta',
'Alt',
'Control',
'CapsLock',
'NumLock',
'ScrollLock',
'Pause',
'Insert',
'Home',
'PageUp',
'Delete',
'End',
'PageDown',
'ContextMenu',
'PrintScreen',
'BrowserBack',
'BrowserForward',
'BrowserRefresh',
'BrowserStop',
'BrowserSearch',
'BrowserFavorites',
'BrowserHome',
'VolumeMute',
'VolumeDown',
'VolumeUp',
'MediaTrackNext',
'MediaTrackPrevious',
'MediaStop',
'MediaPlayPause',
];
return nonTextWritingKeys.includes(key);
}

View File

@ -0,0 +1,8 @@
import { HotkeyScope } from '../types/HotkeyScope';
export function isSameHotkeyScope(
hotkeyScope1: HotkeyScope | undefined | null,
hotkeyScope2: HotkeyScope | undefined | null,
): boolean {
return JSON.stringify(hotkeyScope1) === JSON.stringify(hotkeyScope2);
}