mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-24 08:31:48 +03:00
fix: left sidebar style fixes (#3950)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
parent
e92d27549a
commit
d62935935f
@ -27,7 +27,6 @@ export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
|
@ -26,7 +26,6 @@ export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
|
@ -29,6 +29,7 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, [workspaceId]);
|
||||
return path;
|
||||
};
|
||||
|
@ -111,6 +111,7 @@ export const RootAppSidebar = ({
|
||||
if (isDesktop) {
|
||||
return window.events?.applicationMenu.onNewPageAction(onClickNewPage);
|
||||
}
|
||||
return;
|
||||
}, [onClickNewPage]);
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
|
||||
|
@ -15,7 +15,7 @@ export const updaterHandlers = {
|
||||
if (res) {
|
||||
const { updateInfo } = res;
|
||||
return {
|
||||
updateInfo,
|
||||
version: updateInfo.version,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -24,7 +24,7 @@ import { type PropsWithChildren, useState } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar',
|
||||
title: 'AFFiNE/AppSidebar',
|
||||
component: AppSidebar,
|
||||
} satisfies Meta;
|
||||
|
||||
|
60
apps/storybook/src/stories/app-updater-button.stories.tsx
Normal file
60
apps/storybook/src/stories/app-updater-button.stories.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
type AddPageButtonPureProps,
|
||||
AppUpdaterButtonPure,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/AppUpdaterButton',
|
||||
component: AppUpdaterButtonPure,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<typeof AppUpdaterButtonPure>;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '320px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#eee',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
|
||||
export const Default: StoryFn<AddPageButtonPureProps> = props => {
|
||||
return (
|
||||
<Container>
|
||||
<AppUpdaterButtonPure {...props} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Default.args = {
|
||||
appQuitting: false,
|
||||
updateReady: true,
|
||||
updateAvailable: {
|
||||
version: '1.0.0-beta.1',
|
||||
allowAutoUpdate: true,
|
||||
},
|
||||
downloadProgress: 42,
|
||||
currentChangelogUnread: true,
|
||||
};
|
||||
|
||||
export const Updated: StoryFn<AddPageButtonPureProps> = props => {
|
||||
return (
|
||||
<Container>
|
||||
<AppUpdaterButtonPure {...props} updateAvailable={null} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Updated.args = {
|
||||
currentChangelogUnread: true,
|
||||
};
|
3
packages/@types/env/__all.d.ts
vendored
3
packages/@types/env/__all.d.ts
vendored
@ -3,6 +3,7 @@ import type {
|
||||
DBHandlerManager,
|
||||
DebugHandlerManager,
|
||||
DialogHandlerManager,
|
||||
EventMap,
|
||||
ExportHandlerManager,
|
||||
UIHandlerManager,
|
||||
UnwrapManagerHandlerToClientSide,
|
||||
@ -24,7 +25,7 @@ declare global {
|
||||
updater: UnwrapManagerHandlerToClientSide<UpdaterHandlerManager>;
|
||||
workspace: UnwrapManagerHandlerToClientSide<WorkspaceHandlerManager>;
|
||||
};
|
||||
events: any;
|
||||
events: EventMap;
|
||||
}
|
||||
|
||||
interface WindowEventMap {
|
||||
|
@ -18,21 +18,23 @@ export const root = style({
|
||||
'&:hover': {
|
||||
background: 'var(--affine-white-60)',
|
||||
},
|
||||
'&[data-has-update="true"]:before': {
|
||||
'&[data-disabled="true"]': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&:after': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: '-3px',
|
||||
right: '-3px',
|
||||
top: '-2px',
|
||||
right: '-2px',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: 'var(--affine-primary-color)',
|
||||
borderRadius: '50%',
|
||||
zIndex: 1,
|
||||
opacity: 1,
|
||||
transition: '0.3s ease',
|
||||
transition: 'opacity 0.3s',
|
||||
},
|
||||
'&[data-disabled="true"]': {
|
||||
pointerEvents: 'none',
|
||||
'&:hover:after': {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
vars: {
|
||||
@ -42,7 +44,7 @@ export const root = style({
|
||||
|
||||
export const icon = style({
|
||||
marginRight: '18px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '24px',
|
||||
});
|
||||
|
||||
@ -90,7 +92,7 @@ export const installLabelNormal = style([
|
||||
{
|
||||
justifyContent: 'space-between',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
[`${root}:hover &, ${root}[data-updating=true] &`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
@ -102,7 +104,7 @@ export const installLabelHover = style([
|
||||
{
|
||||
display: 'none',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
[`${root}:hover &, ${root}[data-updating=true] &`]: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
@ -175,7 +177,7 @@ export const particles = style({
|
||||
pointerEvents: 'none',
|
||||
display: 'none',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
[`${root}:hover &, ${root}[data-updating=true] &`]: {
|
||||
display: 'block',
|
||||
},
|
||||
'&:before': {
|
||||
@ -224,8 +226,10 @@ export const halo = style({
|
||||
'radial-gradient(ellipse 30% 45% at bottom, rgba(30, 150, 235, 0.6), transparent)',
|
||||
},
|
||||
selectors: {
|
||||
'&:hover:before, &:hover:after': {
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
[`${root}:hover &:before, ${root}:hover &:after,
|
||||
${root}[data-updating=true] &:before, ${root}[data-updating=true] &:after`]:
|
||||
{
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { isBrowser } from '@affine/env/constant';
|
||||
import type { UpdateMeta } from '@toeverything/infra';
|
||||
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@ -8,7 +9,7 @@ function rpcToObservable<
|
||||
H extends () => Promise<T>,
|
||||
E extends (callback: (t: T) => void) => () => void,
|
||||
>(
|
||||
initialValue: T,
|
||||
initialValue: T | null,
|
||||
{
|
||||
event,
|
||||
handler,
|
||||
@ -18,8 +19,8 @@ function rpcToObservable<
|
||||
handler?: H;
|
||||
onSubscribe?: () => void;
|
||||
}
|
||||
) {
|
||||
return new Observable<T>(subscriber => {
|
||||
): Observable<T | null> {
|
||||
return new Observable<T | null>(subscriber => {
|
||||
subscriber.next(initialValue);
|
||||
onSubscribe?.();
|
||||
if (!isBrowser || !environment.isDesktop || !event) {
|
||||
@ -40,13 +41,13 @@ function rpcToObservable<
|
||||
}
|
||||
|
||||
export const updateReadyAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as any | null, {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateReady,
|
||||
});
|
||||
});
|
||||
|
||||
export const updateAvailableAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as any | null, {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateAvailable,
|
||||
onSubscribe: () => {
|
||||
window.apis?.updater.checkForUpdatesAndNotify().catch(err => {
|
||||
@ -56,8 +57,8 @@ export const updateAvailableAtom = atomWithObservable(() => {
|
||||
});
|
||||
});
|
||||
|
||||
export const downloadProgressAtom = atomWithObservable<number>(() => {
|
||||
return rpcToObservable(0, {
|
||||
export const downloadProgressAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as number | null, {
|
||||
event: window.events?.updater.onDownloadProgress,
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,17 @@ import {
|
||||
updateReadyAtom,
|
||||
} from './index.jotai';
|
||||
|
||||
interface AddPageButtonProps {
|
||||
export interface AddPageButtonPureProps {
|
||||
onClickUpdate: () => void;
|
||||
onDismissCurrentChangelog: () => void;
|
||||
currentChangelogUnread: boolean;
|
||||
updateReady: boolean;
|
||||
updateAvailable: {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
} | null;
|
||||
downloadProgress: number | null;
|
||||
appQuitting: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
@ -39,61 +49,19 @@ const currentChangelogUnreadAtom = atom(async get => {
|
||||
return false;
|
||||
});
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
export function AppUpdaterButtonPure({
|
||||
updateReady,
|
||||
onClickUpdate,
|
||||
onDismissCurrentChangelog,
|
||||
currentChangelogUnread,
|
||||
updateAvailable,
|
||||
downloadProgress,
|
||||
appQuitting,
|
||||
className,
|
||||
style,
|
||||
}: AddPageButtonPureProps) {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom);
|
||||
const [appQuitting, setAppQuitting] = useState(false);
|
||||
|
||||
const onDismissCurrentChangelog = useCallback(() => {
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
startTransition(() =>
|
||||
setChangelogCheckAtom(mapping => {
|
||||
return {
|
||||
...mapping,
|
||||
[currentVersion]: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [currentVersion, setChangelogCheckAtom]);
|
||||
const onClickUpdate = useCallback(() => {
|
||||
if (updateReady) {
|
||||
setAppQuitting(true);
|
||||
window.apis?.updater.quitAndInstall().catch(err => {
|
||||
// TODO: add error toast here
|
||||
console.error(err);
|
||||
});
|
||||
} else if (updateAvailable) {
|
||||
if (updateAvailable.allowAutoUpdate) {
|
||||
// wait for download to finish
|
||||
} else {
|
||||
window.open(
|
||||
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
} else if (currentChangelogUnread) {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
onDismissCurrentChangelog();
|
||||
} else {
|
||||
throw new Unreachable();
|
||||
}
|
||||
}, [
|
||||
currentChangelogUnread,
|
||||
currentVersion,
|
||||
onDismissCurrentChangelog,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
|
||||
if (!updateAvailable && !currentChangelogUnread) {
|
||||
return null;
|
||||
}
|
||||
@ -125,7 +93,8 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
<button
|
||||
style={style}
|
||||
className={clsx([styles.root, className])}
|
||||
data-has-update={updateAvailable ? 'true' : 'false'}
|
||||
data-has-update={!!updateAvailable}
|
||||
data-updating={appQuitting}
|
||||
data-disabled={
|
||||
(updateAvailable?.allowAutoUpdate && !updateReady) || appQuitting
|
||||
}
|
||||
@ -157,7 +126,9 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
<div className={clsx([styles.installLabelHover])}>
|
||||
<ResetIcon className={styles.icon} />
|
||||
<span className={styles.ellipsisTextOverflow}>
|
||||
{t['com.affine.updater.restart-to-update']()}
|
||||
{t[
|
||||
appQuitting ? 'Loading' : 'com.affine.updater.restart-to-update'
|
||||
]()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@ -215,3 +186,76 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AppUpdaterButton({
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom);
|
||||
const [appQuitting, setAppQuitting] = useState(false);
|
||||
|
||||
const onDismissCurrentChangelog = useCallback(() => {
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
startTransition(() =>
|
||||
setChangelogCheckAtom(mapping => {
|
||||
return {
|
||||
...mapping,
|
||||
[currentVersion]: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [currentVersion, setChangelogCheckAtom]);
|
||||
const onClickUpdate = useCallback(() => {
|
||||
if (updateReady) {
|
||||
setAppQuitting(true);
|
||||
window.apis?.updater.quitAndInstall().catch(err => {
|
||||
// TODO: add error toast here
|
||||
console.error(err);
|
||||
});
|
||||
} else if (updateAvailable) {
|
||||
if (updateAvailable.allowAutoUpdate) {
|
||||
// wait for download to finish
|
||||
} else {
|
||||
window.open(
|
||||
`https://github.com/toeverything/AFFiNE/releases/tag/v${currentVersion}`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
} else if (currentChangelogUnread) {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
onDismissCurrentChangelog();
|
||||
} else {
|
||||
throw new Unreachable();
|
||||
}
|
||||
}, [
|
||||
currentChangelogUnread,
|
||||
currentVersion,
|
||||
onDismissCurrentChangelog,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
]);
|
||||
return (
|
||||
<AppUpdaterButtonPure
|
||||
appQuitting={appQuitting}
|
||||
updateReady={!!updateReady}
|
||||
onClickUpdate={onClickUpdate}
|
||||
onDismissCurrentChangelog={onDismissCurrentChangelog}
|
||||
currentChangelogUnread={currentChangelogUnread}
|
||||
updateAvailable={updateAvailable}
|
||||
downloadProgress={downloadProgress}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export const mainContainerStyle = style({
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
selectors: {
|
||||
'&[data-show-padding="true"]': {
|
||||
margin: '8px',
|
||||
margin: '8px 8px 8px 0',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
|
@ -184,10 +184,15 @@ export type ExportHandlers = {
|
||||
savePDFFileAs: (title: string) => Promise<any>;
|
||||
};
|
||||
|
||||
export interface UpdateMeta {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
}
|
||||
|
||||
export type UpdaterHandlers = {
|
||||
currentVersion: () => Promise<any>;
|
||||
quitAndInstall: () => Promise<any>;
|
||||
checkForUpdatesAndNotify: () => Promise<any>;
|
||||
currentVersion: () => Promise<string>;
|
||||
quitAndInstall: () => Promise<void>;
|
||||
checkForUpdatesAndNotify: () => Promise<{ version: string } | null>;
|
||||
};
|
||||
|
||||
export type WorkspaceHandlers = {
|
||||
@ -196,15 +201,6 @@ export type WorkspaceHandlers = {
|
||||
getMeta: (id: string) => Promise<WorkspaceMeta>;
|
||||
};
|
||||
|
||||
export type EventMap = DBHandlers &
|
||||
DebugHandlers &
|
||||
DialogHandlers &
|
||||
UIHandlers &
|
||||
ClipboardHandlers &
|
||||
ExportHandlers &
|
||||
UpdaterHandlers &
|
||||
WorkspaceHandlers;
|
||||
|
||||
export type UnwrapManagerHandlerToServerSide<
|
||||
ElectronEvent extends {
|
||||
frameId: number;
|
||||
@ -242,3 +238,36 @@ export type App<
|
||||
> = TypedEventEmitter<{
|
||||
[K in keyof Handlers as `${Namespace}:${K & string}`]: Handlers[K];
|
||||
}>;
|
||||
|
||||
export interface UpdaterEvents {
|
||||
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => () => void;
|
||||
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => () => void;
|
||||
onDownloadProgress: (fn: (progress: number) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface ApplicationMenuEvents {
|
||||
onNewPageAction: (fn: () => void) => () => void;
|
||||
}
|
||||
|
||||
export interface DBEvents {
|
||||
onExternalUpdate: (
|
||||
fn: (update: {
|
||||
workspaceId: string;
|
||||
update: Uint8Array;
|
||||
docId?: string;
|
||||
}) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface WorkspaceEvents {
|
||||
onMetaChange: (
|
||||
fn: (workspaceId: string, meta: WorkspaceMeta) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
export interface EventMap {
|
||||
updater: UpdaterEvents;
|
||||
applicationMenu: ApplicationMenuEvents;
|
||||
db: DBEvents;
|
||||
workspace: WorkspaceEvents;
|
||||
}
|
||||
|
@ -55,7 +55,6 @@ vi.stubGlobal('window', {
|
||||
},
|
||||
events: {
|
||||
db: {
|
||||
// @ts-expect-error
|
||||
onExternalUpdate: fn => {
|
||||
triggerDBUpdate = fn;
|
||||
return () => {
|
||||
@ -119,7 +118,6 @@ describe('SQLite download provider', () => {
|
||||
|
||||
offlineYdoc.getText('text').insert(0, 'sqlite-world');
|
||||
|
||||
// @ts-expect-error
|
||||
triggerDBUpdate?.({
|
||||
workspaceId: id + '-another-id',
|
||||
update: Y.encodeStateAsUpdate(offlineYdoc),
|
||||
@ -128,7 +126,6 @@ describe('SQLite download provider', () => {
|
||||
// not yet updated (because the workspace id is different)
|
||||
expect(workspace.doc.getText('text').toString()).toBe('');
|
||||
|
||||
// @ts-expect-error
|
||||
triggerDBUpdate?.({
|
||||
workspaceId: id,
|
||||
update: Y.encodeStateAsUpdate(offlineYdoc),
|
||||
|
@ -85,7 +85,7 @@ const ImagePreviewModalImpl = (
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
return;
|
||||
}, [isOpen, props, setIsOpen]);
|
||||
|
||||
const nextImageHandler = useCallback(
|
||||
|
Loading…
Reference in New Issue
Block a user