feat: supports publishing optional subviews of database views (#21)
This commit is contained in:
parent
8094ceff5f
commit
50d33e7de4
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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'}>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue