refactor: guide atoms (#2196)

This commit is contained in:
Himself65 2023-04-28 15:59:59 -05:00 committed by GitHub
parent 31cccafb40
commit 1031fbc7ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 85 additions and 231 deletions

View File

@ -1,29 +0,0 @@
import { atomWithStorage } from 'jotai/utils';
export type Visibility = Record<string, boolean>;
const DEFAULT_VALUE = '0.0.0';
//atomWithStorage always uses initial value when first render
//https://github.com/pmndrs/jotai/discussions/1737
function getInitialValue() {
if (typeof window !== 'undefined') {
const storedValue = window.localStorage.getItem('lastVersion');
if (storedValue) {
return JSON.parse(storedValue);
}
}
return DEFAULT_VALUE;
}
export const lastVersionAtom = atomWithStorage(
'lastVersion',
getInitialValue()
);
export const guideHiddenAtom = atomWithStorage<Visibility>('guideHidden', {});
export const guideHiddenUntilNextUpdateAtom = atomWithStorage<Visibility>(
'guideHiddenUntilNextUpdate',
{}
);

View File

@ -0,0 +1,54 @@
// these atoms cannot be moved to @affine/jotai since they use atoms from @affine/component
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export type Guide = {
// should show quick search tips
quickSearchTips: boolean;
// should show change log
changeLog: boolean;
// should show recording tips
onBoarding: boolean;
};
const guidePrimitiveAtom = atomWithStorage<Guide>('helper-guide', {
quickSearchTips: true,
changeLog: true,
onBoarding: true,
});
export const guideQuickSearchTipsAtom = atom<
Guide['quickSearchTips'],
[open: boolean],
void
>(
get => {
const open = get(appSidebarOpenAtom);
const guide = get(guidePrimitiveAtom);
// only show the tips when the sidebar is closed
return guide.quickSearchTips && open === false;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
quickSearchTips: open,
}));
}
);
export const guideChangeLogAtom = atom<
Guide['changeLog'],
[open: boolean],
void
>(
get => {
return get(guidePrimitiveAtom).changeLog;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
changeLog: open,
}));
}
);

View File

@ -5,11 +5,6 @@ import { useTranslation } from '@affine/i18n';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
useUpdateTipsOnVersionChange,
} from '../../../hooks/use-is-first-load';
import { SidebarSwitchIcon } from './icons';
import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
@ -18,18 +13,14 @@ type SidebarSwitchProps = {
};
// fixme: the following code is not correct, SSR will fail because hydrate will not match the client side render
// in `StyledSidebarSwitch` component
// in `StyledSidebarSwitch` a component
export const SidebarSwitch = ({
visible = true,
tooltipContent,
...props
}: SidebarSwitchProps) => {
useUpdateTipsOnVersionChange();
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const [tooltipVisible, setTooltipVisible] = useState(false);
const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
useGuideHiddenUntilNextUpdate();
const { t } = useTranslation();
const checkIsMac = () => {
const env = getEnvironment();
@ -59,22 +50,7 @@ export const SidebarSwitch = ({
onClick={useCallback(() => {
setOpen(open => !open);
setTooltipVisible(false);
if (!guideHiddenUntilNextUpdate['quickSearchTips']) {
setGuideHiddenUntilNextUpdate({
...guideHiddenUntilNextUpdate,
quickSearchTips: true,
});
setTimeout(() => {
setGuideHidden({ ...guideHidden, quickSearchTips: false });
}, 200);
}
}, [
guideHidden,
guideHiddenUntilNextUpdate,
setGuideHidden,
setGuideHiddenUntilNextUpdate,
setOpen,
])}
}, [setOpen])}
onMouseEnter={useCallback(() => {
setTooltipVisible(true);
}, [])}

View File

@ -4,12 +4,12 @@ import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useRef } from 'react';
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
import { useGuideHidden } from '../../../hooks/use-is-first-load';
import { guideQuickSearchTipsAtom } from '../../../atoms/guide';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
@ -39,7 +39,6 @@ export const WorkspaceHeader = forwardRef<
);
assertExists(pageMeta);
const title = pageMeta.title;
const [isTipsHidden, setTipsHidden] = useGuideHidden();
const isMac = () => {
const env = getEnvironment();
return env.isBrowser && env.isMacOs;
@ -47,14 +46,18 @@ export const WorkspaceHeader = forwardRef<
const popperRef: PopperProps['popperRef'] = useRef(null);
const [showQuickSearchTips, setShowQuickSearchTips] = useAtom(
guideQuickSearchTipsAtom
);
useElementResizeEffect(
useAtomValue(currentEditorAtom),
useCallback(() => {
if (isTipsHidden.quickSearchTips || !popperRef.current) {
if (showQuickSearchTips || !popperRef.current) {
return;
}
popperRef.current.update();
}, [isTipsHidden.quickSearchTips])
}, [showQuickSearchTips])
);
const TipsContent = (
@ -77,9 +80,7 @@ export const WorkspaceHeader = forwardRef<
</div>
<StyledQuickSearchTipButton
data-testid="quick-search-got-it"
onClick={() =>
setTipsHidden({ ...isTipsHidden, quickSearchTips: true })
}
onClick={() => setShowQuickSearchTips(false)}
>
Got it
</StyledQuickSearchTipButton>
@ -107,7 +108,7 @@ export const WorkspaceHeader = forwardRef<
content={TipsContent}
placement="bottom"
popperRef={popperRef}
open={!isTipsHidden.quickSearchTips}
open={showQuickSearchTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>

View File

@ -1,30 +1,15 @@
import ChangeLogComponent from '@affine/component/changeLog';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
} from '../../../../hooks/use-is-first-load';
import { guideChangeLogAtom } from '../../../../atoms/guide';
export const ChangeLog = () => {
const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
useGuideHiddenUntilNextUpdate();
const [showChangeLogTips, setShowChangeLogTips] = useAtom(guideChangeLogAtom);
const onCloseWhatsNew = useCallback(() => {
setTimeout(() => {
setGuideHiddenUntilNextUpdate({
...guideHiddenUntilNextUpdate,
changeLog: true,
});
setGuideHidden({ ...guideHidden, changeLog: true });
}, 300);
}, [
guideHidden,
guideHiddenUntilNextUpdate,
setGuideHidden,
setGuideHiddenUntilNextUpdate,
]);
if (guideHiddenUntilNextUpdate.changeLog) {
setShowChangeLogTips(false);
}, [setShowChangeLogTips]);
if (!showChangeLogTips) {
return <></>;
}
return <ChangeLogComponent onCloseWhatsNew={onCloseWhatsNew} />;

View File

@ -34,12 +34,6 @@ import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../current/use-current-workspace';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
useLastVersion,
useTipsDisplayStatus,
} from '../use-is-first-load';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
@ -278,47 +272,3 @@ describe('useRecentlyViewed', () => {
]);
});
});
describe('useIsFirstLoad', () => {
test('useLastVersion', async () => {
const lastVersion = renderHook(() => useLastVersion());
const setLastVersion = lastVersion.result.current[1];
expect(lastVersion.result.current[0]).toEqual('0.0.0');
setLastVersion('testVersion');
lastVersion.rerender();
expect(lastVersion.result.current[0]).toEqual('testVersion');
});
test('useGuideHidden', async () => {
const guideHidden = renderHook(() => useGuideHidden());
const setGuideHidden = guideHidden.result.current[1];
expect(guideHidden.result.current[0]).toEqual({});
setGuideHidden({ test: true });
guideHidden.rerender();
expect(guideHidden.result.current[0]).toEqual({ test: true });
});
test('useGuideHiddenUntilNextUpdate', async () => {
const guideHiddenUntilNextUpdate = renderHook(() =>
useGuideHiddenUntilNextUpdate()
);
const setGuideHiddenUntilNextUpdate =
guideHiddenUntilNextUpdate.result.current[1];
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({});
setGuideHiddenUntilNextUpdate({ test: true });
guideHiddenUntilNextUpdate.rerender();
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({
test: true,
});
});
test('useTipsDisplayStatus', async () => {
const tipsDisplayStatus = renderHook(() => useTipsDisplayStatus());
expect(tipsDisplayStatus.result.current).toEqual({
quickSearchTips: {
permanentlyHidden: true,
hiddenUntilNextUpdate: true,
},
changeLog: {
permanentlyHidden: true,
hiddenUntilNextUpdate: true,
},
});
});
});

View File

@ -1,74 +0,0 @@
import { config } from '@affine/env';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useEffect } from 'react';
import {
guideHiddenAtom,
guideHiddenUntilNextUpdateAtom,
lastVersionAtom,
} from '../atoms/first-load';
export function useLastVersion() {
return useAtom(lastVersionAtom);
}
export function useGuideHidden() {
return useAtom(guideHiddenAtom);
}
export function useGuideHiddenUntilNextUpdate() {
return useAtom(guideHiddenUntilNextUpdateAtom);
}
const TIPS = {
quickSearchTips: true,
changeLog: true,
};
export function useTipsDisplayStatus() {
const permanentlyHiddenTips = useAtomValue(guideHiddenAtom);
const hiddenUntilNextUpdateTips = useAtomValue(
guideHiddenUntilNextUpdateAtom
);
return {
quickSearchTips: {
permanentlyHidden: permanentlyHiddenTips.quickSearchTips || true,
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.quickSearchTips || true,
},
changeLog: {
permanentlyHidden: permanentlyHiddenTips.changeLog || true,
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.changeLog || true,
},
};
}
export function useUpdateTipsOnVersionChange() {
const [lastVersion, setLastVersion] = useLastVersion();
const currentVersion = config.gitVersion;
const tipsDisplayStatus = useTipsDisplayStatus();
const setPermanentlyHiddenTips = useSetAtom(guideHiddenAtom);
const setHiddenUntilNextUpdateTips = useSetAtom(
guideHiddenUntilNextUpdateAtom
);
useEffect(() => {
if (lastVersion !== currentVersion) {
setLastVersion(currentVersion);
const newHiddenUntilNextUpdateTips = { ...TIPS };
const newPermanentlyHiddenTips = { ...TIPS, changeLog: false };
Object.keys(tipsDisplayStatus).forEach(tipKey => {
newHiddenUntilNextUpdateTips[tipKey as keyof typeof TIPS] = false;
});
setHiddenUntilNextUpdateTips(newHiddenUntilNextUpdateTips);
setPermanentlyHiddenTips(newPermanentlyHiddenTips);
}
}, [
currentVersion,
lastVersion,
setLastVersion,
setPermanentlyHiddenTips,
setHiddenUntilNextUpdateTips,
tipsDisplayStatus,
]);
}

View File

@ -1,7 +1,8 @@
import { atomWithStorage } from 'jotai/utils';
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
export const appSidebarOpenAtom = atomWithStorage(
'app-sidebar-open',
APP_SIDEBAR_OPEN,
undefined as boolean | undefined
);
export const appSidebarWidthAtom = atomWithStorage(

View File

@ -21,7 +21,11 @@ import {
sidebarButtonStyle,
sidebarFloatMaskStyle,
} from './index.css';
import { appSidebarOpenAtom, appSidebarWidthAtom } from './index.jotai';
import {
APP_SIDEBAR_OPEN,
appSidebarOpenAtom,
appSidebarWidthAtom,
} from './index.jotai';
export { appSidebarOpenAtom };
@ -41,7 +45,10 @@ export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
}, [setOpen]);
useEffect(() => {
if (open === undefined) {
if (
open === undefined &&
localStorage.getItem(APP_SIDEBAR_OPEN) === null
) {
// give the initial value,
// so that the sidebar can be closed on mobile by default
const { matches } = window.matchMedia(

View File

@ -1,5 +1,3 @@
import { resolve } from 'node:path';
import type {
PlaywrightTestConfig,
PlaywrightWorkerOptions,
@ -44,18 +42,6 @@ const config: PlaywrightTestConfig = {
reporter: process.env.CI ? 'github' : 'list',
webServer: [
{
command: 'cargo run -p affine-cloud',
port: 3000,
timeout: 10 * 1000,
reuseExistingServer: true,
cwd: process.env.OCTOBASE_CWD ?? resolve(process.cwd(), 'apps', 'server'),
env: {
SIGN_KEY: 'test123',
RUST_LOG: 'debug',
JWST_DEV: '1',
},
},
{
// Intentionally not building the storybook, reminds you to run it by yourself.
command: 'yarn run start:storybook',

View File

@ -179,15 +179,13 @@ test('When opening the website for the first time, the first folding sidebar wil
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
// fixme: when first close, the tooltip will not show
await page.getByTestId('sliderBar-arrowButton-expand').click();
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar-inner');
await expect(sliderBarArea).not.toBeInViewport();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
});
test('After appearing once, it will not appear a second time', async ({
page,
}) => {
@ -196,8 +194,6 @@ test('After appearing once, it will not appear a second time', async ({
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
await page.getByTestId('sliderBar-arrowButton-expand').click();
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible();
await expect(quickSearchTips).toBeVisible();
@ -205,6 +201,7 @@ test('After appearing once, it will not appear a second time', async ({
await expect(quickSearchTips).not.toBeVisible();
await page.reload();
await page.waitForSelector('v-line');
await page.getByTestId('sliderBar-arrowButton-expand').click();
await page.getByTestId('app-sidebar-arrow-button-collapse').click();
await expect(quickSearchTips).not.toBeVisible();
});