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",
"noSearchResults": "No search results",
"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>;
uploadFile?: (viewId: string, file: File, onProgress?: (n: number) => void) => Promise<string>;
getSubscriptions?: () => Promise<Subscription[]>;
publish?: (view: View, publishName?: string) => Promise<void>;
publish?: (view: View, publishName?: string, visibleViewIds?: string[]) => Promise<void>;
unpublish?: (viewId: string) => Promise<void>;
}
@ -645,16 +645,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
}
}, [currentWorkspaceId, service]);
const publish = useCallback(async (view: View, publishName?: string) => {
const publish = useCallback(async (view: View, publishName?: string, visibleViewIds?: string[]) => {
if (!service || !currentWorkspaceId) return;
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
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, {
publish_name: publishName,
visible_database_view_ids: isDatabase ? visibleViewIds : undefined,
visible_database_view_ids: visibleViewIds,
});
await loadOutline(currentWorkspaceId, false);
}, [currentWorkspaceId, loadOutline, service]);

View File

@ -1,11 +1,15 @@
import { ViewLayout } from '@/application/types';
import { notify } from '@/components/_shared/notify';
import PageIcon from '@/components/_shared/view-icon/PageIcon';
import { useAppHandlers } from '@/components/app/app.hooks';
import { useLoadPublishInfo } from '@/components/app/share/publish.hooks';
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 { useTranslation } from 'react-i18next';
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 }) {
const { t } = useTranslation();
@ -24,6 +28,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
} = useLoadPublishInfo(viewId);
const [unpublishLoading, setUnpublishLoading] = React.useState<boolean>(false);
const [publishLoading, setPublishLoading] = React.useState<boolean>(false);
const [visibleViewId, setVisibleViewId] = React.useState<string[] | undefined>(undefined);
useEffect(() => {
if (opened) {
@ -35,8 +40,10 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
if (!publish || !view) return;
setPublishLoading(true);
const newPublishName = publishName || publishInfo?.publishName || undefined;
try {
await publish(view, publishName || publishInfo?.publishName);
await publish(view, newPublishName, visibleViewId);
await loadPublishInfo();
notify.success(t('publish.publishSuccessfully'));
// eslint-disable-next-line
@ -45,7 +52,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
} finally {
setPublishLoading(false);
}
}, [loadPublishInfo, publish, t, view, publishInfo]);
}, [loadPublishInfo, publish, t, view, publishInfo, visibleViewId]);
const handleUnpublish = useCallback(async () => {
if (!view || !unpublish) return;
@ -103,22 +110,84 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
</div>;
}, [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(() => {
return <Button
onClick={() => {
void handlePublish();
}}
variant={'contained'}
className={'w-full'}
color={'primary'}
startIcon={publishLoading ? <CircularProgress
color={'inherit'}
size={16}
/> : undefined}
>{
t('shareAction.publish')
}</Button>;
}, [handlePublish, publishLoading, t]);
if (!view) return null;
const list = [view, ...view.children];
return <div className={'flex flex-col gap-4 w-full'}>
{isDatabase &&
<div className={'flex mt-2 text-sm flex-col gap-3 rounded-[16px] border border-line-divider py-3 px-4'}>
<div className={'text-text-caption'}>{t('publishSelectedViews', {
count: visibleViewId?.length || 0,
})}</div>
<Divider />
<div className={'flex flex-col gap-1 overflow-y-auto max-h-[300px] appflowy-scroller overflow-x-hidden'}>
{list.map((item) => {
const id = item.view_id;
const isCurrentView = view.view_id === item.view_id;
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 (
<div className={'flex flex-col gap-2 w-full overflow-hidden'}>

View File

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