mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge branch 'main' into archive_table
This commit is contained in:
		
						commit
						8e1bf96d04
					
				@ -78,7 +78,7 @@
 | 
				
			|||||||
    "lodash.clonedeep": "4.5.0",
 | 
					    "lodash.clonedeep": "4.5.0",
 | 
				
			||||||
    "msw": "0.42.0",
 | 
					    "msw": "0.42.0",
 | 
				
			||||||
    "pkginfo": "^0.4.1",
 | 
					    "pkginfo": "^0.4.1",
 | 
				
			||||||
    "plausible-tracker": "^0.3.5",
 | 
					    "plausible-tracker": "0.3.7",
 | 
				
			||||||
    "prettier": "2.6.2",
 | 
					    "prettier": "2.6.2",
 | 
				
			||||||
    "prop-types": "15.8.1",
 | 
					    "prop-types": "15.8.1",
 | 
				
			||||||
    "react": "17.0.2",
 | 
					    "react": "17.0.2",
 | 
				
			||||||
@ -94,11 +94,11 @@
 | 
				
			|||||||
    "swr": "1.3.0",
 | 
					    "swr": "1.3.0",
 | 
				
			||||||
    "tss-react": "3.7.0",
 | 
					    "tss-react": "3.7.0",
 | 
				
			||||||
    "typescript": "4.7.3",
 | 
					    "typescript": "4.7.3",
 | 
				
			||||||
    "vite": "2.9.9",
 | 
					    "vite": "2.9.10",
 | 
				
			||||||
    "vite-plugin-env-compatible": "^1.1.1",
 | 
					    "vite-plugin-env-compatible": "^1.1.1",
 | 
				
			||||||
    "vite-plugin-svgr": "2.1.0",
 | 
					    "vite-plugin-svgr": "2.1.0",
 | 
				
			||||||
    "vite-tsconfig-paths": "3.5.0",
 | 
					    "vite-tsconfig-paths": "3.5.0",
 | 
				
			||||||
    "vitest": "0.14.0",
 | 
					    "vitest": "0.14.1",
 | 
				
			||||||
    "whatwg-fetch": "^3.6.2"
 | 
					    "whatwg-fetch": "^3.6.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "jest": {
 | 
					  "jest": {
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ interface ITableSearchProps {
 | 
				
			|||||||
export const TableSearch: FC<ITableSearchProps> = ({
 | 
					export const TableSearch: FC<ITableSearchProps> = ({
 | 
				
			||||||
    initialValue,
 | 
					    initialValue,
 | 
				
			||||||
    onChange = () => {},
 | 
					    onChange = () => {},
 | 
				
			||||||
    placeholder = 'Search',
 | 
					    placeholder,
 | 
				
			||||||
    hasFilters,
 | 
					    hasFilters,
 | 
				
			||||||
    getSearchContext,
 | 
					    getSearchContext,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
@ -30,7 +30,7 @@ export const TableSearch: FC<ITableSearchProps> = ({
 | 
				
			|||||||
        <TableSearchField
 | 
					        <TableSearchField
 | 
				
			||||||
            value={searchInputState!}
 | 
					            value={searchInputState!}
 | 
				
			||||||
            onChange={onSearchChange}
 | 
					            onChange={onSearchChange}
 | 
				
			||||||
            placeholder={`${placeholder}…`}
 | 
					            placeholder={placeholder}
 | 
				
			||||||
            hasFilters={hasFilters}
 | 
					            hasFilters={hasFilters}
 | 
				
			||||||
            getSearchContext={getSearchContext}
 | 
					            getSearchContext={getSearchContext}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,18 @@
 | 
				
			|||||||
 | 
					import { useRef, useState } from 'react';
 | 
				
			||||||
import { IconButton, InputBase, Tooltip } from '@mui/material';
 | 
					import { IconButton, InputBase, Tooltip } from '@mui/material';
 | 
				
			||||||
import { Search, Close } from '@mui/icons-material';
 | 
					import { Search, Close } from '@mui/icons-material';
 | 
				
			||||||
import classnames from 'classnames';
 | 
					import classnames from 'classnames';
 | 
				
			||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
					import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
				
			||||||
import { useStyles } from './TableSearchField.styles';
 | 
					import { useStyles } from './TableSearchField.styles';
 | 
				
			||||||
import { TableSearchFieldSuggestions } from './TableSearchFieldSuggestions/TableSearchFieldSuggestions';
 | 
					import { TableSearchFieldSuggestions } from './TableSearchFieldSuggestions/TableSearchFieldSuggestions';
 | 
				
			||||||
import { useState } from 'react';
 | 
					 | 
				
			||||||
import { IGetSearchContextOutput } from 'hooks/useSearch';
 | 
					import { IGetSearchContextOutput } from 'hooks/useSearch';
 | 
				
			||||||
 | 
					import { useKeyboardShortcut } from 'hooks/useKeyboardShortcut';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ITableSearchFieldProps {
 | 
					interface ITableSearchFieldProps {
 | 
				
			||||||
    value: string;
 | 
					    value: string;
 | 
				
			||||||
    onChange: (value: string) => void;
 | 
					    onChange: (value: string) => void;
 | 
				
			||||||
    className?: string;
 | 
					    className?: string;
 | 
				
			||||||
    placeholder: string;
 | 
					    placeholder?: string;
 | 
				
			||||||
    hasFilters?: boolean;
 | 
					    hasFilters?: boolean;
 | 
				
			||||||
    getSearchContext?: () => IGetSearchContextOutput;
 | 
					    getSearchContext?: () => IGetSearchContextOutput;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -20,12 +21,26 @@ export const TableSearchField = ({
 | 
				
			|||||||
    value = '',
 | 
					    value = '',
 | 
				
			||||||
    onChange,
 | 
					    onChange,
 | 
				
			||||||
    className,
 | 
					    className,
 | 
				
			||||||
    placeholder,
 | 
					    placeholder: customPlaceholder,
 | 
				
			||||||
    hasFilters,
 | 
					    hasFilters,
 | 
				
			||||||
    getSearchContext,
 | 
					    getSearchContext,
 | 
				
			||||||
}: ITableSearchFieldProps) => {
 | 
					}: ITableSearchFieldProps) => {
 | 
				
			||||||
 | 
					    const ref = useRef<HTMLInputElement>();
 | 
				
			||||||
    const { classes: styles } = useStyles();
 | 
					    const { classes: styles } = useStyles();
 | 
				
			||||||
    const [showSuggestions, setShowSuggestions] = useState(false);
 | 
					    const [showSuggestions, setShowSuggestions] = useState(false);
 | 
				
			||||||
 | 
					    const hotkey = useKeyboardShortcut(
 | 
				
			||||||
 | 
					        { modifiers: ['ctrl'], key: 'k', preventDefault: true },
 | 
				
			||||||
 | 
					        () => {
 | 
				
			||||||
 | 
					            ref.current?.focus();
 | 
				
			||||||
 | 
					            setShowSuggestions(true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    useKeyboardShortcut({ key: 'Escape' }, () => {
 | 
				
			||||||
 | 
					        if (document.activeElement === ref.current) {
 | 
				
			||||||
 | 
					            setShowSuggestions(suggestions => !suggestions);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const placeholder = `${customPlaceholder ?? 'Search'} (${hotkey})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <div className={styles.container}>
 | 
					        <div className={styles.container}>
 | 
				
			||||||
@ -40,6 +55,7 @@ export const TableSearchField = ({
 | 
				
			|||||||
                    className={classnames(styles.searchIcon, 'search-icon')}
 | 
					                    className={classnames(styles.searchIcon, 'search-icon')}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                <InputBase
 | 
					                <InputBase
 | 
				
			||||||
 | 
					                    inputRef={ref}
 | 
				
			||||||
                    placeholder={placeholder}
 | 
					                    placeholder={placeholder}
 | 
				
			||||||
                    classes={{
 | 
					                    classes={{
 | 
				
			||||||
                        root: classnames(styles.inputRoot, 'input-container'),
 | 
					                        root: classnames(styles.inputRoot, 'input-container'),
 | 
				
			||||||
@ -64,6 +80,7 @@ export const TableSearchField = ({
 | 
				
			|||||||
                                    size="small"
 | 
					                                    size="small"
 | 
				
			||||||
                                    onClick={() => {
 | 
					                                    onClick={() => {
 | 
				
			||||||
                                        onChange('');
 | 
					                                        onChange('');
 | 
				
			||||||
 | 
					                                        ref.current?.focus();
 | 
				
			||||||
                                    }}
 | 
					                                    }}
 | 
				
			||||||
                                >
 | 
					                                >
 | 
				
			||||||
                                    <Close className={styles.clearIcon} />
 | 
					                                    <Close className={styles.clearIcon} />
 | 
				
			||||||
 | 
				
			|||||||
@ -53,13 +53,13 @@ exports[`renders an empty list correctly 1`] = `
 | 
				
			|||||||
                    onClick={[Function]}
 | 
					                    onClick={[Function]}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <input
 | 
					                    <input
 | 
				
			||||||
                      aria-label="Search…"
 | 
					                      aria-label="Search (Ctrl+K)"
 | 
				
			||||||
                      className="MuiInputBase-input mui-j79lc6-MuiInputBase-input"
 | 
					                      className="MuiInputBase-input mui-j79lc6-MuiInputBase-input"
 | 
				
			||||||
                      onAnimationStart={[Function]}
 | 
					                      onAnimationStart={[Function]}
 | 
				
			||||||
                      onBlur={[Function]}
 | 
					                      onBlur={[Function]}
 | 
				
			||||||
                      onChange={[Function]}
 | 
					                      onChange={[Function]}
 | 
				
			||||||
                      onFocus={[Function]}
 | 
					                      onFocus={[Function]}
 | 
				
			||||||
                      placeholder="Search…"
 | 
					                      placeholder="Search (Ctrl+K)"
 | 
				
			||||||
                      type="text"
 | 
					                      type="text"
 | 
				
			||||||
                      value=""
 | 
					                      value=""
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								frontend/src/hooks/useIsAppleDevice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/hooks/useIsAppleDevice.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useIsAppleDevice = () => {
 | 
				
			||||||
 | 
					    const [isAppleDevice, setIsAppleDevice] = useState<boolean>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const platform =
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                navigator as unknown as {
 | 
				
			||||||
 | 
					                    userAgentData: { platform: string };
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )?.userAgentData?.platform ||
 | 
				
			||||||
 | 
					            navigator?.platform ||
 | 
				
			||||||
 | 
					            'unknown';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setIsAppleDevice(
 | 
				
			||||||
 | 
					            platform.toLowerCase().includes('mac') ||
 | 
				
			||||||
 | 
					                platform === 'iPhone' ||
 | 
				
			||||||
 | 
					                platform === 'iPad'
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return isAppleDevice;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										76
									
								
								frontend/src/hooks/useKeyboardShortcut.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								frontend/src/hooks/useKeyboardShortcut.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					import { useEffect, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { useIsAppleDevice } from './useIsAppleDevice';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useKeyboardShortcut = (
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        key,
 | 
				
			||||||
 | 
					        modifiers = [],
 | 
				
			||||||
 | 
					        preventDefault = false,
 | 
				
			||||||
 | 
					    }: {
 | 
				
			||||||
 | 
					        key: string;
 | 
				
			||||||
 | 
					        modifiers?: Array<'ctrl' | 'alt' | 'shift'>;
 | 
				
			||||||
 | 
					        preventDefault?: boolean;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    callback: () => void
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					    const isAppleDevice = useIsAppleDevice();
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const onKeyDown = (event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					            if (key !== event.key) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (modifiers.includes('ctrl')) {
 | 
				
			||||||
 | 
					                if (isAppleDevice) {
 | 
				
			||||||
 | 
					                    if (!event.metaKey) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    if (!event.ctrlKey) {
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (modifiers.includes('alt') && !event.altKey) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (modifiers.includes('shift') && !event.shiftKey) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (preventDefault) {
 | 
				
			||||||
 | 
					                event.preventDefault();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            callback();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.addEventListener('keydown', onKeyDown);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return () => {
 | 
				
			||||||
 | 
					            window.removeEventListener('keydown', onKeyDown);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }, [isAppleDevice, key, modifiers, preventDefault, callback]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const formattedModifiers = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            modifiers.map(
 | 
				
			||||||
 | 
					                modifier =>
 | 
				
			||||||
 | 
					                    ({
 | 
				
			||||||
 | 
					                        ctrl: isAppleDevice ? '⌘' : 'Ctrl',
 | 
				
			||||||
 | 
					                        alt: 'Alt',
 | 
				
			||||||
 | 
					                        shift: 'Shift',
 | 
				
			||||||
 | 
					                    }[modifier])
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        [isAppleDevice, modifiers]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hotkeyDescription = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                ...formattedModifiers,
 | 
				
			||||||
 | 
					                `${key[0].toUpperCase()}${key.slice(1)}`,
 | 
				
			||||||
 | 
					            ].join('+'),
 | 
				
			||||||
 | 
					        [formattedModifiers, key]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return hotkeyDescription;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -5041,10 +5041,10 @@ pkginfo@^0.4.1:
 | 
				
			|||||||
  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
 | 
					  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
 | 
				
			||||||
  integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
 | 
					  integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
plausible-tracker@^0.3.5:
 | 
					plausible-tracker@0.3.7:
 | 
				
			||||||
  version "0.3.5"
 | 
					  version "0.3.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.5.tgz#49c09a7eb727f1d5c859c3fc8072837b13ee9b85"
 | 
					  resolved "https://registry.yarnpkg.com/plausible-tracker/-/plausible-tracker-0.3.7.tgz#e93d241a1c46bf70f05317a6f177feff6c4865d5"
 | 
				
			||||||
  integrity sha512-6c6VPdPtI9KmIsfr8zLBViIDMt369eeaNA1J8JrAmAtrpSkeJWvjwcJ+cLn7gVJn5AtQWC8NgSEee2d/5RNytA==
 | 
					  integrity sha512-yhM3VJekqMIEDbvx/PqratMyHpF4T/skTO4owzxSo3YMB/ZVmAYwh9c2iKRuJnkE3b4NwsMMW9b0Vw5VD5Gpyw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
postcss@^8.4.13:
 | 
					postcss@^8.4.13:
 | 
				
			||||||
  version "8.4.13"
 | 
					  version "8.4.13"
 | 
				
			||||||
@ -6028,7 +6028,19 @@ vite-tsconfig-paths@3.5.0:
 | 
				
			|||||||
    recrawl-sync "^2.0.3"
 | 
					    recrawl-sync "^2.0.3"
 | 
				
			||||||
    tsconfig-paths "^4.0.0"
 | 
					    tsconfig-paths "^4.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vite@2.9.9, vite@^2.9.9:
 | 
					vite@2.9.10:
 | 
				
			||||||
 | 
					  version "2.9.10"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.10.tgz#f574d96655622c2e0fbc662edd0ed199c60fe91a"
 | 
				
			||||||
 | 
					  integrity sha512-TwZRuSMYjpTurLqXspct+HZE7ONiW9d+wSWgvADGxhDPPyoIcNywY+RX4ng+QpK30DCa1l/oZgi2PLZDibhzbQ==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    esbuild "^0.14.27"
 | 
				
			||||||
 | 
					    postcss "^8.4.13"
 | 
				
			||||||
 | 
					    resolve "^1.22.0"
 | 
				
			||||||
 | 
					    rollup "^2.59.0"
 | 
				
			||||||
 | 
					  optionalDependencies:
 | 
				
			||||||
 | 
					    fsevents "~2.3.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					vite@^2.9.9:
 | 
				
			||||||
  version "2.9.9"
 | 
					  version "2.9.9"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.9.tgz#8b558987db5e60fedec2f4b003b73164cb081c5e"
 | 
					  resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.9.tgz#8b558987db5e60fedec2f4b003b73164cb081c5e"
 | 
				
			||||||
  integrity sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==
 | 
					  integrity sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==
 | 
				
			||||||
@ -6040,10 +6052,10 @@ vite@2.9.9, vite@^2.9.9:
 | 
				
			|||||||
  optionalDependencies:
 | 
					  optionalDependencies:
 | 
				
			||||||
    fsevents "~2.3.2"
 | 
					    fsevents "~2.3.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vitest@0.14.0:
 | 
					vitest@0.14.1:
 | 
				
			||||||
  version "0.14.0"
 | 
					  version "0.14.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.14.0.tgz#8fe92608d5379972b8f5a2424187fc4a3e453cd5"
 | 
					  resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.14.1.tgz#f2fd8b31abdbbadb9ee895f8fde35a068ea2a5f5"
 | 
				
			||||||
  integrity sha512-3Ns7c6TindS6OmweY4dU7sWT7g/YJBenwsMdUMEO/oS81bzoCEaA58vlg/9U1WRjrNFwQYsuyISehAvCKwoMZQ==
 | 
					  integrity sha512-2UUm6jYgkwh7Y3VKSRR8OuaNCm+iA5LPDnal7jyITN39maZK9L+JVxqjtQ39PSFo5Fl3/BgaJvER6GGHX9JLxg==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    "@types/chai" "^4.3.1"
 | 
					    "@types/chai" "^4.3.1"
 | 
				
			||||||
    "@types/chai-subset" "^1.3.3"
 | 
					    "@types/chai-subset" "^1.3.3"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user