feat: modifed toast styles (#10)

* feat: support publish (#7)

* fix: update pnpm-lock
This commit is contained in:
Kilu.He 2025-01-08 14:24:13 +08:00 committed by GitHub
parent 3e9b0bead3
commit d647387acd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 7903 additions and 10054 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,40 +14,45 @@ const CustomSnackbar = React.forwardRef<HTMLDivElement, CustomContentProps>((pro
const { closeSnackbar } = useSnackbar(); const { closeSnackbar } = useSnackbar();
const icons = { const icons = {
success: <TaskAltRounded className="w-6 h-6 text-green-500" />, success: <TaskAltRounded className="w-5 h-5 text-green-500" />,
error: <HighlightOff className="w-6 h-6 text-red-500" />, error: <HighlightOff className="w-5 h-5 text-red-500" />,
warning: <ErrorOutline className="w-6 h-6 text-yellow-500" />, warning: <ErrorOutline className="w-5 h-5 text-yellow-500" />,
info: <PowerSettingsNew className="w-6 h-6 text-blue-500" />, info: <PowerSettingsNew className="w-5 h-5 text-blue-500" />,
loading: null, loading: null,
default: null, default: null,
}; };
const colors = { const colors = {
success: 'bg-green-50 border-green-300', success: 'border-green-300 border bg-bg-body',
error: 'bg-red-50', error: 'bg-bg-body border-red-300 border',
warning: 'bg-yellow-50 border-yellow-300', warning: 'bg-bg-body border-yellow-300 border',
info: 'bg-blue-50 border-blue-300', info: 'bg-bg-body border-blue-300 border',
default: 'bg-bg-body border border-content-blue-400', default: 'bg-bg-body border border-content-blue-400',
}; };
const [hovered, setHovered] = React.useState<boolean>(false);
return ( return (
<SnackbarContent <SnackbarContent
ref={ref} ref={ref}
className={`${colors[variant]} rounded-lg shadow-lg`} className={`${colors[variant]} rounded-lg shadow`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
> >
<div className="flex items-center justify-between w-full p-4"> <div className="flex items-center justify-between w-full p-3">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{icons[variant]} {icons[variant]}
</div> </div>
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<p className="text-sm font-medium">{message}</p> <p className="text-sm font-medium">{message}</p>
</div> </div>
<IconButton {hovered && <IconButton
className={'mx-2'} className={'mx-2 rounded-full bg-fill-list-hover hover:opacity-100 opacity-60'}
onClick={() => closeSnackbar(id)} onClick={() => closeSnackbar(id)}
> >
<CloseIcon className="h-5 w-5 text-text-caption" /> <CloseIcon className="h-3 w-3 text-text-title" />
</IconButton> </IconButton>}
</div> </div>
</SnackbarContent> </SnackbarContent>
); );

View File

@ -15,6 +15,7 @@ import {
YjsEditorKey, YjsEditorKey,
YSharedRoot, YSharedRoot,
} from '@/application/types'; } from '@/application/types';
import { notify } from '@/components/_shared/notify';
import { findAncestors, findView, findViewByLayout } from '@/components/_shared/outline/utils'; import { findAncestors, findView, findViewByLayout } from '@/components/_shared/outline/utils';
import RequestAccess from '@/components/app/landing-pages/RequestAccess'; import RequestAccess from '@/components/app/landing-pages/RequestAccess';
import { AFConfigContext, useService } from '@/components/main/app.hooks'; import { AFConfigContext, useService } from '@/components/main/app.hooks';
@ -62,6 +63,8 @@ export interface AppContextType {
updateSpace?: (payload: UpdateSpacePayload) => Promise<void>; updateSpace?: (payload: UpdateSpacePayload) => Promise<void>;
uploadFile?: (viewId: string, file: File, onProgress?: (n: number) => void) => Promise<string>; uploadFile?: (viewId: string, file: File, onProgress?: (n: number) => void) => Promise<string>;
getSubscriptions?: () => Promise<Subscription[]>; getSubscriptions?: () => Promise<Subscription[]>;
publish?: (view: View, publishName?: string) => Promise<void>;
unpublish?: (viewId: string) => Promise<void>;
} }
const USER_NO_ACCESS_CODE = [1024, 1012]; const USER_NO_ACCESS_CODE = [1024, 1012];
@ -641,6 +644,24 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
} }
}, [currentWorkspaceId, service]); }, [currentWorkspaceId, service]);
const publish = useCallback(async (view: View, publishName?: string) => {
if (!service || !currentWorkspaceId) return;
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
const viewId = view.view_id;
await service.publishView(currentWorkspaceId, viewId, {
publish_name: publishName,
visible_database_view_ids: isDatabase ? view.children?.map((v) => v.view_id) : undefined,
});
void loadOutline(currentWorkspaceId, false);
}, [currentWorkspaceId, loadOutline, service]);
const unpublish = useCallback(async (viewId: string) => {
if (!service || !currentWorkspaceId) return;
await service.unpublishView(currentWorkspaceId, viewId);
void loadOutline(currentWorkspaceId, false);
}, [currentWorkspaceId, loadOutline, service]);
return <AppContext.Provider return <AppContext.Provider
value={{ value={{
currentWorkspaceId, currentWorkspaceId,
@ -679,6 +700,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
updateSpace, updateSpace,
uploadFile, uploadFile,
getSubscriptions, getSubscriptions,
publish,
unpublish,
}} }}
> >
{requestAccessOpened ? <RequestAccess /> : children} {requestAccessOpened ? <RequestAccess /> : children}
@ -819,6 +842,8 @@ export function useAppHandlers () {
updateSpace: context.updateSpace, updateSpace: context.updateSpace,
uploadFile: context.uploadFile, uploadFile: context.uploadFile,
getSubscriptions: context.getSubscriptions, getSubscriptions: context.getSubscriptions,
publish: context.publish,
unpublish: context.unpublish,
}; };
} }

View File

@ -1,4 +1,4 @@
import { SubscriptionPlan, View, ViewLayout } from '@/application/types'; import { SubscriptionPlan, View } from '@/application/types';
import { notify } from '@/components/_shared/notify'; import { notify } from '@/components/_shared/notify';
import { flattenViews } from '@/components/_shared/outline/utils'; import { flattenViews } from '@/components/_shared/outline/utils';
import { useAppHandlers, useUserWorkspaceInfo } from '@/components/app/app.hooks'; import { useAppHandlers, useUserWorkspaceInfo } from '@/components/app/app.hooks';
@ -129,36 +129,34 @@ export function PublishManage ({
}, [isOwner, service, t, workspaceId]); }, [isOwner, service, t, workspaceId]);
const currentWorkspaceId = userWorkspaceInfo?.selectedWorkspace?.id; const {
publish,
unpublish,
} = useAppHandlers();
const handlePublish = useCallback(async (view: View, publishName: string) => { const handlePublish = useCallback(async (view: View, publishName: string) => {
if (!service || !currentWorkspaceId) return; if (!publish) return;
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
const viewId = view.view_id;
try { try {
await service.publishView(currentWorkspaceId, viewId, { await publish(view, publishName);
publish_name: publishName,
visible_database_view_ids: isDatabase ? view.children?.map((v) => v.view_id) : undefined,
});
notify.success(t('publish.publishSuccessfully')); notify.success(t('publish.publishSuccessfully'));
// eslint-disable-next-line // eslint-disable-next-line
} catch (e: any) { } catch (e: any) {
notify.error(e.message); notify.error(e.message);
} }
}, [currentWorkspaceId, service, t]); }, [publish, t]);
const handleUnpublish = useCallback(async (viewId: string) => { const handleUnpublish = useCallback(async (viewId: string) => {
if (!service || !currentWorkspaceId) return; if (!unpublish) return;
try { try {
await service.unpublishView(currentWorkspaceId, viewId); await unpublish(viewId);
void loadPublishPages(); void loadPublishPages();
notify.success(t('publish.unpublishSuccessfully')); notify.success(t('publish.unpublishSuccessfully'));
// eslint-disable-next-line // eslint-disable-next-line
} catch (e: any) { } catch (e: any) {
notify.error(e.message); notify.error(e.message);
} }
}, [currentWorkspaceId, loadPublishPages, service, t]); }, [loadPublishPages, t, unpublish]);
const { const {
getSubscriptions, getSubscriptions,

View File

@ -5,7 +5,7 @@ import { CircularProgress, IconButton, InputBase, Tooltip } from '@mui/material'
import { ReactComponent as LinkIcon } from '@/assets/link.svg'; import { ReactComponent as LinkIcon } from '@/assets/link.svg';
import { ReactComponent as DownIcon } from '@/assets/chevron_down.svg'; import { ReactComponent as DownIcon } from '@/assets/chevron_down.svg';
import { ReactComponent as CheckIcon } from '@/assets/check.svg'; import { ReactComponent as CheckIcon } from '@/assets/check.svg';
import React from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
function PublishLinkPreview ({ function PublishLinkPreview ({
@ -15,6 +15,7 @@ function PublishLinkPreview ({
url, url,
isOwner, isOwner,
isPublisher, isPublisher,
onClose,
}: { }: {
publishInfo: { namespace: string, publishName: string }; publishInfo: { namespace: string, publishName: string };
onUnPublish: () => Promise<void>; onUnPublish: () => Promise<void>;
@ -22,6 +23,7 @@ function PublishLinkPreview ({
url: string; url: string;
isOwner: boolean; isOwner: boolean;
isPublisher: boolean; isPublisher: boolean;
onClose?: () => void;
}) { }) {
const [siteOpen, setSiteOpen] = React.useState<boolean>(false); const [siteOpen, setSiteOpen] = React.useState<boolean>(false);
const [renameOpen, setRenameOpen] = React.useState<boolean>(false); const [renameOpen, setRenameOpen] = React.useState<boolean>(false);
@ -30,6 +32,10 @@ function PublishLinkPreview ({
const [focused, setFocused] = React.useState<boolean>(false); const [focused, setFocused] = React.useState<boolean>(false);
const [loading, setLoading] = React.useState<boolean>(false); const [loading, setLoading] = React.useState<boolean>(false);
useEffect(() => {
setPublishName(publishInfo.publishName);
}, [publishInfo.publishName]);
const handlePublish = async () => { const handlePublish = async () => {
if (loading) return; if (loading) return;
if (publishName === publishInfo.publishName) return; if (publishName === publishInfo.publishName) return;
@ -62,6 +68,7 @@ function PublishLinkPreview ({
size={'small'} size={'small'}
onClick={() => { onClick={() => {
setSiteOpen(true); setSiteOpen(true);
onClose?.();
}} }}
> >
<DownIcon className={'w-4 h-4'} /> <DownIcon className={'w-4 h-4'} />
@ -111,6 +118,7 @@ function PublishLinkPreview ({
} }
setRenameOpen(true); setRenameOpen(true);
onClose?.();
}} }}
> >
{loading ? <CircularProgress size={14} /> : {loading ? <CircularProgress size={14} /> :
@ -142,7 +150,7 @@ function PublishLinkPreview ({
onPublish={onPublish} onPublish={onPublish}
url={url} url={url}
/>} />}
{siteOpen && <NormalModal <NormalModal
okButtonProps={{ okButtonProps={{
className: 'hidden', className: 'hidden',
}} }}
@ -168,7 +176,7 @@ function PublishLinkPreview ({
/> />
</div> </div>
</NormalModal>} </NormalModal>
</div> </div>
</> </>

View File

@ -1,17 +1,18 @@
import { ViewLayout } from '@/application/types';
import { notify } from '@/components/_shared/notify'; import { notify } from '@/components/_shared/notify';
import { useCurrentWorkspaceId } from '@/components/app/app.hooks'; import { useAppHandlers } from '@/components/app/app.hooks';
import { useLoadPublishInfo } from '@/components/app/share/publish.hooks'; import { useLoadPublishInfo } from '@/components/app/share/publish.hooks';
import PublishLinkPreview from '@/components/app/share/PublishLinkPreview'; import PublishLinkPreview from '@/components/app/share/PublishLinkPreview';
import { useService } from '@/components/main/app.hooks';
import { Button, CircularProgress, Typography } from '@mui/material'; import { Button, CircularProgress, Typography } from '@mui/material';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as PublishIcon } from '@/assets/publish.svg'; import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
function PublishPanel ({ viewId }: { viewId: string }) { function PublishPanel ({ viewId, onClose }: { viewId: string; onClose: () => void }) {
const currentWorkspaceId = useCurrentWorkspaceId();
const { t } = useTranslation(); const { t } = useTranslation();
const {
publish,
unpublish,
} = useAppHandlers();
const { const {
url, url,
loadPublishInfo, loadPublishInfo,
@ -22,40 +23,35 @@ function PublishPanel ({ viewId }: { viewId: string }) {
isPublisher, isPublisher,
} = useLoadPublishInfo(viewId); } = useLoadPublishInfo(viewId);
const service = useService();
const handlePublish = useCallback(async (publishName?: string) => { const handlePublish = useCallback(async (publishName?: string) => {
if (!service || !currentWorkspaceId || !view) return; if (!publish || !view) return;
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
try { try {
await service.publishView(currentWorkspaceId, viewId, { await publish(view, publishName);
publish_name: publishName, void loadPublishInfo();
visible_database_view_ids: isDatabase ? view.children?.map((v) => v.view_id) : undefined,
});
await loadPublishInfo();
notify.success(t('publish.publishSuccessfully')); notify.success(t('publish.publishSuccessfully'));
// eslint-disable-next-line // eslint-disable-next-line
} catch (e: any) { } catch (e: any) {
notify.error(e.message); notify.error(e.message);
} }
}, [currentWorkspaceId, loadPublishInfo, service, t, view, viewId]); }, [loadPublishInfo, publish, t, view]);
const handleUnpublish = useCallback(async () => { const handleUnpublish = useCallback(async () => {
if (!service || !currentWorkspaceId || !view) return; if (!view || !unpublish) return;
if (!isOwner && !isPublisher) { if (!isOwner && !isPublisher) {
notify.error(t('settings.sites.error.publishPermissionDenied')); notify.error(t('settings.sites.error.publishPermissionDenied'));
return; return;
} }
try { try {
await service.unpublishView(currentWorkspaceId, viewId); await unpublish(viewId);
await loadPublishInfo(); await loadPublishInfo();
notify.success(t('publish.unpublishSuccessfully')); notify.success(t('publish.unpublishSuccessfully'));
// eslint-disable-next-line // eslint-disable-next-line
} catch (e: any) { } catch (e: any) {
notify.error(e.message); notify.error(e.message);
} }
}, [currentWorkspaceId, isOwner, isPublisher, loadPublishInfo, service, t, view, viewId]); }, [isOwner, isPublisher, loadPublishInfo, t, unpublish, view, viewId]);
const renderPublished = useCallback(() => { const renderPublished = useCallback(() => {
if (!publishInfo || !view) return null; if (!publishInfo || !view) return null;
@ -67,6 +63,7 @@ function PublishPanel ({ viewId }: { viewId: string }) {
onUnPublish={handleUnpublish} onUnPublish={handleUnpublish}
isOwner={isOwner} isOwner={isOwner}
isPublisher={isPublisher} isPublisher={isPublisher}
onClose={onClose}
/> />
<div className={'flex items-center gap-4 justify-end w-full'}> <div className={'flex items-center gap-4 justify-end w-full'}>
<Button <Button
@ -86,7 +83,7 @@ function PublishPanel ({ viewId }: { viewId: string }) {
>{t('shareAction.visitSite')}</Button> >{t('shareAction.visitSite')}</Button>
</div> </div>
</div>; </div>;
}, [handlePublish, handleUnpublish, isOwner, isPublisher, publishInfo, t, url, view]); }, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, url, view]);
const renderUnpublished = useCallback(() => { const renderUnpublished = useCallback(() => {
return <Button return <Button

View File

@ -22,7 +22,8 @@ export function ShareButton ({ viewId }: { viewId: string }) {
variant={'contained'} variant={'contained'}
color={'primary'} color={'primary'}
>{t('shareAction.buttonText')}</Button> >{t('shareAction.buttonText')}</Button>
{opened && <Popover <Popover
keepMounted
open={opened} open={opened}
anchorEl={ref.current} anchorEl={ref.current}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
@ -41,9 +42,13 @@ export function ShareButton ({ viewId }: { viewId: string }) {
}} }}
> >
<div className={'flex flex-col gap-2 w-fit p-2'}> <div className={'flex flex-col gap-2 w-fit p-2'}>
<ShareTabs viewId={viewId} /> <ShareTabs
opened={opened}
viewId={viewId}
onClose={() => setOpened(false)}
/>
</div> </div>
</Popover>} </Popover>
</> </>
); );
} }

View File

@ -15,7 +15,7 @@ enum TabKey {
TEMPLATE = 'template', TEMPLATE = 'template',
} }
function ShareTabs({ viewId }: { viewId: string }) { function ShareTabs ({ opened, viewId, onClose }: { opened: boolean, viewId: string; onClose: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const view = useAppView(viewId); const view = useAppView(viewId);
const [value, setValue] = React.useState<TabKey>(TabKey.SHARE); const [value, setValue] = React.useState<TabKey>(TabKey.SHARE);
@ -29,18 +29,18 @@ function ShareTabs({ viewId }: { viewId: string }) {
}, { }, {
value: TabKey.PUBLISH, value: TabKey.PUBLISH,
label: t('shareAction.publish'), label: t('shareAction.publish'),
icon: view?.is_published ? <PublishedWithChanges className={'w-4 h-4 text-function-success mb-0'}/> : undefined, icon: view?.is_published ? <PublishedWithChanges className={'w-4 h-4 text-function-success mb-0'} /> : undefined,
Panel: PublishPanel, Panel: PublishPanel,
}, currentUser?.email?.endsWith('appflowy.io') && view?.is_published && { }, currentUser?.email?.endsWith('appflowy.io') && view?.is_published && {
value: TabKey.TEMPLATE, value: TabKey.TEMPLATE,
label: t('template.asTemplate'), label: t('template.asTemplate'),
icon: <Templates className={'w-4 h-4 mb-0'}/>, icon: <Templates className={'w-4 h-4 mb-0'} />,
Panel: TemplatePanel, Panel: TemplatePanel,
}].filter(Boolean) as { }].filter(Boolean) as {
value: TabKey; value: TabKey;
label: string; label: string;
icon?: React.JSX.Element; icon?: React.JSX.Element;
Panel: React.FC<{ viewId: string }> Panel: React.FC<{ viewId: string; onClose: () => void }>
}[]; }[];
}, [currentUser?.email, t, view?.is_published]); }, [currentUser?.email, t, view?.is_published]);
@ -56,7 +56,7 @@ function ShareTabs({ viewId }: { viewId: string }) {
onChange={onChange} onChange={onChange}
value={value} value={value}
> >
{options.map((option) => ( {opened && options.map((option) => (
<ViewTab <ViewTab
className={'flex items-center flex-row justify-center gap-1.5'} className={'flex items-center flex-row justify-center gap-1.5'}
key={option.value} key={option.value}
@ -74,7 +74,10 @@ function ShareTabs({ viewId }: { viewId: string }) {
index={option.value} index={option.value}
value={value} value={value}
> >
<option.Panel viewId={viewId}/> <option.Panel
viewId={viewId}
onClose={onClose}
/>
</TabPanel> </TabPanel>
))} ))}
</div> </div>

View File

@ -19,8 +19,8 @@ const StyledSnackbarProvider = styled(SnackbarProvider)`
`; `;
export default function withAppWrapper(Component: React.FC): React.FC { export default function withAppWrapper (Component: React.FC): React.FC {
return function AppWrapper(): JSX.Element { return function AppWrapper (): JSX.Element {
return ( return (
<AppTheme> <AppTheme>
<ErrorBoundary FallbackComponent={ErrorHandlerPage}> <ErrorBoundary FallbackComponent={ErrorHandlerPage}>
@ -41,7 +41,7 @@ export default function withAppWrapper(Component: React.FC): React.FC {
> >
<AppConfig> <AppConfig>
<Suspense> <Suspense>
<Component/> <Component />
</Suspense> </Suspense>
</AppConfig> </AppConfig>
</StyledSnackbarProvider> </StyledSnackbarProvider>