Skip to content

Commit 8ac85eb

Browse files
authored
feat: support upload page icon (#59)
1 parent 2c322d1 commit 8ac85eb

File tree

10 files changed

+149
-41
lines changed

10 files changed

+149
-41
lines changed

src/application/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ export interface ViewMetaProps {
996996
extra?: ViewExtra | null;
997997
readOnly?: boolean;
998998
updatePage?: (viewId: string, data: UpdatePagePayload) => Promise<void>;
999+
uploadFile?: (file: File) => Promise<string>;
9991000
onEnter?: (text: string) => void;
10001001
maxWidth?: number;
10011002
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export default function LoadingDots({
2+
className,
3+
colors = ['#00b5ff', '#e3006d', '#f7931e'],
4+
}: {
5+
className?: string;
6+
colors?: [string, string, string];
7+
}) {
8+
return (
9+
<div className={className}>
10+
<div
11+
style={{
12+
width: `30px`,
13+
aspectRatio: '2',
14+
background: `
15+
radial-gradient(circle closest-side, ${colors[0]} 90%, transparent) 0% 50%,
16+
radial-gradient(circle closest-side, ${colors[1]} 90%, transparent) 50% 50%,
17+
radial-gradient(circle closest-side, ${colors[2]} 90%, transparent) 100% 50%
18+
`,
19+
backgroundSize: 'calc(100%/3) 50%',
20+
backgroundRepeat: 'no-repeat',
21+
animation: 'dots-loading 1s infinite linear',
22+
}}
23+
/>
24+
</div>
25+
);
26+
}

src/components/_shared/image-upload/UploadImage.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FileDropzone from '@/components/_shared/file-dropzone/FileDropzone';
2+
import LoadingDots from '@/components/_shared/LoadingDots';
23
import { notify } from '@/components/_shared/notify';
34
import React, { useCallback } from 'react';
45
import { useTranslation } from 'react-i18next';
@@ -10,24 +11,28 @@ export function UploadImage({ onDone, uploadAction }: {
1011
uploadAction?: (file: File) => Promise<string>
1112
}) {
1213
const { t } = useTranslation();
13-
const handleFileChange = useCallback(async (files: File[]) => {
14+
const [loading, setLoading] = React.useState(false);
15+
const handleFileChange = useCallback(async(files: File[]) => {
16+
setLoading(true);
1417
const file = files[0];
1518

16-
if (!file) return;
19+
if(!file) return;
1720

1821
try {
1922
const url = await uploadAction?.(file);
2023

21-
if (!url) {
24+
if(!url) {
2225
onDone?.(URL.createObjectURL(file));
2326
return;
2427
}
2528

2629
onDone?.(url);
2730
// eslint-disable-next-line
28-
} catch (e: any) {
31+
} catch(e: any) {
2932
notify.error(e.message);
3033
onDone?.(URL.createObjectURL(file));
34+
} finally {
35+
setLoading(false);
3136
}
3237

3338
}, [onDone, uploadAction]);
@@ -39,6 +44,10 @@ export function UploadImage({ onDone, uploadAction }: {
3944
onChange={handleFileChange}
4045
accept={ALLOWED_IMAGE_EXTENSIONS.join(',')}
4146
/>
47+
{loading &&
48+
<div className={'absolute bg-bg-body z-10 opacity-90 flex items-center inset-0 justify-center w-full h-full'}>
49+
<LoadingDots />
50+
</div>}
4251
</div>
4352

4453
);

src/components/_shared/view-icon/ChangeIconPopover.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { ViewIconType } from '@/application/types';
22
import { EmojiPicker } from '@/components/_shared/emoji-picker';
33
import IconPicker from '@/components/_shared/icon-picker/IconPicker';
4+
import { UploadImage } from '@/components/_shared/image-upload';
45
import { Popover } from '@/components/_shared/popover';
56
import { TabPanel, ViewTab, ViewTabs } from '@/components/_shared/tabs/ViewTabs';
67
import { Button } from '@mui/material';
78
import { PopoverProps } from '@mui/material/Popover';
89
import React, { useState } from 'react';
910
import { useTranslation } from 'react-i18next';
1011

11-
function ChangeIconPopover ({
12+
function ChangeIconPopover({
1213
open,
1314
anchorEl,
1415
onClose,
@@ -20,16 +21,20 @@ function ChangeIconPopover ({
2021
removeIcon,
2122
anchorPosition,
2223
hideRemove,
24+
uploadEnabled,
25+
onUploadFile,
2326
}: {
2427
open: boolean,
2528
anchorEl?: HTMLElement | null,
2629
anchorPosition?: PopoverProps['anchorPosition'],
2730
onClose: () => void,
28-
defaultType: 'emoji' | 'icon',
31+
defaultType: 'emoji' | 'icon' | 'upload',
2932
emojiEnabled?: boolean,
33+
uploadEnabled?: boolean,
3034
iconEnabled?: boolean,
3135
popoverProps?: Partial<PopoverProps>,
3236
onSelectIcon?: (icon: { ty: ViewIconType, value: string, color?: string, content?: string }) => void,
37+
onUploadFile?: (file: File) => Promise<string>,
3338
removeIcon?: () => void,
3439
hideRemove?: boolean,
3540
}) {
@@ -80,6 +85,16 @@ function ChangeIconPopover ({
8085
/>
8186
)
8287
}
88+
{
89+
uploadEnabled && (
90+
<ViewTab
91+
className={'flex items-center flex-row justify-center gap-1.5'}
92+
value={'upload'}
93+
label={'Upload'}
94+
data-testid="upload-tab"
95+
/>
96+
)
97+
}
8398

8499
</ViewTabs>
85100
{!hideRemove && <Button
@@ -126,6 +141,24 @@ function ChangeIconPopover ({
126141
hideRemove
127142
/>
128143
</TabPanel>}
144+
{uploadEnabled && <TabPanel
145+
index={'upload'}
146+
value={value}
147+
>
148+
<div className={'pt-4 relative pb-2'}>
149+
<UploadImage
150+
onDone={(url) => {
151+
onSelectIcon?.({
152+
ty: ViewIconType.URL,
153+
value: url,
154+
});
155+
handleClose();
156+
}}
157+
uploadAction={onUploadFile}
158+
/>
159+
</div>
160+
161+
</TabPanel>}
129162
</Popover>
130163
);
131164
}

src/components/app/DatabaseView.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ import React, { Suspense, useCallback, useMemo } from 'react';
1616
import { useSearchParams } from 'react-router-dom';
1717
import ViewMetaPreview from 'src/components/view-meta/ViewMetaPreview';
1818

19-
function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
19+
function DatabaseView({ viewMeta, uploadFile, ...props }: ViewComponentProps) {
2020
const [search, setSearch] = useSearchParams();
2121
const outline = useAppOutline();
2222
const iidIndex = viewMeta.viewId;
2323
const view = useMemo(() => {
24-
if (!outline || !iidIndex) return;
24+
if(!outline || !iidIndex) return;
2525
return findView(outline || [], iidIndex);
2626
}, [outline, iidIndex]);
2727

2828
const visibleViewIds = useMemo(() => {
29-
if (!view) return [];
29+
if(!view) return [];
3030
return [view.view_id, ...(view.children?.map(v => v.view_id) || [])];
3131
}, [view]);
3232

@@ -58,11 +58,11 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
5858
const doc = props.doc;
5959
const database = doc?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
6060
const skeleton = useMemo(() => {
61-
if (rowId) {
61+
if(rowId) {
6262
return <DocumentSkeleton />;
6363
}
6464

65-
switch (viewMeta.layout) {
65+
switch(viewMeta.layout) {
6666
case ViewLayout.Grid:
6767
return <GridSkeleton includeTitle={false} />;
6868
case ViewLayout.Board:
@@ -74,7 +74,7 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
7474
}
7575
}, [rowId, viewMeta.layout]);
7676

77-
if (!viewId || !doc || !database) return null;
77+
if(!viewId || !doc || !database) return null;
7878

7979
return (
8080
<div
@@ -87,6 +87,7 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
8787
{...viewMeta}
8888
readOnly={props.readOnly}
8989
updatePage={props.updatePage}
90+
uploadFile={uploadFile}
9091
/>}
9192

9293
<Suspense fallback={skeleton}>

src/components/app/outline/ViewItem.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
4040
const selectedViewId = useAppViewId();
4141
const viewId = view.view_id;
4242
const selected = selectedViewId === viewId;
43-
const { updatePage } = useAppHandlers();
43+
const { updatePage, uploadFile } = useAppHandlers();
4444

4545
const isExpanded = expandIds.includes(viewId);
4646
const [hovered, setHovered] = React.useState<boolean>(false);
@@ -57,6 +57,11 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
5757
/></span>;
5858
}, [isExpanded, level, toggleExpand, viewId]);
5959

60+
const onUploadFile = useCallback(async(file: File) => {
61+
if(!uploadFile) return Promise.reject();
62+
return uploadFile(viewId, file);
63+
}, [uploadFile, viewId]);
64+
6065
const renderItem = useMemo(() => {
6166
if(!view) return null;
6267

@@ -165,6 +170,8 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
165170
onClose={() => {
166171
setIconPopoverAnchorEl(null);
167172
}}
173+
uploadEnabled
174+
onUploadFile={onUploadFile}
168175
popoverProps={popoverProps}
169176
onSelectIcon={(icon) => {
170177
if(icon.ty === ViewIconType.Icon) {

src/components/app/view-actions/MorePageActions.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,35 @@ function MorePageActions({ view, onClose }: {
3838

3939
const {
4040
updatePage,
41+
uploadFile,
4142
} = useAppHandlers();
4243
const { t } = useTranslation();
4344

44-
const handleChangeIcon = useCallback(async (icon: { ty: ViewIconType, value: string }) => {
45+
const viewId = view.view_id;
46+
47+
const onUploadFile = useCallback(async(file: File) => {
48+
if(!uploadFile) return Promise.reject();
49+
return uploadFile(viewId, file);
50+
}, [uploadFile, viewId]);
51+
52+
const handleChangeIcon = useCallback(async(icon: { ty: ViewIconType, value: string, color?: string }) => {
4553
try {
4654
await updatePage?.(view.view_id, {
47-
icon: icon,
55+
icon: icon.ty === ViewIconType.Icon ? {
56+
ty: ViewIconType.Icon,
57+
value: JSON.stringify({
58+
color: icon.color,
59+
groupName: icon.value.split('/')[0],
60+
iconName: icon.value.split('/')[1],
61+
}),
62+
} : icon,
4863
name: view.name,
4964
extra: view.extra || {},
5065
});
5166
setIconPopoverAnchorEl(null);
5267
onClose?.();
5368
// eslint-disable-next-line
54-
} catch (e: any) {
69+
} catch(e: any) {
5570
notify.error(e);
5671
}
5772
}, [onClose, updatePage, view.extra, view.name, view.view_id]);
@@ -63,14 +78,14 @@ function MorePageActions({ view, onClose }: {
6378
const actions = useMemo(() => {
6479
return [{
6580
label: t('button.rename'),
66-
icon: <EditIcon/>,
81+
icon: <EditIcon />,
6782
onClick: () => {
6883
setRenameModalOpen(true);
6984
onClose?.();
7085
},
7186
}, {
7287
label: t('disclosureAction.changeIcon'),
73-
icon: <ChangeIcon/>,
88+
icon: <ChangeIcon />,
7489
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
7590
setIconPopoverAnchorEl(e.currentTarget);
7691
},
@@ -96,32 +111,34 @@ function MorePageActions({ view, onClose }: {
96111
viewId={view.view_id}
97112
movePopoverOrigins={popoverProps}
98113
/>
99-
<Divider className={'w-full'}/>
114+
<Divider className={'w-full'} />
100115
<Button
101116
size={'small'}
102117

103118
className={'px-3 py-1 justify-start'}
104119
color={'inherit'}
105120
onClick={() => {
106-
if (!currentWorkspaceId) return;
121+
if(!currentWorkspaceId) return;
107122
onClose?.();
108123
window.open(`/app/${currentWorkspaceId}/${view.view_id}`, '_blank');
109124

110125
}}
111-
startIcon={<OpenInBrowserIcon className={'w-4 h-4'}/>}
126+
startIcon={<OpenInBrowserIcon className={'w-4 h-4'} />}
112127
>
113128
{t('disclosureAction.openNewTab')}
114129
</Button>
115130
<Suspense fallback={null}>
116131
<ChangeIconPopover
117-
iconEnabled={false}
132+
iconEnabled
118133
defaultType={'emoji'}
119134
open={openIconPopover}
120135
anchorEl={iconPopoverAnchorEl}
121136
onClose={() => {
122137
onClose?.();
123138
setIconPopoverAnchorEl(null);
124139
}}
140+
onUploadFile={onUploadFile}
141+
uploadEnabled
125142
popoverProps={popoverProps}
126143
onSelectIcon={handleChangeIcon}
127144
removeIcon={handleRemoveIcon}

src/components/document/Document.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const Document = (props: DocumentProps) => {
2424
updatePage,
2525
onRendered,
2626
onEditorConnected,
27+
uploadFile,
2728
} = props;
2829
const blockId = search.get('blockId') || undefined;
2930

@@ -76,6 +77,7 @@ export const Document = (props: DocumentProps) => {
7677
updatePage={updatePage}
7778
onEnter={readOnly ? undefined : handleEnter}
7879
maxWidth={988}
80+
uploadFile={uploadFile}
7981
/>
8082
<Suspense fallback={<EditorSkeleton />}>
8183
<div className={'flex justify-center w-full'}>

src/components/view-meta/AddIconCover.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function AddIconCover({
1515
setIconAnchorEl,
1616
maxWidth,
1717
visible,
18+
onUploadFile,
1819
}: {
1920
visible: boolean;
2021
hasIcon: boolean;
@@ -24,6 +25,7 @@ function AddIconCover({
2425
iconAnchorEl: HTMLElement | null;
2526
setIconAnchorEl: (el: HTMLElement | null) => void;
2627
maxWidth?: number;
28+
onUploadFile: (file: File) => Promise<string>;
2729
}) {
2830
const { t } = useTranslation();
2931

@@ -80,6 +82,8 @@ function AddIconCover({
8082
setIconAnchorEl(null);
8183
onUpdateIcon?.({ ty: ViewIconType.Emoji, value: '' });
8284
}}
85+
uploadEnabled
86+
onUploadFile={onUploadFile}
8387
/>
8488
</>
8589

0 commit comments

Comments
 (0)