refactor(infra): directory structure (#4615)

This commit is contained in:
Joooye_34
2023-10-18 23:30:08 +08:00
committed by GitHub
parent 814d552be8
commit bed9310519
1150 changed files with 539 additions and 584 deletions
@@ -0,0 +1,37 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { createStore } from 'jotai';
import { describe, expect, test } from 'vitest';
import {
pageSettingFamily,
pageSettingsAtom,
recentPageIdsBaseAtom,
} from '../index';
describe('page mode atom', () => {
test('basic', () => {
const store = createStore();
const page0SettingAtom = pageSettingFamily('page0');
store.set(page0SettingAtom, {
mode: 'page',
});
expect(store.get(pageSettingsAtom)).toEqual({
page0: {
mode: 'page',
},
});
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page0']);
const page1SettingAtom = pageSettingFamily('page1');
store.set(page1SettingAtom, {
mode: 'edgeless',
});
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page1', 'page0']);
});
});
@@ -0,0 +1,4 @@
import { atom } from 'jotai';
import type { SessionContextValue } from 'next-auth/react';
export const sessionAtom = atom<SessionContextValue<true> | null>(null);
@@ -0,0 +1,5 @@
import { atom } from 'jotai/vanilla';
export const appHeaderAtom = atom<HTMLDivElement | null>(null);
export const mainContainerAtom = atom<HTMLDivElement | null>(null);
+25
View File
@@ -0,0 +1,25 @@
import { atom, useAtom } from 'jotai';
import { useCallback } from 'react';
export type OnceSignedInEvent = () => void;
export const onceSignedInEventsAtom = atom<OnceSignedInEvent[]>([]);
export const setOnceSignedInEventAtom = atom(
null,
(get, set, event: OnceSignedInEvent) => {
set(onceSignedInEventsAtom, [...get(onceSignedInEventsAtom), event]);
}
);
export const useOnceSignedInEvents = () => {
const [events, setEvents] = useAtom(onceSignedInEventsAtom);
return useCallback(async () => {
try {
await Promise.all(events.map(event => event()));
} catch (err) {
console.error('Error executing one of the events:', err);
}
setEvents([]);
}, [events, setEvents]);
};
+91
View File
@@ -0,0 +1,91 @@
// 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;
// should show download client tips
downloadClientTip: boolean;
};
const guidePrimitiveAtom = atomWithStorage<Guide>('helper-guide', {
quickSearchTips: true,
changeLog: true,
onBoarding: true,
downloadClientTip: 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,
}));
}
);
export const guideOnboardingAtom = atom<
Guide['onBoarding'],
[open: boolean],
void
>(
get => {
return get(guidePrimitiveAtom).onBoarding;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
onBoarding: open,
}));
}
);
export const guideDownloadClientTipAtom = atom<
Guide['downloadClientTip'],
[open: boolean],
void
>(
get => {
if (environment.isDesktop) {
return false;
}
return get(guidePrimitiveAtom).downloadClientTip;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
downloadClientTip: open,
}));
}
);
+106
View File
@@ -0,0 +1,106 @@
import { useAtom } from 'jotai';
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useNavigate } from 'react-router-dom';
import { router } from '../router';
export type History = {
stack: string[];
current: number;
skip: boolean;
};
export const MAX_HISTORY = 50;
const historyBaseAtom = atomWithStorage<History>(
'router-history',
{
stack: [],
current: 0,
skip: false,
},
createJSONStorage(() => sessionStorage)
);
historyBaseAtom.onMount = set => {
const unsubscribe = router.subscribe(state => {
set(prev => {
const url = state.location.pathname;
// if stack top is the same as current, skip
if (prev.stack[prev.current] === url) {
return prev;
}
if (prev.skip) {
return {
stack: [...prev.stack],
current: prev.current,
skip: false,
};
} else {
if (prev.current < prev.stack.length - 1) {
const newStack = prev.stack.slice(0, prev.current);
newStack.push(url);
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
} else {
const newStack = [...prev.stack, url];
if (newStack.length > MAX_HISTORY) {
newStack.shift();
}
return {
stack: newStack,
current: newStack.length - 1,
skip: false,
};
}
}
});
});
return () => {
unsubscribe();
};
};
export function useHistoryAtom() {
const navigate = useNavigate();
const [base, setBase] = useAtom(historyBaseAtom);
return [
base,
useCallback(
(forward: boolean) => {
setBase(prev => {
if (forward) {
const target = Math.min(prev.stack.length - 1, prev.current + 1);
const url = prev.stack[target];
navigate(url);
return {
...prev,
current: target,
skip: true,
};
} else {
const target = Math.max(0, prev.current - 1);
const url = prev.stack[target];
navigate(url);
return {
...prev,
current: target,
skip: true,
};
}
});
},
[setBase, navigate]
),
] as const;
}
+105
View File
@@ -0,0 +1,105 @@
import type { PrimitiveAtom } from 'jotai';
import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils';
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
import type { AuthProps } from '../components/affine/auth';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
import type { SettingProps } from '../components/affine/setting-modal';
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false);
export const openSignOutModalAtom = atom(false);
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
open: boolean;
};
export const openSettingModalAtom = atom<SettingAtom>({
activeTab: 'appearance',
workspaceId: null,
open: false,
});
export type AuthAtom = {
openModal: boolean;
state: AuthProps['state'];
email?: string;
emailType?: AuthProps['emailType'];
};
export const authAtom = atom<AuthAtom>({
openModal: false,
state: 'signIn',
email: '',
emailType: 'changeEmail',
});
export const openDisableCloudAlertModalAtom = atom(false);
type PageMode = 'page' | 'edgeless';
type PageLocalSetting = {
mode: PageMode;
};
const pageSettingsBaseAtom = atomWithStorage(
'pageSettings',
{} as Record<string, PageLocalSetting>
);
// readonly atom by design
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
'recentPageSettings',
[]
);
const defaultPageSetting = {
mode: 'page',
} satisfies PageLocalSetting;
export const pageSettingFamily: AtomFamily<
string,
PrimitiveAtom<PageLocalSetting>
> = atomFamily((pageId: string) =>
atom(
get =>
get(pageSettingsBaseAtom)[pageId] ?? {
...defaultPageSetting,
},
(get, set, patch) => {
// fixme: this does not work when page reload,
// since atomWithStorage is async
set(recentPageIdsBaseAtom, ids => {
// pick 3 recent page ids
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
});
const prevSetting = {
...defaultPageSetting,
...get(pageSettingsBaseAtom)[pageId],
};
set(pageSettingsBaseAtom, settings => ({
...settings,
[pageId]: {
...prevSetting,
...(typeof patch === 'function' ? patch(prevSetting) : patch),
},
}));
}
)
);
export const setPageModeAtom = atom(
void 0,
(_, set, pageId: string, mode: PageMode) => {
set(pageSettingFamily(pageId), { mode });
}
);
export type PageModeOption = 'all' | 'page' | 'edgeless';
export const allPageModeSelectAtom = atom<PageModeOption>('all');
export const openWorkspaceListModalAtom = atom(false);
+12
View File
@@ -0,0 +1,12 @@
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { atom } from 'jotai/vanilla';
import { pageSettingFamily } from './index';
export const currentModeAtom = atom<'page' | 'edgeless'>(get => {
const pageId = get(currentPageIdAtom);
if (!pageId) {
return 'page';
}
return get(pageSettingFamily(pageId)).mode;
});
@@ -0,0 +1,81 @@
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export type DateFormats =
| 'MM/dd/YYYY'
| 'dd/MM/YYYY'
| 'YYYY-MM-dd'
| 'YYYY.MM.dd'
| 'YYYY/MM/dd'
| 'dd-MMM-YYYY'
| 'dd MMMM YYYY';
export type AppSetting = {
clientBorder: boolean;
fullWidthLayout: boolean;
windowFrameStyle: 'frameless' | 'NativeTitleBar';
fontStyle: FontFamily;
dateFormat: DateFormats;
startWeekOnMonday: boolean;
enableBlurBackground: boolean;
enableNoisyBackground: boolean;
autoCheckUpdate: boolean;
autoDownloadUpdate: boolean;
};
export const windowFrameStyleOptions: AppSetting['windowFrameStyle'][] = [
'frameless',
'NativeTitleBar',
];
export const dateFormatOptions: DateFormats[] = [
'MM/dd/YYYY',
'dd/MM/YYYY',
'YYYY-MM-dd',
'YYYY.MM.dd',
'YYYY/MM/dd',
'dd-MMM-YYYY',
'dd MMMM YYYY',
];
export type FontFamily = 'Sans' | 'Serif' | 'Mono';
export const fontStyleOptions = [
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
] satisfies {
key: FontFamily;
value: string;
}[];
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
clientBorder: environment.isDesktop && globalThis.platform !== 'win32',
fullWidthLayout: false,
windowFrameStyle: 'frameless',
fontStyle: 'Sans',
dateFormat: dateFormatOptions[0],
startWeekOnMonday: false,
enableBlurBackground: true,
enableNoisyBackground: true,
autoCheckUpdate: true,
autoDownloadUpdate: true,
});
type SetStateAction<Value> = Value | ((prev: Value) => Value);
export const appSettingAtom = atom<
AppSetting,
[SetStateAction<Partial<AppSetting>>],
void
>(
get => get(appSettingBaseAtom),
(get, set, apply) => {
const prev = get(appSettingBaseAtom);
const next = typeof apply === 'function' ? apply(prev) : apply;
set(appSettingBaseAtom, { ...prev, ...next });
}
);
export const useAppSetting = () => {
return useAtom(appSettingAtom);
};
@@ -0,0 +1,13 @@
import { atom } from 'jotai';
export type TrashModal = {
open: boolean;
pageId: string;
pageTitle: string;
};
export const trashModalAtom = atom<TrashModal>({
open: false,
pageId: '',
pageTitle: '',
});