Skip to content

Commit 50d33e7

Browse files
authored
feat: supports publishing optional subviews of database views (#21)
1 parent 8094cef commit 50d33e7

File tree

4 files changed

+97
-48
lines changed

4 files changed

+97
-48
lines changed

src/@types/translations/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3054,5 +3054,7 @@
30543054
"searchResults": "Search results",
30553055
"noSearchResults": "No search results",
30563056
"AISearchPlaceholder": "Search or ask a question...",
3057-
"searchLabel": "Search"
3057+
"searchLabel": "Search",
3058+
"publishSelectedViews": "Publish {{count}} selected views",
3059+
"untitled": "Untitled"
30583060
}

src/components/app/app.hooks.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export interface AppContextType {
6262
updateSpace?: (payload: UpdateSpacePayload) => Promise<void>;
6363
uploadFile?: (viewId: string, file: File, onProgress?: (n: number) => void) => Promise<string>;
6464
getSubscriptions?: () => Promise<Subscription[]>;
65-
publish?: (view: View, publishName?: string) => Promise<void>;
65+
publish?: (view: View, publishName?: string, visibleViewIds?: string[]) => Promise<void>;
6666
unpublish?: (viewId: string) => Promise<void>;
6767
}
6868

@@ -645,16 +645,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
645645
}
646646
}, [currentWorkspaceId, service]);
647647

648-
const publish = useCallback(async (view: View, publishName?: string) => {
648+
const publish = useCallback(async (view: View, publishName?: string, visibleViewIds?: string[]) => {
649649
if (!service || !currentWorkspaceId) return;
650-
const isDatabase = [ViewLayout.Board, ViewLayout.Grid, ViewLayout.Calendar].includes(view.layout);
651650
const viewId = view.view_id;
652-
const children = view.children || [];
653-
const visibleViewIds = [view.view_id, ...(children.map((v) => v.view_id))];
654651

655652
await service.publishView(currentWorkspaceId, viewId, {
656653
publish_name: publishName,
657-
visible_database_view_ids: isDatabase ? visibleViewIds : undefined,
654+
visible_database_view_ids: visibleViewIds,
658655
});
659656
await loadOutline(currentWorkspaceId, false);
660657
}, [currentWorkspaceId, loadOutline, service]);

src/components/app/share/PublishPanel.tsx

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { ViewLayout } from '@/application/types';
12
import { notify } from '@/components/_shared/notify';
3+
import PageIcon from '@/components/_shared/view-icon/PageIcon';
24
import { useAppHandlers } from '@/components/app/app.hooks';
35
import { useLoadPublishInfo } from '@/components/app/share/publish.hooks';
46
import PublishLinkPreview from '@/components/app/share/PublishLinkPreview';
5-
import { Button, CircularProgress, Typography } from '@mui/material';
7+
import { Button, CircularProgress, Divider, Typography } from '@mui/material';
68
import React, { useCallback, useEffect } from 'react';
79
import { useTranslation } from 'react-i18next';
810
import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
11+
import { ReactComponent as CheckboxCheckSvg } from '@/assets/check_filled.svg';
12+
import { ReactComponent as CheckboxUncheckSvg } from '@/assets/uncheck.svg';
913

1014
function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: () => void; opened: boolean }) {
1115
const { t } = useTranslation();
@@ -24,6 +28,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
2428
} = useLoadPublishInfo(viewId);
2529
const [unpublishLoading, setUnpublishLoading] = React.useState<boolean>(false);
2630
const [publishLoading, setPublishLoading] = React.useState<boolean>(false);
31+
const [visibleViewId, setVisibleViewId] = React.useState<string[] | undefined>(undefined);
2732

2833
useEffect(() => {
2934
if (opened) {
@@ -35,8 +40,10 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
3540
if (!publish || !view) return;
3641

3742
setPublishLoading(true);
43+
const newPublishName = publishName || publishInfo?.publishName || undefined;
44+
3845
try {
39-
await publish(view, publishName || publishInfo?.publishName);
46+
await publish(view, newPublishName, visibleViewId);
4047
await loadPublishInfo();
4148
notify.success(t('publish.publishSuccessfully'));
4249
// eslint-disable-next-line
@@ -45,7 +52,7 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
4552
} finally {
4653
setPublishLoading(false);
4754
}
48-
}, [loadPublishInfo, publish, t, view, publishInfo]);
55+
}, [loadPublishInfo, publish, t, view, publishInfo, visibleViewId]);
4956

5057
const handleUnpublish = useCallback(async () => {
5158
if (!view || !unpublish) return;
@@ -103,22 +110,84 @@ function PublishPanel ({ viewId, opened, onClose }: { viewId: string; onClose: (
103110
</div>;
104111
}, [handlePublish, handleUnpublish, isOwner, isPublisher, onClose, publishInfo, t, unpublishLoading, url, view]);
105112

113+
const layout = view?.layout;
114+
const isDatabase = layout !== undefined ? [ViewLayout.Grid, ViewLayout.Board, ViewLayout.Calendar].includes(layout) : false;
115+
const hasPublished = view?.is_published;
116+
117+
useEffect(() => {
118+
if (!hasPublished && isDatabase && view) {
119+
const childIds = [view.view_id, ...view.children.map((child) => child.view_id)];
120+
121+
setVisibleViewId(childIds);
122+
} else {
123+
setVisibleViewId(undefined);
124+
}
125+
}, [hasPublished, isDatabase, view]);
126+
106127
const renderUnpublished = useCallback(() => {
107-
return <Button
108-
onClick={() => {
109-
void handlePublish();
110-
}}
111-
variant={'contained'}
112-
className={'w-full'}
113-
color={'primary'}
114-
startIcon={publishLoading ? <CircularProgress
115-
color={'inherit'}
116-
size={16}
117-
/> : undefined}
118-
>{
119-
t('shareAction.publish')
120-
}</Button>;
121-
}, [handlePublish, publishLoading, t]);
128+
if (!view) return null;
129+
const list = [view, ...view.children];
130+
131+
return <div className={'flex flex-col gap-4 w-full'}>
132+
{isDatabase &&
133+
<div className={'flex mt-2 text-sm flex-col gap-3 rounded-[16px] border border-line-divider py-3 px-4'}>
134+
<div className={'text-text-caption'}>{t('publishSelectedViews', {
135+
count: visibleViewId?.length || 0,
136+
})}</div>
137+
<Divider />
138+
<div className={'flex flex-col gap-1 overflow-y-auto max-h-[300px] appflowy-scroller overflow-x-hidden'}>
139+
{list.map((item) => {
140+
const id = item.view_id;
141+
const isCurrentView = view.view_id === item.view_id;
142+
143+
const selected = visibleViewId?.includes(item.view_id);
144+
145+
return <Button
146+
disabled={isCurrentView}
147+
onClick={() => {
148+
setVisibleViewId(prev => {
149+
const checked = prev?.includes(id);
150+
151+
if (checked) {
152+
return prev?.filter((i) => i !== id);
153+
} else {
154+
return [...(prev || []), id];
155+
}
156+
157+
});
158+
}}
159+
key={id}
160+
className={'flex justify-start items-center'}
161+
size={'small'}
162+
startIcon={selected ? <CheckboxCheckSvg className={'w-5 h-5'} /> :
163+
<CheckboxUncheckSvg className={'w-5 h-5'} />}
164+
color={'inherit'}
165+
>
166+
<div className={'flex items-center gap-2'}>
167+
<PageIcon view={item} />
168+
{item.name || t('untitled')}
169+
</div>
170+
171+
</Button>;
172+
})}
173+
</div>
174+
175+
</div>}
176+
<Button
177+
onClick={() => {
178+
void handlePublish();
179+
}}
180+
variant={'contained'}
181+
className={'w-full'}
182+
color={'primary'}
183+
startIcon={publishLoading ? <CircularProgress
184+
color={'inherit'}
185+
size={16}
186+
/> : undefined}
187+
>{
188+
t('shareAction.publish')
189+
}</Button></div>;
190+
}, [handlePublish, isDatabase, publishLoading, t, view, visibleViewId]);
122191

123192
return (
124193
<div className={'flex flex-col gap-2 w-full overflow-hidden'}>

src/components/app/share/publish.hooks.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { View } from '@/application/types';
2-
import { useCurrentWorkspaceId, useUserWorkspaceInfo } from '@/components/app/app.hooks';
1+
import { useAppView, useUserWorkspaceInfo } from '@/components/app/app.hooks';
32
import { useCurrentUser, useService } from '@/components/main/app.hooks';
43
import React, { useCallback, useEffect, useMemo } from 'react';
54

65
export function useLoadPublishInfo (viewId: string) {
7-
const [view, setView] = React.useState<View>();
8-
const currentWorkspaceId = useCurrentWorkspaceId();
6+
const view = useAppView(viewId);
97
const [publishInfo, setPublishInfo] = React.useState<{
108
namespace: string,
119
publishName: string,
@@ -18,29 +16,12 @@ export function useLoadPublishInfo (viewId: string) {
1816
const currentUser = useCurrentUser();
1917
const isOwner = userWorkspaceInfo?.selectedWorkspace?.owner?.uid.toString() === currentUser?.uid.toString();
2018
const isPublisher = publishInfo?.publisherEmail === currentUser?.email;
21-
const loadView = useCallback(async () => {
22-
if (!viewId || !service || !currentWorkspaceId) return;
23-
try {
24-
25-
const view = await service.getAppView(currentWorkspaceId, viewId);
2619

27-
setView(view);
28-
return view;
29-
// eslint-disable-next-line
30-
} catch (e: any) {
31-
// do nothing
32-
console.error(e);
33-
}
34-
}, [currentWorkspaceId, service, viewId]);
3520
const loadPublishInfo = useCallback(async () => {
3621
if (!service) return;
3722
setLoading(true);
3823
try {
39-
const view = await loadView();
40-
41-
if (!view) return;
42-
43-
const res = await service.getPublishInfo(view?.view_id);
24+
const res = await service.getPublishInfo(viewId);
4425

4526
setPublishInfo(res);
4627

@@ -50,7 +31,7 @@ export function useLoadPublishInfo (viewId: string) {
5031
}
5132

5233
setLoading(false);
53-
}, [loadView, service]);
34+
}, [viewId, service]);
5435

5536
useEffect(() => {
5637
void loadPublishInfo();

0 commit comments

Comments
 (0)