mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-12-15 12:28:36 +08:00
feat: support upload page icon (#59)
This commit is contained in:
@@ -996,6 +996,7 @@ export interface ViewMetaProps {
|
|||||||
extra?: ViewExtra | null;
|
extra?: ViewExtra | null;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
updatePage?: (viewId: string, data: UpdatePagePayload) => Promise<void>;
|
updatePage?: (viewId: string, data: UpdatePagePayload) => Promise<void>;
|
||||||
|
uploadFile?: (file: File) => Promise<string>;
|
||||||
onEnter?: (text: string) => void;
|
onEnter?: (text: string) => void;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/components/_shared/LoadingDots.tsx
Normal file
26
src/components/_shared/LoadingDots.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export default function LoadingDots({
|
||||||
|
className,
|
||||||
|
colors = ['#00b5ff', '#e3006d', '#f7931e'],
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
colors?: [string, string, string];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: `30px`,
|
||||||
|
aspectRatio: '2',
|
||||||
|
background: `
|
||||||
|
radial-gradient(circle closest-side, ${colors[0]} 90%, transparent) 0% 50%,
|
||||||
|
radial-gradient(circle closest-side, ${colors[1]} 90%, transparent) 50% 50%,
|
||||||
|
radial-gradient(circle closest-side, ${colors[2]} 90%, transparent) 100% 50%
|
||||||
|
`,
|
||||||
|
backgroundSize: 'calc(100%/3) 50%',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
animation: 'dots-loading 1s infinite linear',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import FileDropzone from '@/components/_shared/file-dropzone/FileDropzone';
|
import FileDropzone from '@/components/_shared/file-dropzone/FileDropzone';
|
||||||
|
import LoadingDots from '@/components/_shared/LoadingDots';
|
||||||
import { notify } from '@/components/_shared/notify';
|
import { notify } from '@/components/_shared/notify';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -10,24 +11,28 @@ export function UploadImage({ onDone, uploadAction }: {
|
|||||||
uploadAction?: (file: File) => Promise<string>
|
uploadAction?: (file: File) => Promise<string>
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleFileChange = useCallback(async (files: File[]) => {
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const handleFileChange = useCallback(async(files: File[]) => {
|
||||||
|
setLoading(true);
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
if (!file) return;
|
if(!file) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = await uploadAction?.(file);
|
const url = await uploadAction?.(file);
|
||||||
|
|
||||||
if (!url) {
|
if(!url) {
|
||||||
onDone?.(URL.createObjectURL(file));
|
onDone?.(URL.createObjectURL(file));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDone?.(url);
|
onDone?.(url);
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
} catch (e: any) {
|
} catch(e: any) {
|
||||||
notify.error(e.message);
|
notify.error(e.message);
|
||||||
onDone?.(URL.createObjectURL(file));
|
onDone?.(URL.createObjectURL(file));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [onDone, uploadAction]);
|
}, [onDone, uploadAction]);
|
||||||
@@ -39,6 +44,10 @@ export function UploadImage({ onDone, uploadAction }: {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept={ALLOWED_IMAGE_EXTENSIONS.join(',')}
|
accept={ALLOWED_IMAGE_EXTENSIONS.join(',')}
|
||||||
/>
|
/>
|
||||||
|
{loading &&
|
||||||
|
<div className={'absolute bg-bg-body z-10 opacity-90 flex items-center inset-0 justify-center w-full h-full'}>
|
||||||
|
<LoadingDots />
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ViewIconType } from '@/application/types';
|
import { ViewIconType } from '@/application/types';
|
||||||
import { EmojiPicker } from '@/components/_shared/emoji-picker';
|
import { EmojiPicker } from '@/components/_shared/emoji-picker';
|
||||||
import IconPicker from '@/components/_shared/icon-picker/IconPicker';
|
import IconPicker from '@/components/_shared/icon-picker/IconPicker';
|
||||||
|
import { UploadImage } from '@/components/_shared/image-upload';
|
||||||
import { Popover } from '@/components/_shared/popover';
|
import { Popover } from '@/components/_shared/popover';
|
||||||
import { TabPanel, ViewTab, ViewTabs } from '@/components/_shared/tabs/ViewTabs';
|
import { TabPanel, ViewTab, ViewTabs } from '@/components/_shared/tabs/ViewTabs';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
@@ -8,7 +9,7 @@ import { PopoverProps } from '@mui/material/Popover';
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function ChangeIconPopover ({
|
function ChangeIconPopover({
|
||||||
open,
|
open,
|
||||||
anchorEl,
|
anchorEl,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -20,16 +21,20 @@ function ChangeIconPopover ({
|
|||||||
removeIcon,
|
removeIcon,
|
||||||
anchorPosition,
|
anchorPosition,
|
||||||
hideRemove,
|
hideRemove,
|
||||||
|
uploadEnabled,
|
||||||
|
onUploadFile,
|
||||||
}: {
|
}: {
|
||||||
open: boolean,
|
open: boolean,
|
||||||
anchorEl?: HTMLElement | null,
|
anchorEl?: HTMLElement | null,
|
||||||
anchorPosition?: PopoverProps['anchorPosition'],
|
anchorPosition?: PopoverProps['anchorPosition'],
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
defaultType: 'emoji' | 'icon',
|
defaultType: 'emoji' | 'icon' | 'upload',
|
||||||
emojiEnabled?: boolean,
|
emojiEnabled?: boolean,
|
||||||
|
uploadEnabled?: boolean,
|
||||||
iconEnabled?: boolean,
|
iconEnabled?: boolean,
|
||||||
popoverProps?: Partial<PopoverProps>,
|
popoverProps?: Partial<PopoverProps>,
|
||||||
onSelectIcon?: (icon: { ty: ViewIconType, value: string, color?: string, content?: string }) => void,
|
onSelectIcon?: (icon: { ty: ViewIconType, value: string, color?: string, content?: string }) => void,
|
||||||
|
onUploadFile?: (file: File) => Promise<string>,
|
||||||
removeIcon?: () => void,
|
removeIcon?: () => void,
|
||||||
hideRemove?: boolean,
|
hideRemove?: boolean,
|
||||||
}) {
|
}) {
|
||||||
@@ -80,6 +85,16 @@ function ChangeIconPopover ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
uploadEnabled && (
|
||||||
|
<ViewTab
|
||||||
|
className={'flex items-center flex-row justify-center gap-1.5'}
|
||||||
|
value={'upload'}
|
||||||
|
label={'Upload'}
|
||||||
|
data-testid="upload-tab"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
</ViewTabs>
|
</ViewTabs>
|
||||||
{!hideRemove && <Button
|
{!hideRemove && <Button
|
||||||
@@ -126,6 +141,24 @@ function ChangeIconPopover ({
|
|||||||
hideRemove
|
hideRemove
|
||||||
/>
|
/>
|
||||||
</TabPanel>}
|
</TabPanel>}
|
||||||
|
{uploadEnabled && <TabPanel
|
||||||
|
index={'upload'}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<div className={'pt-4 relative pb-2'}>
|
||||||
|
<UploadImage
|
||||||
|
onDone={(url) => {
|
||||||
|
onSelectIcon?.({
|
||||||
|
ty: ViewIconType.URL,
|
||||||
|
value: url,
|
||||||
|
});
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
uploadAction={onUploadFile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</TabPanel>}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ import React, { Suspense, useCallback, useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import ViewMetaPreview from 'src/components/view-meta/ViewMetaPreview';
|
import ViewMetaPreview from 'src/components/view-meta/ViewMetaPreview';
|
||||||
|
|
||||||
function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
|
function DatabaseView({ viewMeta, uploadFile, ...props }: ViewComponentProps) {
|
||||||
const [search, setSearch] = useSearchParams();
|
const [search, setSearch] = useSearchParams();
|
||||||
const outline = useAppOutline();
|
const outline = useAppOutline();
|
||||||
const iidIndex = viewMeta.viewId;
|
const iidIndex = viewMeta.viewId;
|
||||||
const view = useMemo(() => {
|
const view = useMemo(() => {
|
||||||
if (!outline || !iidIndex) return;
|
if(!outline || !iidIndex) return;
|
||||||
return findView(outline || [], iidIndex);
|
return findView(outline || [], iidIndex);
|
||||||
}, [outline, iidIndex]);
|
}, [outline, iidIndex]);
|
||||||
|
|
||||||
const visibleViewIds = useMemo(() => {
|
const visibleViewIds = useMemo(() => {
|
||||||
if (!view) return [];
|
if(!view) return [];
|
||||||
return [view.view_id, ...(view.children?.map(v => v.view_id) || [])];
|
return [view.view_id, ...(view.children?.map(v => v.view_id) || [])];
|
||||||
}, [view]);
|
}, [view]);
|
||||||
|
|
||||||
@@ -58,11 +58,11 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
|
|||||||
const doc = props.doc;
|
const doc = props.doc;
|
||||||
const database = doc?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
|
const database = doc?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
|
||||||
const skeleton = useMemo(() => {
|
const skeleton = useMemo(() => {
|
||||||
if (rowId) {
|
if(rowId) {
|
||||||
return <DocumentSkeleton />;
|
return <DocumentSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (viewMeta.layout) {
|
switch(viewMeta.layout) {
|
||||||
case ViewLayout.Grid:
|
case ViewLayout.Grid:
|
||||||
return <GridSkeleton includeTitle={false} />;
|
return <GridSkeleton includeTitle={false} />;
|
||||||
case ViewLayout.Board:
|
case ViewLayout.Board:
|
||||||
@@ -74,7 +74,7 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
|
|||||||
}
|
}
|
||||||
}, [rowId, viewMeta.layout]);
|
}, [rowId, viewMeta.layout]);
|
||||||
|
|
||||||
if (!viewId || !doc || !database) return null;
|
if(!viewId || !doc || !database) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -87,6 +87,7 @@ function DatabaseView ({ viewMeta, ...props }: ViewComponentProps) {
|
|||||||
{...viewMeta}
|
{...viewMeta}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
updatePage={props.updatePage}
|
updatePage={props.updatePage}
|
||||||
|
uploadFile={uploadFile}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
<Suspense fallback={skeleton}>
|
<Suspense fallback={skeleton}>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
|
|||||||
const selectedViewId = useAppViewId();
|
const selectedViewId = useAppViewId();
|
||||||
const viewId = view.view_id;
|
const viewId = view.view_id;
|
||||||
const selected = selectedViewId === viewId;
|
const selected = selectedViewId === viewId;
|
||||||
const { updatePage } = useAppHandlers();
|
const { updatePage, uploadFile } = useAppHandlers();
|
||||||
|
|
||||||
const isExpanded = expandIds.includes(viewId);
|
const isExpanded = expandIds.includes(viewId);
|
||||||
const [hovered, setHovered] = React.useState<boolean>(false);
|
const [hovered, setHovered] = React.useState<boolean>(false);
|
||||||
@@ -57,6 +57,11 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
|
|||||||
/></span>;
|
/></span>;
|
||||||
}, [isExpanded, level, toggleExpand, viewId]);
|
}, [isExpanded, level, toggleExpand, viewId]);
|
||||||
|
|
||||||
|
const onUploadFile = useCallback(async(file: File) => {
|
||||||
|
if(!uploadFile) return Promise.reject();
|
||||||
|
return uploadFile(viewId, file);
|
||||||
|
}, [uploadFile, viewId]);
|
||||||
|
|
||||||
const renderItem = useMemo(() => {
|
const renderItem = useMemo(() => {
|
||||||
if(!view) return null;
|
if(!view) return null;
|
||||||
|
|
||||||
@@ -165,6 +170,8 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIconPopoverAnchorEl(null);
|
setIconPopoverAnchorEl(null);
|
||||||
}}
|
}}
|
||||||
|
uploadEnabled
|
||||||
|
onUploadFile={onUploadFile}
|
||||||
popoverProps={popoverProps}
|
popoverProps={popoverProps}
|
||||||
onSelectIcon={(icon) => {
|
onSelectIcon={(icon) => {
|
||||||
if(icon.ty === ViewIconType.Icon) {
|
if(icon.ty === ViewIconType.Icon) {
|
||||||
|
|||||||
@@ -38,20 +38,35 @@ function MorePageActions({ view, onClose }: {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
updatePage,
|
updatePage,
|
||||||
|
uploadFile,
|
||||||
} = useAppHandlers();
|
} = useAppHandlers();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleChangeIcon = useCallback(async (icon: { ty: ViewIconType, value: string }) => {
|
const viewId = view.view_id;
|
||||||
|
|
||||||
|
const onUploadFile = useCallback(async(file: File) => {
|
||||||
|
if(!uploadFile) return Promise.reject();
|
||||||
|
return uploadFile(viewId, file);
|
||||||
|
}, [uploadFile, viewId]);
|
||||||
|
|
||||||
|
const handleChangeIcon = useCallback(async(icon: { ty: ViewIconType, value: string, color?: string }) => {
|
||||||
try {
|
try {
|
||||||
await updatePage?.(view.view_id, {
|
await updatePage?.(view.view_id, {
|
||||||
icon: icon,
|
icon: icon.ty === ViewIconType.Icon ? {
|
||||||
|
ty: ViewIconType.Icon,
|
||||||
|
value: JSON.stringify({
|
||||||
|
color: icon.color,
|
||||||
|
groupName: icon.value.split('/')[0],
|
||||||
|
iconName: icon.value.split('/')[1],
|
||||||
|
}),
|
||||||
|
} : icon,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
extra: view.extra || {},
|
extra: view.extra || {},
|
||||||
});
|
});
|
||||||
setIconPopoverAnchorEl(null);
|
setIconPopoverAnchorEl(null);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
} catch (e: any) {
|
} catch(e: any) {
|
||||||
notify.error(e);
|
notify.error(e);
|
||||||
}
|
}
|
||||||
}, [onClose, updatePage, view.extra, view.name, view.view_id]);
|
}, [onClose, updatePage, view.extra, view.name, view.view_id]);
|
||||||
@@ -63,14 +78,14 @@ function MorePageActions({ view, onClose }: {
|
|||||||
const actions = useMemo(() => {
|
const actions = useMemo(() => {
|
||||||
return [{
|
return [{
|
||||||
label: t('button.rename'),
|
label: t('button.rename'),
|
||||||
icon: <EditIcon/>,
|
icon: <EditIcon />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRenameModalOpen(true);
|
setRenameModalOpen(true);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
label: t('disclosureAction.changeIcon'),
|
label: t('disclosureAction.changeIcon'),
|
||||||
icon: <ChangeIcon/>,
|
icon: <ChangeIcon />,
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setIconPopoverAnchorEl(e.currentTarget);
|
setIconPopoverAnchorEl(e.currentTarget);
|
||||||
},
|
},
|
||||||
@@ -96,25 +111,25 @@ function MorePageActions({ view, onClose }: {
|
|||||||
viewId={view.view_id}
|
viewId={view.view_id}
|
||||||
movePopoverOrigins={popoverProps}
|
movePopoverOrigins={popoverProps}
|
||||||
/>
|
/>
|
||||||
<Divider className={'w-full'}/>
|
<Divider className={'w-full'} />
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
|
|
||||||
className={'px-3 py-1 justify-start'}
|
className={'px-3 py-1 justify-start'}
|
||||||
color={'inherit'}
|
color={'inherit'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!currentWorkspaceId) return;
|
if(!currentWorkspaceId) return;
|
||||||
onClose?.();
|
onClose?.();
|
||||||
window.open(`/app/${currentWorkspaceId}/${view.view_id}`, '_blank');
|
window.open(`/app/${currentWorkspaceId}/${view.view_id}`, '_blank');
|
||||||
|
|
||||||
}}
|
}}
|
||||||
startIcon={<OpenInBrowserIcon className={'w-4 h-4'}/>}
|
startIcon={<OpenInBrowserIcon className={'w-4 h-4'} />}
|
||||||
>
|
>
|
||||||
{t('disclosureAction.openNewTab')}
|
{t('disclosureAction.openNewTab')}
|
||||||
</Button>
|
</Button>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ChangeIconPopover
|
<ChangeIconPopover
|
||||||
iconEnabled={false}
|
iconEnabled
|
||||||
defaultType={'emoji'}
|
defaultType={'emoji'}
|
||||||
open={openIconPopover}
|
open={openIconPopover}
|
||||||
anchorEl={iconPopoverAnchorEl}
|
anchorEl={iconPopoverAnchorEl}
|
||||||
@@ -122,6 +137,8 @@ function MorePageActions({ view, onClose }: {
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
setIconPopoverAnchorEl(null);
|
setIconPopoverAnchorEl(null);
|
||||||
}}
|
}}
|
||||||
|
onUploadFile={onUploadFile}
|
||||||
|
uploadEnabled
|
||||||
popoverProps={popoverProps}
|
popoverProps={popoverProps}
|
||||||
onSelectIcon={handleChangeIcon}
|
onSelectIcon={handleChangeIcon}
|
||||||
removeIcon={handleRemoveIcon}
|
removeIcon={handleRemoveIcon}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const Document = (props: DocumentProps) => {
|
|||||||
updatePage,
|
updatePage,
|
||||||
onRendered,
|
onRendered,
|
||||||
onEditorConnected,
|
onEditorConnected,
|
||||||
|
uploadFile,
|
||||||
} = props;
|
} = props;
|
||||||
const blockId = search.get('blockId') || undefined;
|
const blockId = search.get('blockId') || undefined;
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export const Document = (props: DocumentProps) => {
|
|||||||
updatePage={updatePage}
|
updatePage={updatePage}
|
||||||
onEnter={readOnly ? undefined : handleEnter}
|
onEnter={readOnly ? undefined : handleEnter}
|
||||||
maxWidth={988}
|
maxWidth={988}
|
||||||
|
uploadFile={uploadFile}
|
||||||
/>
|
/>
|
||||||
<Suspense fallback={<EditorSkeleton />}>
|
<Suspense fallback={<EditorSkeleton />}>
|
||||||
<div className={'flex justify-center w-full'}>
|
<div className={'flex justify-center w-full'}>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function AddIconCover({
|
|||||||
setIconAnchorEl,
|
setIconAnchorEl,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
visible,
|
visible,
|
||||||
|
onUploadFile,
|
||||||
}: {
|
}: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
hasIcon: boolean;
|
hasIcon: boolean;
|
||||||
@@ -24,6 +25,7 @@ function AddIconCover({
|
|||||||
iconAnchorEl: HTMLElement | null;
|
iconAnchorEl: HTMLElement | null;
|
||||||
setIconAnchorEl: (el: HTMLElement | null) => void;
|
setIconAnchorEl: (el: HTMLElement | null) => void;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
|
onUploadFile: (file: File) => Promise<string>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -80,6 +82,8 @@ function AddIconCover({
|
|||||||
setIconAnchorEl(null);
|
setIconAnchorEl(null);
|
||||||
onUpdateIcon?.({ ty: ViewIconType.Emoji, value: '' });
|
onUpdateIcon?.({ ty: ViewIconType.Emoji, value: '' });
|
||||||
}}
|
}}
|
||||||
|
uploadEnabled
|
||||||
|
onUploadFile={onUploadFile}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { CoverType, ViewIconType, ViewLayout, ViewMetaCover, ViewMetaIcon, ViewM
|
|||||||
import { notify } from '@/components/_shared/notify';
|
import { notify } from '@/components/_shared/notify';
|
||||||
import TitleEditable from '@/components/view-meta/TitleEditable';
|
import TitleEditable from '@/components/view-meta/TitleEditable';
|
||||||
import ViewCover from '@/components/view-meta/ViewCover';
|
import ViewCover from '@/components/view-meta/ViewCover';
|
||||||
import React, { lazy, Suspense, useEffect, useMemo } from 'react';
|
import React, { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PageIcon from '@/components/_shared/view-icon/PageIcon';
|
import PageIcon from '@/components/_shared/view-icon/PageIcon';
|
||||||
|
|
||||||
const AddIconCover = lazy(() => import('@/components/view-meta/AddIconCover'));
|
const AddIconCover = lazy(() => import('@/components/view-meta/AddIconCover'));
|
||||||
|
|
||||||
export function ViewMetaPreview ({
|
export function ViewMetaPreview({
|
||||||
icon: iconProp,
|
icon: iconProp,
|
||||||
cover: coverProp,
|
cover: coverProp,
|
||||||
name,
|
name,
|
||||||
@@ -18,6 +18,7 @@ export function ViewMetaPreview ({
|
|||||||
updatePage,
|
updatePage,
|
||||||
onEnter,
|
onEnter,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
uploadFile,
|
||||||
}: ViewMetaProps) {
|
}: ViewMetaProps) {
|
||||||
const [iconAnchorEl, setIconAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [iconAnchorEl, setIconAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const [cover, setCover] = React.useState<ViewMetaCover | null>(coverProp || null);
|
const [cover, setCover] = React.useState<ViewMetaCover | null>(coverProp || null);
|
||||||
@@ -32,21 +33,21 @@ export function ViewMetaPreview ({
|
|||||||
}, [iconProp]);
|
}, [iconProp]);
|
||||||
|
|
||||||
const coverType = useMemo(() => {
|
const coverType = useMemo(() => {
|
||||||
if (cover && [CoverType.NormalColor, CoverType.GradientColor].includes(cover.type)) {
|
if(cover && [CoverType.NormalColor, CoverType.GradientColor].includes(cover.type)) {
|
||||||
return 'color';
|
return 'color';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CoverType.BuildInImage === cover?.type) {
|
if(CoverType.BuildInImage === cover?.type) {
|
||||||
return 'built_in';
|
return 'built_in';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(cover.type)) {
|
if(cover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(cover.type)) {
|
||||||
return 'custom';
|
return 'custom';
|
||||||
}
|
}
|
||||||
}, [cover]);
|
}, [cover]);
|
||||||
|
|
||||||
const coverValue = useMemo(() => {
|
const coverValue = useMemo(() => {
|
||||||
if (coverType === CoverType.BuildInImage) {
|
if(coverType === CoverType.BuildInImage) {
|
||||||
return {
|
return {
|
||||||
1: '/covers/m_cover_image_1.png',
|
1: '/covers/m_cover_image_1.png',
|
||||||
2: '/covers/m_cover_image_2.png',
|
2: '/covers/m_cover_image_2.png',
|
||||||
@@ -63,8 +64,8 @@ export function ViewMetaPreview ({
|
|||||||
|
|
||||||
const [isHover, setIsHover] = React.useState(false);
|
const [isHover, setIsHover] = React.useState(false);
|
||||||
|
|
||||||
const handleUpdateIcon = React.useCallback(async (icon: { ty: ViewIconType, value: string }) => {
|
const handleUpdateIcon = React.useCallback(async(icon: { ty: ViewIconType, value: string }) => {
|
||||||
if (!updatePage || !viewId) return;
|
if(!updatePage || !viewId) return;
|
||||||
setIcon(icon);
|
setIcon(icon);
|
||||||
try {
|
try {
|
||||||
await updatePage(viewId, {
|
await updatePage(viewId, {
|
||||||
@@ -73,15 +74,15 @@ export function ViewMetaPreview ({
|
|||||||
extra: extra || {},
|
extra: extra || {},
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
} catch (e: any) {
|
} catch(e: any) {
|
||||||
notify.error(e.message);
|
notify.error(e.message);
|
||||||
}
|
}
|
||||||
}, [updatePage, viewId, name, extra]);
|
}, [updatePage, viewId, name, extra]);
|
||||||
|
|
||||||
const handleUpdateName = React.useCallback(async (newName: string) => {
|
const handleUpdateName = React.useCallback(async(newName: string) => {
|
||||||
if (!updatePage || !viewId) return;
|
if(!updatePage || !viewId) return;
|
||||||
try {
|
try {
|
||||||
if (name === newName) return;
|
if(name === newName) return;
|
||||||
await updatePage(viewId, {
|
await updatePage(viewId, {
|
||||||
icon: icon || {
|
icon: icon || {
|
||||||
ty: ViewIconType.Emoji,
|
ty: ViewIconType.Emoji,
|
||||||
@@ -91,16 +92,16 @@ export function ViewMetaPreview ({
|
|||||||
extra: extra || {},
|
extra: extra || {},
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
} catch (e: any) {
|
} catch(e: any) {
|
||||||
notify.error(e.message);
|
notify.error(e.message);
|
||||||
}
|
}
|
||||||
}, [name, updatePage, viewId, icon, extra]);
|
}, [name, updatePage, viewId, icon, extra]);
|
||||||
|
|
||||||
const handleUpdateCover = React.useCallback(async (cover?: {
|
const handleUpdateCover = React.useCallback(async(cover?: {
|
||||||
type: CoverType;
|
type: CoverType;
|
||||||
value: string;
|
value: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!updatePage || !viewId) return;
|
if(!updatePage || !viewId) return;
|
||||||
setCover(cover ? cover : null);
|
setCover(cover ? cover : null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -116,11 +117,16 @@ export function ViewMetaPreview ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
} catch (e: any) {
|
} catch(e: any) {
|
||||||
notify.error(e.message);
|
notify.error(e.message);
|
||||||
}
|
}
|
||||||
}, [extra, icon, name, updatePage, viewId]);
|
}, [extra, icon, name, updatePage, viewId]);
|
||||||
|
|
||||||
|
const onUploadFile = useCallback(async(file: File) => {
|
||||||
|
if(!uploadFile) return Promise.reject();
|
||||||
|
return uploadFile(file);
|
||||||
|
}, [uploadFile]);
|
||||||
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -133,13 +139,13 @@ export function ViewMetaPreview ({
|
|||||||
setIsHover(false);
|
setIsHover(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (el) {
|
if(el) {
|
||||||
el.addEventListener('mouseenter', handleMouseEnter);
|
el.addEventListener('mouseenter', handleMouseEnter);
|
||||||
el.addEventListener('mouseleave', handleMouseLeave);
|
el.addEventListener('mouseleave', handleMouseLeave);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (el) {
|
if(el) {
|
||||||
el.removeEventListener('mouseenter', handleMouseEnter);
|
el.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
el.removeEventListener('mouseleave', handleMouseLeave);
|
el.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
}
|
}
|
||||||
@@ -174,8 +180,10 @@ export function ViewMetaPreview ({
|
|||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
iconAnchorEl={iconAnchorEl}
|
iconAnchorEl={iconAnchorEl}
|
||||||
setIconAnchorEl={setIconAnchorEl}
|
setIconAnchorEl={setIconAnchorEl}
|
||||||
|
onUploadFile={onUploadFile}
|
||||||
/></Suspense>}
|
/></Suspense>}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`relative mb-6 flex items-center overflow-visible w-full justify-center`}
|
className={`relative mb-6 flex items-center overflow-visible w-full justify-center`}
|
||||||
@@ -192,7 +200,7 @@ export function ViewMetaPreview ({
|
|||||||
{icon?.value ?
|
{icon?.value ?
|
||||||
<div
|
<div
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
if (readOnly) return;
|
if(readOnly) return;
|
||||||
setIconAnchorEl(e.currentTarget);
|
setIconAnchorEl(e.currentTarget);
|
||||||
}}
|
}}
|
||||||
className={`view-icon flex h-[1.25em] px-1.5 items-center justify-center ${readOnly ? 'cursor-default' : 'cursor-pointer hover:bg-fill-list-hover '}`}
|
className={`view-icon flex h-[1.25em] px-1.5 items-center justify-center ${readOnly ? 'cursor-default' : 'cursor-pointer hover:bg-fill-list-hover '}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user