feat: supports publishing optional subviews of database views (#21)

This commit is contained in:
Kilu.He 2025-01-13 14:18:44 +08:00 committed by GitHub
parent 8094ceff5f
commit 50d33e7de4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 97 additions and 48 deletions

View File

@ -3054,5 +3054,7 @@
"searchResults": "Search results", "searchResults": "Search results",
"noSearchResults": "No search results", "noSearchResults": "No search results",
"AISearchPlaceholder": "Search or ask a question...", "AISearchPlaceholder": "Search or ask a question...",
"searchLabel": "Search" "searchLabel": "Search",
"publishSelectedViews": "Publish {{count}} selected views",
"untitled": "Untitled"
} }

View File

@ -62,7 +62,7 @@ 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>; publish?: (view: View, publishName?: string, visibleViewIds?: string[]) => Promise<void>;
unpublish?: (viewId: string) => Promise<void>; unpublish?: (viewId: string) => Promise<void>;
} }
@ -645,16 +645,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
} }
}, [currentWorkspaceId, service]); }, [currentWorkspaceId, service]);
const publish = useCallback(async (view: View, publishName?: string) => { const publish = useCallback(async (view: View, publishName?: string, visibleViewIds?: string[]) => {
if (!service || !currentWorkspaceId) return; if (!service || !currentWorkspaceId) return;
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
const viewId = view.view_id; const viewId = view.view_id;
const children = view.children || [];
const visibleViewIds = [view.view_id, ...(children.map((v) => v.view_id))];
await service.publishView(currentWorkspaceId, viewId, { await service.publishView(currentWorkspaceId, viewId, {
publish_name: publishName, publish_name: publishName,
visible_database_view_ids: isDatabase ? visibleViewIds : undefined, visible_database_view_ids: visibleViewIds,
}); });
await loadOutline(currentWorkspaceId, false); await loadOutline(currentWorkspaceId, false);
}, [currentWorkspaceId, loadOutline, service]); }, [currentWorkspaceId, loadOutline, service]);

View File

@ -1,11 +1,15 @@
import { ViewLayout } from '@/application/types';
import { notify } from '@/components/_shared/notify'; import { notify } from '@/components/_shared/notify';
import PageIcon from '@/components/_shared/view-icon/PageIcon';
import { useAppHandlers } 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 { Button, CircularProgress, Typography } from '@mui/material'; import { Button, CircularProgress, Divider, Typography } from '@mui/material';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } 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';
import { ReactComponent as CheckboxCheckSvg } from '@/assets/check_filled.svg';
import { ReactComponent as CheckboxUncheckSvg } from '@/assets/uncheck.svg';
function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: () => void; opened: boolean }) { function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: () => void; opened: boolean }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -24,6 +28,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
} = useLoadPublishInfo(viewId); } = useLoadPublishInfo(viewId);
const [unpublishLoading, setUnpublishLoading] = React.useState<boolean>(false); const [unpublishLoading, setUnpublishLoading] = React.useState<boolean>(false);
const [publishLoading, setPublishLoading] = React.useState<boolean>(false); const [publishLoading, setPublishLoading] = React.useState<boolean>(false);
const [visibleViewId, setVisibleViewId] = React.useState<string[] | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
@ -35,8 +40,10 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
if (!publish || !view) return; if (!publish || !view) return;
setPublishLoading(true); setPublishLoading(true);
const newPublishName = publishName || publishInfo?.publishName || undefined;
try { try {
await publish(view, publishName || publishInfo?.publishName); await publish(view, newPublishName, visibleViewId);
await loadPublishInfo(); await loadPublishInfo();
notify.success(t('publish.publishSuccessfully')); notify.success(t('publish.publishSuccessfully'));
// eslint-disable-next-line // eslint-disable-next-line
@ -45,7 +52,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
} finally { } finally {
setPublishLoading(false); setPublishLoading(false);
} }
}, [loadPublishInfo, publish, t, view, publishInfo]); }, [loadPublishInfo, publish, t, view, publishInfo, visibleViewId]);
const handleUnpublish = useCallback(async () => { const handleUnpublish = useCallback(async () => {
if (!view || !unpublish) return; if (!view || !unpublish) return;
@ -103,22 +110,84 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
</div>; </div>;
}, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, unpublishLoading, url, view]); }, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, unpublishLoading, url, view]);
const layout = view?.layout;
const isDatabase = layout !== undefined ? [ViewLayout.Grid, ViewLayout.Board, ViewLayout.Calendar].includes(layout) : false;
const hasPublished = view?.is_published;
useEffect(() => {
if (!hasPublished && isDatabase && view) {
const childIds = [view.view_id, ...view.children.map((child) => child.view_id)];
setVisibleViewId(childIds);
} else {
setVisibleViewId(undefined);
}
}, [hasPublished, isDatabase, view]);
const renderUnpublished = useCallback(() => { const renderUnpublished = useCallback(() => {
return <Button if (!view) return null;
onClick={() => { const list = [view, ...view.children];
void handlePublish();
}} return <div className={'flex flex-col gap-4 w-full'}>
variant={'contained'} {isDatabase &&
className={'w-full'} <div className={'flex mt-2 text-sm flex-col gap-3 rounded-[16px] border border-line-divider py-3 px-4'}>
color={'primary'} <div className={'text-text-caption'}>{t('publishSelectedViews', {
startIcon={publishLoading ? <CircularProgress count: visibleViewId?.length || 0,
color={'inherit'} })}</div>
size={16} <Divider />
/> : undefined} <div className={'flex flex-col gap-1 overflow-y-auto max-h-[300px] appflowy-scroller overflow-x-hidden'}>
>{ {list.map((item) => {
t('shareAction.publish') const id = item.view_id;
}</Button>; const isCurrentView = view.view_id === item.view_id;
}, [handlePublish, publishLoading, t]);
const selected = visibleViewId?.includes(item.view_id);
return <Button
disabled={isCurrentView}
onClick={() => {
setVisibleViewId(prev => {
const checked = prev?.includes(id);
if (checked) {
return prev?.filter((i) => i !== id);
} else {
return [...(prev || []), id];
}
});
}}
key={id}
className={'flex justify-start items-center'}
size={'small'}
startIcon={selected ? <CheckboxCheckSvg className={'w-5 h-5'} /> :
<CheckboxUncheckSvg className={'w-5 h-5'} />}
color={'inherit'}
>
<div className={'flex items-center gap-2'}>
<PageIcon view={item} />
{item.name || t('untitled')}
</div>
</Button>;
})}
</div>
</div>}
<Button
onClick={() => {
void handlePublish();
}}
variant={'contained'}
className={'w-full'}
color={'primary'}
startIcon={publishLoading ? <CircularProgress
color={'inherit'}
size={16}
/> : undefined}
>{
t('shareAction.publish')
}</Button></div>;
}, [handlePublish, isDatabase, publishLoading, t, view, visibleViewId]);
return ( return (
<div className={'flex flex-col gap-2 w-full overflow-hidden'}> <div className={'flex flex-col gap-2 w-full overflow-hidden'}>

View File

@ -1,11 +1,9 @@
import { View } from '@/application/types'; import { useAppView, useUserWorkspaceInfo } from '@/components/app/app.hooks';
import { useCurrentWorkspaceId, useUserWorkspaceInfo } from '@/components/app/app.hooks';
import { useCurrentUser, useService } from '@/components/main/app.hooks'; import { useCurrentUser, useService } from '@/components/main/app.hooks';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
export function useLoadPublishInfo (viewId: string) { export function useLoadPublishInfo (viewId: string) {
const [view, setView] = React.useState<View>(); const view = useAppView(viewId);
const currentWorkspaceId = useCurrentWorkspaceId();
const [publishInfo, setPublishInfo] = React.useState<{ const [publishInfo, setPublishInfo] = React.useState<{
namespace: string, namespace: string,
publishName: string, publishName: string,
@ -18,29 +16,12 @@ export function useLoadPublishInfo (viewId: string) {
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const isOwner = userWorkspaceInfo?.selectedWorkspace?.owner?.uid.toString() === currentUser?.uid.toString(); const isOwner = userWorkspaceInfo?.selectedWorkspace?.owner?.uid.toString() === currentUser?.uid.toString();
const isPublisher = publishInfo?.publisherEmail === currentUser?.email; const isPublisher = publishInfo?.publisherEmail === currentUser?.email;
const loadView = useCallback(async () => {
if (!viewId || !service || !currentWorkspaceId) return;
try {
const view = await service.getAppView(currentWorkspaceId, viewId);
setView(view);
return view;
// eslint-disable-next-line
} catch (e: any) {
// do nothing
console.error(e);
}
}, [currentWorkspaceId, service, viewId]);
const loadPublishInfo = useCallback(async () => { const loadPublishInfo = useCallback(async () => {
if (!service) return; if (!service) return;
setLoading(true); setLoading(true);
try { try {
const view = await loadView(); const res = await service.getPublishInfo(viewId);
if (!view) return;
const res = await service.getPublishInfo(view?.view_id);
setPublishInfo(res); setPublishInfo(res);
@ -50,7 +31,7 @@ export function useLoadPublishInfo (viewId: string) {
} }
setLoading(false); setLoading(false);
}, [loadView, service]); }, [viewId, service]);
useEffect(() => { useEffect(() => {
void loadPublishInfo(); void loadPublishInfo();