From d25efba292bee34fa2e8af5e180c8b26b034a818 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:01:23 +0800 Subject: [PATCH] feat: support file block preview on web (#6081) --- .../src/application/collab.type.ts | 217 ++++++++++-------- .../appflowy_web_app/src/assets/download.svg | 15 ++ .../src/assets/file_upload.svg | 15 ++ .../block-actions/RightTopActions.tsx | 31 ++- .../block-actions/RightTopActionsToolbar.tsx | 10 +- .../components/blocks/file/FileBlock.tsx | 111 +++++++++ .../editor/components/blocks/file/index.ts | 1 + .../editor/components/element/Element.tsx | 3 + .../components/leaf/mention/MentionPage.tsx | 2 +- .../src/components/editor/editor.scss | 4 +- .../src/components/editor/editor.type.ts | 8 +- .../publish/header/BreadcrumbItem.tsx | 6 +- .../components/publish/header/SpaceIcon.tsx | 14 +- .../publish/header/duplicate/SpaceList.tsx | 18 +- .../appflowy_web_app/src/utils/download.ts | 27 +++ frontend/appflowy_web_app/src/utils/time.ts | 2 +- frontend/resources/translations/en.json | 13 +- 17 files changed, 366 insertions(+), 131 deletions(-) create mode 100644 frontend/appflowy_web_app/src/assets/download.svg create mode 100644 frontend/appflowy_web_app/src/assets/file_upload.svg create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/file/FileBlock.tsx create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/file/index.ts create mode 100644 frontend/appflowy_web_app/src/utils/download.ts diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 0040c49ffc..81378c8099 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -34,6 +34,7 @@ export enum BlockType { TableBlock = 'table', TableCell = 'table/cell', LinkPreview = 'link_preview', + FileBlock = 'file', } export enum InlineBlockType { @@ -85,6 +86,18 @@ export interface LinkPreviewBlockData extends BlockData { url?: string; } +export enum FieldURLType { + Upload = 2, + Link = 1, +} + +export interface FileBlockData extends BlockData { + name: string; + uploaded_at: number; + url: string; + url_type: FieldURLType; +} + export enum ImageType { Local = 0, Internal = 1, @@ -271,151 +284,151 @@ export enum YjsDatabaseKey { export interface YDoc extends Y.Doc { // eslint-disable-next-line @typescript-eslint/no-explicit-any - getMap(key: YjsEditorKey.data_section): YSharedRoot | any; + getMap (key: YjsEditorKey.data_section): YSharedRoot | any; } export interface YDatabaseRow extends Y.Map { - get(key: YjsDatabaseKey.id): RowId; + get (key: YjsDatabaseKey.id): RowId; - get(key: YjsDatabaseKey.height): string; + get (key: YjsDatabaseKey.height): string; - get(key: YjsDatabaseKey.visibility): boolean; + get (key: YjsDatabaseKey.visibility): boolean; - get(key: YjsDatabaseKey.created_at): CreatedAt; + get (key: YjsDatabaseKey.created_at): CreatedAt; - get(key: YjsDatabaseKey.last_modified): LastModified; + get (key: YjsDatabaseKey.last_modified): LastModified; - get(key: YjsDatabaseKey.cells): YDatabaseCells; + get (key: YjsDatabaseKey.cells): YDatabaseCells; } export interface YDatabaseCells extends Y.Map { - get(key: FieldId): YDatabaseCell; + get (key: FieldId): YDatabaseCell; } export type EndTimestamp = string; export type ReminderId = string; export interface YDatabaseCell extends Y.Map { - get(key: YjsDatabaseKey.created_at): CreatedAt; + get (key: YjsDatabaseKey.created_at): CreatedAt; - get(key: YjsDatabaseKey.last_modified): LastModified; + get (key: YjsDatabaseKey.last_modified): LastModified; - get(key: YjsDatabaseKey.field_type): string; + get (key: YjsDatabaseKey.field_type): string; - get(key: YjsDatabaseKey.data): object | string | boolean | number; + get (key: YjsDatabaseKey.data): object | string | boolean | number; - get(key: YjsDatabaseKey.end_timestamp): EndTimestamp; + get (key: YjsDatabaseKey.end_timestamp): EndTimestamp; - get(key: YjsDatabaseKey.include_time): boolean; + get (key: YjsDatabaseKey.include_time): boolean; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.is_range): boolean; + get (key: YjsDatabaseKey.is_range): boolean; - get(key: YjsDatabaseKey.reminder_id): ReminderId; + get (key: YjsDatabaseKey.reminder_id): ReminderId; } export interface YSharedRoot extends Y.Map { - get(key: YjsEditorKey.document): YDocument; + get (key: YjsEditorKey.document): YDocument; - get(key: YjsEditorKey.folder): YFolder; + get (key: YjsEditorKey.folder): YFolder; - get(key: YjsEditorKey.database): YDatabase; + get (key: YjsEditorKey.database): YDatabase; - get(key: YjsEditorKey.database_row): YDatabaseRow; + get (key: YjsEditorKey.database_row): YDatabaseRow; } export interface YFolder extends Y.Map { - get(key: YjsFolderKey.views): YViews; + get (key: YjsFolderKey.views): YViews; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.meta): YFolderMeta; + get (key: YjsFolderKey.meta): YFolderMeta; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.relation): YFolderRelation; + get (key: YjsFolderKey.relation): YFolderRelation; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.section): YFolderSection; + get (key: YjsFolderKey.section): YFolderSection; } export interface YViews extends Y.Map { - get(key: ViewId): YView; + get (key: ViewId): YView; } export interface YView extends Y.Map { - get(key: YjsFolderKey.id): ViewId; + get (key: YjsFolderKey.id): ViewId; - get(key: YjsFolderKey.bid): string; + get (key: YjsFolderKey.bid): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.name): string; + get (key: YjsFolderKey.name): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.icon | YjsFolderKey.extra): string; + get (key: YjsFolderKey.icon | YjsFolderKey.extra): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsFolderKey.layout): string; + get (key: YjsFolderKey.layout): string; } export interface YFolderRelation extends Y.Map { - get(key: ViewId): Y.Array; + get (key: ViewId): Y.Array; } export interface YFolderMeta extends Y.Map { - get(key: YjsFolderKey.current_view | YjsFolderKey.current_workspace): string; + get (key: YjsFolderKey.current_view | YjsFolderKey.current_workspace): string; } export interface YFolderSection extends Y.Map { - get(key: YjsFolderKey.favorite | YjsFolderKey.private | YjsFolderKey.recent | YjsFolderKey.trash): YFolderSectionItem; + get (key: YjsFolderKey.favorite | YjsFolderKey.private | YjsFolderKey.recent | YjsFolderKey.trash): YFolderSectionItem; } export interface YFolderSectionItem extends Y.Map { - get(key: string): Y.Array; + get (key: string): Y.Array; } export interface YDocument extends Y.Map { - get(key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; + get (key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; } export interface YBlocks extends Y.Map { - get(key: BlockId): YBlock; + get (key: BlockId): YBlock; } export interface YBlock extends Y.Map { - get(key: YjsEditorKey.block_id | YjsEditorKey.block_parent): BlockId; + get (key: YjsEditorKey.block_id | YjsEditorKey.block_parent): BlockId; - get(key: YjsEditorKey.block_type): BlockType; + get (key: YjsEditorKey.block_type): BlockType; - get(key: YjsEditorKey.block_data): string; + get (key: YjsEditorKey.block_data): string; - get(key: YjsEditorKey.block_children): ChildrenId; + get (key: YjsEditorKey.block_children): ChildrenId; - get(key: YjsEditorKey.block_external_id): ExternalId; + get (key: YjsEditorKey.block_external_id): ExternalId; } export interface YMeta extends Y.Map { - get(key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; + get (key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; } export interface YChildrenMap extends Y.Map { - get(key: ChildrenId): Y.Array; + get (key: ChildrenId): Y.Array; } export interface YTextMap extends Y.Map { - get(key: ExternalId): Y.Text; + get (key: ExternalId): Y.Text; } export interface YDatabase extends Y.Map { - get(key: YjsDatabaseKey.views): YDatabaseViews; + get (key: YjsDatabaseKey.views): YDatabaseViews; - get(key: YjsDatabaseKey.metas): YDatabaseMetas; + get (key: YjsDatabaseKey.metas): YDatabaseMetas; - get(key: YjsDatabaseKey.fields): YDatabaseFields; + get (key: YjsDatabaseKey.fields): YDatabaseFields; - get(key: YjsDatabaseKey.id): string; + get (key: YjsDatabaseKey.id): string; } export interface YDatabaseViews extends Y.Map { - get(key: ViewId): YDatabaseView; + get (key: ViewId): YDatabaseView; } export type DatabaseId = string; @@ -431,32 +444,32 @@ export enum DatabaseViewLayout { } export interface YDatabaseView extends Y.Map { - get(key: YjsDatabaseKey.database_id): DatabaseId; + get (key: YjsDatabaseKey.database_id): DatabaseId; - get(key: YjsDatabaseKey.name): string; + get (key: YjsDatabaseKey.name): string; - get(key: YjsDatabaseKey.created_at): CreatedAt; + get (key: YjsDatabaseKey.created_at): CreatedAt; - get(key: YjsDatabaseKey.modified_at): ModifiedAt; + get (key: YjsDatabaseKey.modified_at): ModifiedAt; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.layout): string; + get (key: YjsDatabaseKey.layout): string; - get(key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; + get (key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; - get(key: YjsDatabaseKey.filters): YDatabaseFilters; + get (key: YjsDatabaseKey.filters): YDatabaseFilters; - get(key: YjsDatabaseKey.groups): YDatabaseGroups; + get (key: YjsDatabaseKey.groups): YDatabaseGroups; - get(key: YjsDatabaseKey.sorts): YDatabaseSorts; + get (key: YjsDatabaseKey.sorts): YDatabaseSorts; - get(key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; + get (key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; - get(key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; + get (key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; - get(key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; + get (key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; - get(key: YjsDatabaseKey.calculations): YDatabaseCalculations; + get (key: YjsDatabaseKey.calculations): YDatabaseCalculations; } export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] @@ -477,128 +490,128 @@ export type GroupId = string; export interface YDatabaseLayoutSettings extends Y.Map { // DatabaseViewLayout.Board - get(key: '1'): YDatabaseBoardLayoutSetting; + get (key: '1'): YDatabaseBoardLayoutSetting; // DatabaseViewLayout.Calendar - get(key: '2'): YDatabaseCalendarLayoutSetting; + get (key: '2'): YDatabaseCalendarLayoutSetting; } export interface YDatabaseBoardLayoutSetting extends Y.Map { - get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; + get (key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; } export interface YDatabaseCalendarLayoutSetting extends Y.Map { - get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string; + get (key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string; - get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean; + get (key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean; } export interface YDatabaseGroup extends Y.Map { - get(key: YjsDatabaseKey.id): GroupId; + get (key: YjsDatabaseKey.id): GroupId; - get(key: YjsDatabaseKey.field_id): FieldId; + get (key: YjsDatabaseKey.field_id): FieldId; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.content): string; + get (key: YjsDatabaseKey.content): string; - get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns; + get (key: YjsDatabaseKey.groups): YDatabaseGroupColumns; } export type YDatabaseGroupColumns = Y.Array; export interface YDatabaseGroupColumn extends Y.Map { - get(key: YjsDatabaseKey.id): string; + get (key: YjsDatabaseKey.id): string; - get(key: YjsDatabaseKey.visible): boolean; + get (key: YjsDatabaseKey.visible): boolean; } export interface YDatabaseRowOrder extends Y.Map { - get(key: YjsDatabaseKey.id): SortId; + get (key: YjsDatabaseKey.id): SortId; - get(key: YjsDatabaseKey.height): number; + get (key: YjsDatabaseKey.height): number; } export interface YDatabaseSort extends Y.Map { - get(key: YjsDatabaseKey.id): SortId; + get (key: YjsDatabaseKey.id): SortId; - get(key: YjsDatabaseKey.field_id): FieldId; + get (key: YjsDatabaseKey.field_id): FieldId; - get(key: YjsDatabaseKey.condition): string; + get (key: YjsDatabaseKey.condition): string; } export type FilterId = string; export interface YDatabaseFilter extends Y.Map { - get(key: YjsDatabaseKey.id): FilterId; + get (key: YjsDatabaseKey.id): FilterId; - get(key: YjsDatabaseKey.field_id): FieldId; + get (key: YjsDatabaseKey.field_id): FieldId; - get(key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; + get (key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; } export interface YDatabaseCalculation extends Y.Map { - get(key: YjsDatabaseKey.field_id): FieldId; + get (key: YjsDatabaseKey.field_id): FieldId; - get(key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; + get (key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; } export interface YDatabaseFieldSettings extends Y.Map { - get(key: FieldId): YDatabaseFieldSetting; + get (key: FieldId): YDatabaseFieldSetting; } export interface YDatabaseFieldSetting extends Y.Map { - get(key: YjsDatabaseKey.visibility): string; + get (key: YjsDatabaseKey.visibility): string; - get(key: YjsDatabaseKey.wrap): boolean; + get (key: YjsDatabaseKey.wrap): boolean; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.width): string; + get (key: YjsDatabaseKey.width): string; } export interface YDatabaseMetas extends Y.Map { - get(key: YjsDatabaseKey.iid): string; + get (key: YjsDatabaseKey.iid): string; } export interface YDatabaseFields extends Y.Map { - get(key: FieldId): YDatabaseField; + get (key: FieldId): YDatabaseField; } export interface YDatabaseField extends Y.Map { - get(key: YjsDatabaseKey.name): string; + get (key: YjsDatabaseKey.name): string; - get(key: YjsDatabaseKey.id): FieldId; + get (key: YjsDatabaseKey.id): FieldId; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.type): string; + get (key: YjsDatabaseKey.type): string; - get(key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; + get (key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; - get(key: YjsDatabaseKey.is_primary): boolean; + get (key: YjsDatabaseKey.is_primary): boolean; - get(key: YjsDatabaseKey.last_modified): LastModified; + get (key: YjsDatabaseKey.last_modified): LastModified; } export interface YDatabaseFieldTypeOption extends Y.Map { // key is the field type - get(key: string): YMapFieldTypeOption; + get (key: string): YMapFieldTypeOption; } export interface YMapFieldTypeOption extends Y.Map { - get(key: YjsDatabaseKey.content): string; + get (key: YjsDatabaseKey.content): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.data): string; + get (key: YjsDatabaseKey.data): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.time_format): string; + get (key: YjsDatabaseKey.time_format): string; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.date_format): string; + get (key: YjsDatabaseKey.date_format): string; - get(key: YjsDatabaseKey.database_id): DatabaseId; + get (key: YjsDatabaseKey.database_id): DatabaseId; // eslint-disable-next-line @typescript-eslint/unified-signatures - get(key: YjsDatabaseKey.format): string; + get (key: YjsDatabaseKey.format): string; } export enum CollabType { diff --git a/frontend/appflowy_web_app/src/assets/download.svg b/frontend/appflowy_web_app/src/assets/download.svg new file mode 100644 index 0000000000..17f4583503 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/download.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/file_upload.svg b/frontend/appflowy_web_app/src/assets/file_upload.svg new file mode 100644 index 0000000000..273b92fb65 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/file_upload.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx index b9681f2f55..e8b87abc86 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx @@ -1,18 +1,41 @@ -import { IconButton, Tooltip } from '@mui/material'; +import { Divider, IconButton, Tooltip } from '@mui/material'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as CopyIcon } from '@/assets/copy.svg'; +import { ReactComponent as DownloadIcon } from '@/assets/download.svg'; -function RightTopActions({ onCopy }: { onCopy: () => void }) { +export interface RightTopActionsProps { + onCopy: () => void; + onDownload?: () => void; +} + +function RightTopActions ({ onCopy, onDownload }: RightTopActionsProps) { const { t } = useTranslation(); return ( -
+
- + { + e.stopPropagation(); + onCopy(); + }} + > + + {onDownload && <> + + + { + e.stopPropagation(); + onDownload?.(); + }} + > + + + + }
); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx index e953d8c87e..e44ea35be1 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx @@ -1,12 +1,16 @@ -import RightTopActions from '@/components/editor/components/block-actions/RightTopActions'; +import RightTopActions, { RightTopActionsProps } from '@/components/editor/components/block-actions/RightTopActions'; import React, { useRef } from 'react'; -function RightTopActionsToolbar({ onCopy, style }: { onCopy: () => void; style?: React.CSSProperties }) { +interface RightTopActionsToolbarProps extends RightTopActionsProps { + style?: React.CSSProperties; +} + +function RightTopActionsToolbar ({ style, ...props }: RightTopActionsToolbarProps) { const ref = useRef(null); return (
- +
); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/file/FileBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/file/FileBlock.tsx new file mode 100644 index 0000000000..ecb5a49b4e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/file/FileBlock.tsx @@ -0,0 +1,111 @@ +import { FieldURLType } from '@/application/collab.type'; +import { notify } from '@/components/_shared/notify'; +import RightTopActionsToolbar from '@/components/editor/components/block-actions/RightTopActionsToolbar'; +import { EditorElementProps, FileNode } from '@/components/editor/editor.type'; +import { copyTextToClipboard } from '@/utils/copy'; +import { downloadFile } from '@/utils/download'; +import { renderDate } from '@/utils/time'; +import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react'; +import { ReactComponent as FileIcon } from '@/assets/file_upload.svg'; +import { useTranslation } from 'react-i18next'; + +export const FileBlock = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const { url, name, url_type, uploaded_at } = useMemo(() => node.data || {}, [node.data]); + + const className = useMemo(() => { + const classList = ['w-full bg-bg-body py-2']; + + if (url) { + classList.push('cursor-pointer'); + } else { + classList.push('text-text-caption'); + } + + if (attributes.className) { + classList.push(attributes.className); + } + + return classList.join(' '); + }, [attributes.className, url]); + const [showToolbar, setShowToolbar] = useState(false); + const { t } = useTranslation(); + + const handleDownload = useCallback(async () => { + try { + if (!url) return; + await downloadFile(url, name); + // eslint-disable-next-line + } catch (e: any) { + notify.error(e.message); + } + }, [url, name]); + + const uploadTypePrefix = useMemo(() => { + const time = renderDate(uploaded_at, 'MMM DD, YYYY', false); + + if (url_type === FieldURLType.Upload) { + return t('web.fileBlock.uploadedAt', { + time, + }); + } else { + return t('web.fileBlock.linkedAt', { + time, + }); + } + }, [uploaded_at, url_type, t]); + + return ( +
{ + if (!url) return; + setShowToolbar(true); + }} + onMouseLeave={() => setShowToolbar(false)} + onClick={handleDownload} + > +
+ +
+ {url ? + <> +
{name?.trim() || t('document.title.placeholder')}
+
+ {uploadTypePrefix} +
+ : +
+ {t('web.fileBlock.empty')} +
+ } +
+ + {showToolbar && url && ( + { + if (!url) return; + try { + await copyTextToClipboard(url); + notify.success(t('publish.copy.fileBlock')); + } catch (_) { + // do nothing + } + }} + /> + )} +
+
+ {children} +
+ +
+ ); + })); + +export default FileBlock; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/file/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/file/index.ts new file mode 100644 index 0000000000..42ff6288d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/file/index.ts @@ -0,0 +1 @@ +export * from './FileBlock'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index 597bf25469..73ffd10ddb 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -22,6 +22,7 @@ import { ToggleList } from 'src/components/editor/components/blocks/toggle-list' import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock'; import { Formula } from '@/components/editor/components/leaf/formula'; import { Mention } from '@/components/editor/components/leaf/mention'; +import { FileBlock } from '@/components/editor/components/blocks/file'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; import React, { FC, useMemo } from 'react'; @@ -74,6 +75,8 @@ export const Element = ({ return DatabaseBlock; case BlockType.LinkPreview: return LinkPreview; + case BlockType.FileBlock: + return FileBlock; default: return UnSupportedBlock; } diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx index eb1c61625d..81bee6bf89 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -6,7 +6,7 @@ import { isFlagEmoji } from '@/utils/emoji'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -function MentionPage({ pageId }: { pageId: string }) { +function MentionPage ({ pageId }: { pageId: string }) { const context = useEditorContext(); const { navigateToView, loadViewMeta } = context; const [unPublished, setUnPublished] = useState(false); diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss index a3700d3288..4455c89be3 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.scss +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -238,7 +238,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { [data-block-type='heading'] { .mention-inline .mention-content { - @apply ml-6; + @apply ml-7; } .level-1, .level-2 { @@ -260,7 +260,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } .mention-content { - @apply ml-6; + @apply ml-7; } } diff --git a/frontend/appflowy_web_app/src/components/editor/editor.type.ts b/frontend/appflowy_web_app/src/components/editor/editor.type.ts index eea2c5c329..95072a481e 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.type.ts +++ b/frontend/appflowy_web_app/src/components/editor/editor.type.ts @@ -17,7 +17,7 @@ import { BlockId, BlockData, DatabaseNodeData, - LinkPreviewBlockData, + LinkPreviewBlockData, FileBlockData, } from '@/application/collab.type'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; @@ -98,6 +98,12 @@ export interface LinkPreviewNode extends BlockNode { data: LinkPreviewBlockData; } +export interface FileNode extends BlockNode { + type: BlockType.FileBlock; + blockId: string; + data: FileBlockData; +} + export interface MathEquationNode extends BlockNode { type: BlockType.EquationBlock; blockId: string; diff --git a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx index 9718f3414a..2e3c5d2e48 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx @@ -18,7 +18,7 @@ export interface Crumb { extra?: string | null; } -function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { +function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { const { viewId, icon, name, layout, extra } = crumb; const extraObj: { @@ -56,11 +56,11 @@ function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disable - + ) : ( diff --git a/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx b/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx index 4ea38df6f8..c1f91d8779 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx @@ -55,17 +55,17 @@ export const getIconComponent = (icon: string) => { } }; -function SpaceIcon({ value }: { value: string }) { +function SpaceIcon ({ value, char }: { value: string, char?: string }) { const IconComponent = getIconComponent(value); const [iconEncodeContent, setIconEncodeContent] = useState(null); useEffect(() => { - if (value && !IconComponent) { + if (!char && value && !IconComponent) { void getIconSvgEncodedContent(value, 'white').then((res) => { setIconEncodeContent(res); }); } - }, [IconComponent, value]); + }, [IconComponent, value, char]); const customIcon = useMemo(() => { if (!iconEncodeContent) { @@ -78,6 +78,14 @@ function SpaceIcon({ value }: { value: string }) { return {value}; }, [iconEncodeContent, value]); + if (char) { + return ( + + {char} + + ); + } + if (!IconComponent) { return customIcon; } diff --git a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx index 8979c0e6f0..6aa63768f2 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx @@ -14,17 +14,17 @@ export interface SpaceListProps { loading?: boolean; } -function SpaceList({ loading, spaceList, value, onChange }: SpaceListProps) { +function SpaceList ({ loading, spaceList, value, onChange }: SpaceListProps) { const { t } = useTranslation(); const getExtraObj = useCallback((extra: string) => { try { return extra ? (JSON.parse(extra) as { - is_space?: boolean; - space_icon?: string; - space_icon_color?: string; - }) + is_space?: boolean; + space_icon?: string; + space_icon_color?: string; + }) : {}; } catch (e) { return {}; @@ -40,11 +40,13 @@ function SpaceList({ loading, spaceList, value, onChange }: SpaceListProps) { - +
{space.name} @@ -53,7 +55,7 @@ function SpaceList({ loading, spaceList, value, onChange }: SpaceListProps) {
); }, - [getExtraObj] + [getExtraObj], ); return ( diff --git a/frontend/appflowy_web_app/src/utils/download.ts b/frontend/appflowy_web_app/src/utils/download.ts new file mode 100644 index 0000000000..22feff52f5 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/download.ts @@ -0,0 +1,27 @@ +export async function downloadFile (url: string, filename?: string): Promise { + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Download failed, the download status is: ${response.status}`); + } + + const blob = await response.blob(); + + const anchor = document.createElement('a'); + const blobUrl = window.URL.createObjectURL(blob); + + anchor.href = blobUrl; + + anchor.download = filename || url.split('/').pop() || 'download'; + + document.body.appendChild(anchor); + anchor.click(); + + document.body.removeChild(anchor); + window.URL.revokeObjectURL(blobUrl); + } catch (error) { + + return Promise.reject(error); + } +} diff --git a/frontend/appflowy_web_app/src/utils/time.ts b/frontend/appflowy_web_app/src/utils/time.ts index 792b72ee61..81f1a7bb62 100644 --- a/frontend/appflowy_web_app/src/utils/time.ts +++ b/frontend/appflowy_web_app/src/utils/time.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; -export function renderDate(date: string, format: string, isUnix?: boolean): string { +export function renderDate (date: string | number, format: string, isUnix?: boolean): string { if (isUnix) return dayjs.unix(Number(date)).format(format); return dayjs(date).format(format); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 683402e7b3..2ff4a5a5f2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -375,7 +375,8 @@ "close": "Close", "next": "Next", "previous": "Previous", - "submit": "Submit" + "submit": "Submit", + "download": "Download" }, "label": { "welcome": "Welcome!", @@ -2327,7 +2328,8 @@ "copy": { "codeBlock": "The content of code block has been copied to the clipboard", "imageBlock": "The image link has been copied to the clipboard", - "mathBlock": "The math equation has been copied to the clipboard" + "mathBlock": "The math equation has been copied to the clipboard", + "fileBlock": "The file link has been copied to the clipboard" }, "containsPublishedPage": "This page contains one or more published pages. If you continue, they will be unpublished. Do you want to proceed with deletion?", "publishSuccessfully": "Published successfully", @@ -2375,7 +2377,12 @@ "termOfUse": "Terms", "privacyPolicy": "Privacy Policy", "signInError": "Sign in error", - "login": "Sign up or log in" + "login": "Sign up or log in", + "fileBlock": { + "uploadedAt": "Uploaded on {time}", + "linkedAt": "Link added on {time}", + "empty": "Upload or embed a file" + } }, "globalComment": { "comments": "Comments",