mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
chore: support display mention person on document (#25)
* chore: support display mention person on document * chore: add recent pages on open view
This commit is contained in:
@@ -3249,5 +3249,6 @@
|
||||
"membersCount_zero": "{{count}} members",
|
||||
"membersCount_one": "{{count}} member",
|
||||
"membersCount_many": "{{count}} members",
|
||||
"membersCount_other": "{{count}} members"
|
||||
"membersCount_other": "{{count}} members",
|
||||
"deleted": "deleted"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -714,4 +714,12 @@ export class AFClientService implements AFService {
|
||||
async approveTurnGuestToMember(workspaceId: string, code: string) {
|
||||
return APIService.approveTurnGuestToMember(workspaceId, code);
|
||||
}
|
||||
|
||||
async getMentionableUsers(workspaceId: string) {
|
||||
return APIService.getMentionableUsers(workspaceId);
|
||||
}
|
||||
|
||||
async addRecentPages(workspaceId: string, viewIds: string[]) {
|
||||
return APIService.addRecentPages(workspaceId, viewIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
GenerateAITranslateRowPayload,
|
||||
GuestInvitation,
|
||||
GuestConversionCodeInfo,
|
||||
MentionablePerson,
|
||||
} from '@/application/types';
|
||||
import { GlobalComment, Reaction } from '@/application/comment.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
@@ -79,6 +80,8 @@ export interface WorkspaceService {
|
||||
acceptGuestInvitation: (workspaceId: string, code: string) => Promise<void>;
|
||||
getGuestToMemberConversionInfo: (workspaceId: string, code: string) => Promise<GuestConversionCodeInfo>;
|
||||
approveTurnGuestToMember: (workspaceId: string, code: string) => Promise<void>;
|
||||
getMentionableUsers: (workspaceId: string) => Promise<MentionablePerson[]>;
|
||||
addRecentPages: (workspaceId: string, viewIds: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AppService {
|
||||
|
||||
@@ -90,7 +90,7 @@ export interface CodeBlockData extends BlockData {
|
||||
|
||||
export interface CalloutBlockData extends BlockData {
|
||||
icon: string;
|
||||
icon_type?: 'emoji' | 'icon',
|
||||
icon_type?: 'emoji' | 'icon';
|
||||
}
|
||||
|
||||
export interface MathEquationBlockData extends BlockData {
|
||||
@@ -150,8 +150,8 @@ export enum GalleryLayout {
|
||||
|
||||
export interface GalleryBlockData extends BlockData {
|
||||
images: {
|
||||
type: ImageType,
|
||||
url: string,
|
||||
type: ImageType;
|
||||
url: string;
|
||||
}[];
|
||||
layout: GalleryLayout;
|
||||
}
|
||||
@@ -211,6 +211,7 @@ export enum MentionType {
|
||||
Date = 'date',
|
||||
childPage = 'childPage',
|
||||
externalLink = 'externalLink',
|
||||
Person = 'person',
|
||||
}
|
||||
|
||||
export interface Mention {
|
||||
@@ -225,6 +226,10 @@ export interface Mention {
|
||||
// external link
|
||||
url?: string;
|
||||
type: MentionType;
|
||||
|
||||
// mention person
|
||||
person_id?: string;
|
||||
person_name?: string;
|
||||
}
|
||||
|
||||
export interface FolderMeta {
|
||||
@@ -362,154 +367,153 @@ 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<unknown> {
|
||||
get (key: YjsDatabaseKey.id): RowId;
|
||||
get(key: YjsDatabaseKey.id): RowId;
|
||||
|
||||
get (key: YjsDatabaseKey.database_id | YjsDatabaseKey.height): string;
|
||||
get(key: YjsDatabaseKey.database_id | YjsDatabaseKey.height): string;
|
||||
|
||||
get (key: YjsDatabaseKey.visibility): boolean;
|
||||
get(key: YjsDatabaseKey.visibility): boolean;
|
||||
|
||||
get (key: YjsDatabaseKey.cells): YDatabaseCells;
|
||||
get(key: YjsDatabaseKey.cells): YDatabaseCells;
|
||||
|
||||
get (key: YjsDatabaseKey.created_at): CreatedAt;
|
||||
|
||||
get (key: YjsDatabaseKey.last_modified): LastModified;
|
||||
get(key: YjsDatabaseKey.created_at): CreatedAt;
|
||||
|
||||
get(key: YjsDatabaseKey.last_modified): LastModified;
|
||||
}
|
||||
|
||||
export interface YDatabaseCells extends Y.Map<unknown> {
|
||||
get (key: FieldId): YDatabaseCell;
|
||||
get(key: FieldId): YDatabaseCell;
|
||||
}
|
||||
|
||||
export type EndTimestamp = string;
|
||||
export type ReminderId = string;
|
||||
|
||||
export interface YDatabaseCell extends Y.Map<unknown> {
|
||||
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): string | boolean | number | null | Y.Array<string> | object;
|
||||
get(key: YjsDatabaseKey.data): string | boolean | number | null | Y.Array<string> | object;
|
||||
|
||||
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<unknown> {
|
||||
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;
|
||||
|
||||
get (key: YjsEditorKey.meta): Y.Map<unknown>;
|
||||
get(key: YjsEditorKey.meta): Y.Map<unknown>;
|
||||
}
|
||||
|
||||
export interface YFolder extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
get (key: ViewId): YView;
|
||||
get(key: ViewId): YView;
|
||||
}
|
||||
|
||||
export interface YView extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
get (key: ViewId): Y.Array<ViewId>;
|
||||
get(key: ViewId): Y.Array<ViewId>;
|
||||
}
|
||||
|
||||
export interface YFolderMeta extends Y.Map<unknown> {
|
||||
get (key: YjsFolderKey.current_view | YjsFolderKey.current_workspace): string;
|
||||
get(key: YjsFolderKey.current_view | YjsFolderKey.current_workspace): string;
|
||||
}
|
||||
|
||||
export interface YFolderSection extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
get (key: string): Y.Array<unknown>;
|
||||
get(key: string): Y.Array<unknown>;
|
||||
}
|
||||
|
||||
export interface YDocument extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
get (key: BlockId): YBlock;
|
||||
get(key: BlockId): YBlock;
|
||||
}
|
||||
|
||||
export interface YBlock extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
get (key: ChildrenId): Y.Array<BlockId>;
|
||||
get(key: ChildrenId): Y.Array<BlockId>;
|
||||
}
|
||||
|
||||
export interface YTextMap extends Y.Map<unknown> {
|
||||
get (key: ExternalId): Y.Text;
|
||||
get(key: ExternalId): Y.Text;
|
||||
}
|
||||
|
||||
export interface YDatabase extends Y.Map<unknown> {
|
||||
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<YDatabaseView> {
|
||||
get (key: ViewId): YDatabaseView;
|
||||
get(key: ViewId): YDatabaseView;
|
||||
}
|
||||
|
||||
export type DatabaseId = string;
|
||||
@@ -525,39 +529,39 @@ export enum DatabaseViewLayout {
|
||||
}
|
||||
|
||||
export interface YDatabaseView extends Y.Map<unknown> {
|
||||
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;
|
||||
|
||||
get (key: YjsDatabaseKey.is_inline): boolean;
|
||||
get(key: YjsDatabaseKey.is_inline): boolean;
|
||||
}
|
||||
|
||||
export type YDatabaseFieldOrders = Y.Array<{ id: FieldId }>; // [ { id: FieldId } ]
|
||||
|
||||
export type YDatabaseRowOrders = Y.Array<{ id: RowId, height: number }>; // [ { id: RowId, height: number } ]
|
||||
export type YDatabaseRowOrders = Y.Array<{ id: RowId; height: number }>; // [ { id: RowId, height: number } ]
|
||||
|
||||
export type YDatabaseGroups = Y.Array<YDatabaseGroup>;
|
||||
|
||||
@@ -573,136 +577,136 @@ export type GroupId = string;
|
||||
|
||||
export interface YDatabaseLayoutSettings extends Y.Map<unknown> {
|
||||
// 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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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; // "{"hide_empty":false,"condition":2}"
|
||||
get(key: YjsDatabaseKey.content): string; // "{"hide_empty":false,"condition":2}"
|
||||
|
||||
get (key: YjsDatabaseKey.groups): YDatabaseGroupColumns;
|
||||
get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns;
|
||||
}
|
||||
|
||||
export type YDatabaseGroupColumns = Y.Array<{ id: string, visible: boolean }>;
|
||||
export type YDatabaseGroupColumns = Y.Array<{ id: string; visible: boolean }>;
|
||||
|
||||
export interface YDatabaseGroupColumn extends Y.Map<unknown> {
|
||||
get (key: YjsDatabaseKey.id): string;
|
||||
get(key: YjsDatabaseKey.id): string;
|
||||
|
||||
get (key: YjsDatabaseKey.visible): boolean;
|
||||
get(key: YjsDatabaseKey.visible): boolean;
|
||||
}
|
||||
|
||||
export interface YDatabaseSort extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
get (key: FieldId): YDatabaseFieldSetting;
|
||||
get(key: FieldId): YDatabaseFieldSetting;
|
||||
}
|
||||
|
||||
export interface YDatabaseFieldSetting extends Y.Map<unknown> {
|
||||
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<unknown> {
|
||||
get (key: YjsDatabaseKey.iid): string;
|
||||
get(key: YjsDatabaseKey.iid): string;
|
||||
}
|
||||
|
||||
export interface YDatabaseFields extends Y.Map<YDatabaseField> {
|
||||
get (key: FieldId): YDatabaseField;
|
||||
get(key: FieldId): YDatabaseField;
|
||||
}
|
||||
|
||||
export interface YDatabaseField extends Y.Map<unknown> {
|
||||
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.icon): string;
|
||||
get(key: YjsDatabaseKey.icon): string;
|
||||
|
||||
// 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<unknown> {
|
||||
// key is the field type
|
||||
get (key: string): YMapFieldTypeOption;
|
||||
get(key: string): YMapFieldTypeOption;
|
||||
}
|
||||
|
||||
export interface YMapFieldTypeOption extends Y.Map<unknown> {
|
||||
// single select, Multi select, File media
|
||||
get (key: YjsDatabaseKey.content): string;
|
||||
get(key: YjsDatabaseKey.content): string;
|
||||
|
||||
// CreatedTime, LastEditedTime, DateTime
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
get (key: YjsDatabaseKey.time_format): string;
|
||||
get(key: YjsDatabaseKey.time_format): string;
|
||||
|
||||
// CreatedTime, LastEditedTime, DateTime
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
get (key: YjsDatabaseKey.date_format): string;
|
||||
get(key: YjsDatabaseKey.date_format): string;
|
||||
|
||||
// Relation
|
||||
get (key: YjsDatabaseKey.database_id): DatabaseId;
|
||||
get(key: YjsDatabaseKey.database_id): DatabaseId;
|
||||
|
||||
// Number
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
get (key: YjsDatabaseKey.format): string;
|
||||
get(key: YjsDatabaseKey.format): string;
|
||||
|
||||
// LastModified and CreatedTime
|
||||
get (key: YjsDatabaseKey.include_time): boolean;
|
||||
get(key: YjsDatabaseKey.include_time): boolean;
|
||||
|
||||
// AI Translate
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
get (key: YjsDatabaseKey.auto_fill): boolean;
|
||||
get(key: YjsDatabaseKey.auto_fill): boolean;
|
||||
|
||||
get (key: YjsDatabaseKey.language): bigint;
|
||||
get(key: YjsDatabaseKey.language): bigint;
|
||||
}
|
||||
|
||||
export enum Types {
|
||||
@@ -720,7 +724,6 @@ export enum CollabOrigin {
|
||||
Local = 'local',
|
||||
// from remote changes and never sync to remote.
|
||||
Remote = 'remote',
|
||||
|
||||
}
|
||||
|
||||
export interface PublishViewPayload {
|
||||
@@ -920,7 +923,7 @@ export interface GuestInvitation {
|
||||
is_existing_member: boolean;
|
||||
}
|
||||
|
||||
export interface GuestConversionCodeInfo {
|
||||
export interface GuestConversionCodeInfo {
|
||||
workspace_name: string;
|
||||
requester_avatar?: string;
|
||||
requester_name: string;
|
||||
@@ -993,7 +996,7 @@ export enum SubscriptionPlan {
|
||||
Free = 'free',
|
||||
Pro = 'pro',
|
||||
Team = 'team',
|
||||
AIMax = 'ai_max'
|
||||
AIMax = 'ai_max',
|
||||
}
|
||||
|
||||
export enum SubscriptionInterval {
|
||||
@@ -1013,8 +1016,8 @@ export type Subscriptions = Subscription[];
|
||||
export interface UpdatePagePayload {
|
||||
name: string;
|
||||
icon?: {
|
||||
ty: ViewIconType,
|
||||
value: string,
|
||||
ty: ViewIconType;
|
||||
value: string;
|
||||
};
|
||||
extra?: Partial<ViewExtra>;
|
||||
}
|
||||
@@ -1085,18 +1088,18 @@ export interface CreatePagePayload {
|
||||
}
|
||||
|
||||
export interface CreateFolderViewPayload {
|
||||
parentViewId: string,
|
||||
layout: ViewLayout,
|
||||
name?: string,
|
||||
viewId?: string,
|
||||
databaseId?: string,
|
||||
parentViewId: string;
|
||||
layout: ViewLayout;
|
||||
name?: string;
|
||||
viewId?: string;
|
||||
databaseId?: string;
|
||||
}
|
||||
|
||||
export interface CreateSpacePayload {
|
||||
name?: string;
|
||||
space_icon?: string;
|
||||
space_icon_color?: string;
|
||||
space_permission?: SpacePermission, // 0 for public space, 1 for private space
|
||||
space_permission?: SpacePermission; // 0 for public space, 1 for private space
|
||||
}
|
||||
|
||||
export interface UpdateSpacePayload extends CreateSpacePayload {
|
||||
@@ -1105,7 +1108,7 @@ export interface UpdateSpacePayload extends CreateSpacePayload {
|
||||
|
||||
export interface QuickNoteEditorData {
|
||||
type: string;
|
||||
delta: { insert: string, attributes?: Record<string, string | boolean | number> }[];
|
||||
delta: { insert: string; attributes?: Record<string, string | boolean | number> }[];
|
||||
data?: BlockData;
|
||||
children: QuickNoteEditorData[];
|
||||
}
|
||||
@@ -1113,7 +1116,7 @@ export interface QuickNoteEditorData {
|
||||
export interface QuickNote {
|
||||
id: string;
|
||||
title: string;
|
||||
data: QuickNoteEditorData[],
|
||||
data: QuickNoteEditorData[];
|
||||
created_at: string;
|
||||
last_updated_at: string;
|
||||
}
|
||||
@@ -1146,9 +1149,9 @@ export interface GenerateAITranslateRowPayload {
|
||||
title: string;
|
||||
// cell data
|
||||
content: string;
|
||||
}[],
|
||||
language: string,
|
||||
include_header?: boolean
|
||||
}[];
|
||||
language: string;
|
||||
include_header?: boolean;
|
||||
}
|
||||
|
||||
export type LoadDatabasePrompts = (config: PromptDatabaseConfiguration) => Promise<{
|
||||
@@ -1183,3 +1186,17 @@ export interface DatabasePromptRow {
|
||||
[fieldId: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MentionablePerson {
|
||||
avatar_url: string | null;
|
||||
cover_image_url: string | null;
|
||||
custom_image_url: string | null;
|
||||
description: string | null;
|
||||
email: string;
|
||||
|
||||
name: string;
|
||||
role: number;
|
||||
uuid: string;
|
||||
invited: boolean;
|
||||
last_mentioned_at: string | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton, Portal, Tooltip } from '@mui/material';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactZoomPanPinchContentRef, TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ReactComponent as ReloadIcon } from '@/assets/icons/reset.svg';
|
||||
import { ReactComponent as DownloadIcon } from '@/assets/icons/save_as.svg';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { copyTextToClipboard } from '@/utils/copy';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
export interface GalleryImage {
|
||||
src: string;
|
||||
@@ -22,11 +23,12 @@ export interface GalleryPreviewProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
previewIndex: number;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
const buttonClassName = 'p-1 hover:bg-transparent text-white hover:text-text-action p-0';
|
||||
|
||||
function GalleryPreview({ images, open, onClose, previewIndex }: GalleryPreviewProps) {
|
||||
function GalleryPreview({ images, open, onClose, previewIndex, workspaceId }: GalleryPreviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const [index, setIndex] = useState(previewIndex);
|
||||
const transformComponentRef = useRef<ReactZoomPanPinchContentRef>(null);
|
||||
@@ -92,6 +94,16 @@ function GalleryPreview({ images, open, onClose, previewIndex }: GalleryPreviewP
|
||||
};
|
||||
}, [handleKeydown]);
|
||||
|
||||
const imageUrl = useMemo(() => {
|
||||
if (isURL(images[index].src)) {
|
||||
return images[index].src;
|
||||
}
|
||||
|
||||
const fileId = images[index].src;
|
||||
|
||||
return import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
}, [images, index, workspaceId]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
@@ -190,7 +202,7 @@ function GalleryPreview({ images, open, onClose, previewIndex }: GalleryPreviewP
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={images[index].src}
|
||||
src={imageUrl}
|
||||
alt={images[index].src}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import { PromptDatabaseConfiguration } from '@appflowyinc/ai-chat';
|
||||
import { sortBy, uniqBy } from 'lodash-es';
|
||||
import React, { createContext, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
import { APP_EVENTS } from '@/application/constants';
|
||||
import { FieldType } from '@/application/database-yjs';
|
||||
import { getCellDataText } from '@/application/database-yjs/cell.parse';
|
||||
import { invalidToken } from '@/application/session/token';
|
||||
import {
|
||||
AppendBreadcrumb,
|
||||
@@ -22,6 +25,7 @@ import {
|
||||
LoadDatabasePrompts,
|
||||
LoadView,
|
||||
LoadViewMeta,
|
||||
MentionablePerson,
|
||||
Subscription,
|
||||
TestDatabasePromptConfig,
|
||||
TextCount,
|
||||
@@ -42,9 +46,6 @@ import { AppOverlayProvider } from '@/components/app/app-overlay/AppOverlayProvi
|
||||
import RequestAccess from '@/components/app/landing-pages/RequestAccess';
|
||||
import { AFConfigContext, useService } from '@/components/main/app.hooks';
|
||||
import { findAncestors, findView, findViewByLayout } from '@/components/_shared/outline/utils';
|
||||
import { PromptDatabaseConfiguration } from '@appflowyinc/ai-chat';
|
||||
import { FieldType } from '@/application/database-yjs';
|
||||
import { getCellDataText } from '@/application/database-yjs/cell.parse';
|
||||
|
||||
const ViewModal = React.lazy(() => import('@/components/app/ViewModal'));
|
||||
|
||||
@@ -96,6 +97,7 @@ export interface AppContextType {
|
||||
loadDatabasePrompts?: LoadDatabasePrompts;
|
||||
testDatabasePromptConfig?: TestDatabasePromptConfig;
|
||||
eventEmitter?: EventEmitter;
|
||||
getMentionUser?: (uuid: string) => Promise<MentionablePerson | undefined>;
|
||||
}
|
||||
|
||||
const USER_NO_ACCESS_CODE = [1024, 1012];
|
||||
@@ -299,6 +301,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
collabType,
|
||||
});
|
||||
|
||||
// add recent pages when view is loaded
|
||||
void (async () => {
|
||||
try {
|
||||
await service.addRecentPages(currentWorkspaceId, [id]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
|
||||
return res;
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
@@ -1040,6 +1051,49 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
[getFields]
|
||||
);
|
||||
|
||||
const mentionableUsersRef = useRef<MentionablePerson[]>([]);
|
||||
const loadMentionableUsers = useCallback(async () => {
|
||||
if (!currentWorkspaceId || !service) {
|
||||
throw new Error('No workspace or service found');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await service.getMentionableUsers(currentWorkspaceId);
|
||||
|
||||
mentionableUsersRef.current = res;
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}, [currentWorkspaceId, service]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadMentionableUsers();
|
||||
}, [loadMentionableUsers]);
|
||||
|
||||
const getMentionUser = useCallback(
|
||||
async (uuid: string) => {
|
||||
console.log('mentionableUsers', mentionableUsersRef.current);
|
||||
if (mentionableUsersRef.current.length > 0) {
|
||||
const user = mentionableUsersRef.current.find((user) => user.uuid === uuid);
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await loadMentionableUsers();
|
||||
|
||||
return res.find((user) => user.uuid === uuid);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[loadMentionableUsers]
|
||||
);
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
@@ -1090,6 +1144,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
loadDatabasePrompts,
|
||||
testDatabasePromptConfig,
|
||||
eventEmitter: eventEmitterRef.current,
|
||||
getMentionUser,
|
||||
}}
|
||||
>
|
||||
<AIChatProvider>
|
||||
@@ -1248,6 +1303,7 @@ export function useAppHandlers() {
|
||||
loadDatabasePrompts: context.loadDatabasePrompts,
|
||||
testDatabasePromptConfig: context.testDatabasePromptConfig,
|
||||
eventEmitter: context.eventEmitter,
|
||||
getMentionUser: context.getMentionUser,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Suspense, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useDatabaseContext } from '@/application/database-yjs';
|
||||
import {
|
||||
CellProps,
|
||||
FileMediaCell as FileMediaCellType,
|
||||
@@ -24,7 +25,7 @@ export function FileMediaCell({
|
||||
readOnly,
|
||||
}: CellProps<FileMediaCellType>) {
|
||||
const value = cell?.data;
|
||||
|
||||
const { workspaceId } = useDatabaseContext();
|
||||
const [openPreview, setOpenPreview] = React.useState(false);
|
||||
const previewIndexRef = React.useRef(0);
|
||||
const photos = useMemo(() => {
|
||||
@@ -111,6 +112,7 @@ export function FileMediaCell({
|
||||
{openPreview && (
|
||||
<Suspense>
|
||||
<GalleryPreview
|
||||
workspaceId={workspaceId}
|
||||
images={photos}
|
||||
previewIndex={previewIndexRef.current}
|
||||
open={openPreview}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { FileMediaCellDataItem } from '@/application/database-yjs/cell.type';
|
||||
import { useMemo } from 'react';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { useDatabaseContext } from '@/application/database-yjs';
|
||||
import { FileMediaCellDataItem } from '@/application/database-yjs/cell.type';
|
||||
|
||||
function PreviewImage({ file, onClick }: { file: FileMediaCellDataItem; onClick: () => void }) {
|
||||
const { workspaceId } = useDatabaseContext();
|
||||
|
||||
const thumb = useMemo(() => {
|
||||
const url = new URL(file.url);
|
||||
let fileUrl = file.url;
|
||||
|
||||
if (!isURL(file.url)) {
|
||||
fileUrl = import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + file.url;
|
||||
}
|
||||
|
||||
const url = new URL(fileUrl);
|
||||
|
||||
url.searchParams.set('auto', 'format');
|
||||
url.searchParams.set('fit', 'crop');
|
||||
|
||||
return url.toString() + '&w=240&q=80';
|
||||
}, [file.url]);
|
||||
}, [file.url, workspaceId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { useDatabaseContext } from '@/application/database-yjs';
|
||||
import { FileMediaCellDataItem } from '@/application/database-yjs/cell.type';
|
||||
import FileIcon from '@/components/database/components/cell/file-media/FileIcon';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { openUrl } from '@/utils/url';
|
||||
|
||||
function UnPreviewFile ({ file }: { file: FileMediaCellDataItem }) {
|
||||
function UnPreviewFile({ file }: { file: FileMediaCellDataItem }) {
|
||||
const { workspaceId } = useDatabaseContext();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
delayDuration={500}
|
||||
disableHoverableContent
|
||||
>
|
||||
<Tooltip delayDuration={500} disableHoverableContent>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
className={'rounded-[4px] text-icon-secondary bg-fill-content-hover cursor-pointer'}
|
||||
onClick={e => {
|
||||
className={'cursor-pointer rounded-[4px] bg-fill-content-hover text-icon-secondary'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void openUrl(file.url, '_blank');
|
||||
if (isURL(file.url)) {
|
||||
void openUrl(file.url, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = file.url;
|
||||
const newUrl = import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
|
||||
void openUrl(newUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<FileIcon fileType={file.file_type} />
|
||||
@@ -26,7 +35,7 @@ function UnPreviewFile ({ file }: { file: FileMediaCellDataItem }) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={'bottom'}>
|
||||
<div className={'flex gap-1.5'}>
|
||||
<span className={'min-w-5 w-5 h-5'}>
|
||||
<span className={'h-5 w-5 min-w-5'}>
|
||||
<FileIcon fileType={file.file_type} />
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Suspense, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFieldSelector } from '@/application/database-yjs';
|
||||
import { useDatabaseContext, useFieldSelector } from '@/application/database-yjs';
|
||||
import {
|
||||
CellProps,
|
||||
FileMediaCell as FileMediaCellType, FileMediaCellData, FileMediaCellDataItem,
|
||||
@@ -31,6 +31,7 @@ export function FileMediaCell ({
|
||||
const value = cell?.data;
|
||||
const { t } = useTranslation();
|
||||
const { field, clock } = useFieldSelector(fieldId);
|
||||
const { workspaceId } = useDatabaseContext();
|
||||
const typeOption = useMemo(() => {
|
||||
if (!field) return null;
|
||||
return parseFileMediaTypeOptions(field);
|
||||
@@ -167,6 +168,7 @@ export function FileMediaCell ({
|
||||
showUpload={true}
|
||||
/>}
|
||||
{openPreview && <Suspense><GalleryPreview
|
||||
workspaceId={workspaceId}
|
||||
images={photos}
|
||||
previewIndex={previewIndexRef.current}
|
||||
open={openPreview}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { useReadOnly } from '@/application/database-yjs';
|
||||
import { useDatabaseContext, useReadOnly } from '@/application/database-yjs';
|
||||
import { FileMediaCellDataItem, FileMediaType } from '@/application/database-yjs/cell.type';
|
||||
import FileIcon from '@/components/database/components/cell/file-media/FileIcon';
|
||||
import FileMediaMore from '@/components/database/components/cell/file-media/FileMediaMore';
|
||||
@@ -24,6 +25,7 @@ function FileMediaItem({
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const readOnly = useReadOnly();
|
||||
const { workspaceId } = useDatabaseContext();
|
||||
|
||||
const isImage = file.file_type === FileMediaType.Image;
|
||||
const mouseDownStartTimeRef = useRef<number | null>(null);
|
||||
@@ -44,6 +46,16 @@ function FileMediaItem({
|
||||
}
|
||||
}, [file.file_type]);
|
||||
|
||||
const fileUrl = useMemo(() => {
|
||||
if (isURL(file.url)) {
|
||||
return file.url;
|
||||
}
|
||||
|
||||
const fileId = file.url;
|
||||
|
||||
return import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
}, [file.url, workspaceId]);
|
||||
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -52,7 +64,15 @@ function FileMediaItem({
|
||||
e.stopPropagation();
|
||||
// Open the file in a new tab
|
||||
if (file.file_type !== FileMediaType.Image) {
|
||||
void openUrl(file.url, '_blank');
|
||||
if (isURL(file.url)) {
|
||||
void openUrl(file.url, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = file.url;
|
||||
const newUrl = import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
|
||||
void openUrl(newUrl, '_blank');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
@@ -80,7 +100,7 @@ function FileMediaItem({
|
||||
>
|
||||
<img
|
||||
draggable={false}
|
||||
src={file.url}
|
||||
src={fileUrl}
|
||||
alt={file.name}
|
||||
className={'aspect-square h-full w-full overflow-hidden object-cover'}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextCount,
|
||||
LoadDatabasePrompts,
|
||||
TestDatabasePromptConfig,
|
||||
MentionablePerson,
|
||||
} from '@/application/types';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import EventEmitter from 'events';
|
||||
@@ -66,6 +67,7 @@ export interface EditorContextState {
|
||||
loadDatabasePrompts?: LoadDatabasePrompts;
|
||||
testDatabasePromptConfig?: TestDatabasePromptConfig;
|
||||
eventEmitter?: EventEmitter;
|
||||
getMentionUser?: (uuid: string) => Promise<MentionablePerson | undefined>;
|
||||
}
|
||||
|
||||
export const EditorContext = createContext<EditorContextState>({
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { BlockType, FieldURLType, FileBlockData } from '@/application/types';
|
||||
import { ReactComponent as FileIcon } from '@/assets/icons/file.svg';
|
||||
import { ReactComponent as ReloadIcon } from '@/assets/icons/regenerate.svg';
|
||||
import { CircularProgress, IconButton, Tooltip } from '@mui/material';
|
||||
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { BlockType, FieldURLType, FileBlockData } from '@/application/types';
|
||||
import { ReactComponent as FileIcon } from '@/assets/icons/file.svg';
|
||||
import { ReactComponent as ReloadIcon } from '@/assets/icons/regenerate.svg';
|
||||
import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext';
|
||||
import FileToolbar from '@/components/editor/components/blocks/file/FileToolbar';
|
||||
import { EditorElementProps, FileNode } from '@/components/editor/editor.type';
|
||||
@@ -11,26 +17,33 @@ import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { FileHandler } from '@/utils/file';
|
||||
import { openUrl } from '@/utils/url';
|
||||
import { CircularProgress, IconButton, Tooltip } from '@mui/material';
|
||||
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
|
||||
export const FileBlock = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<FileNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const { blockId, data } = node;
|
||||
const { uploadFile } = useEditorContext();
|
||||
const { uploadFile, workspaceId } = useEditorContext();
|
||||
const editor = useSlateStatic() as YjsEditor;
|
||||
const [needRetry, setNeedRetry] = useState(false);
|
||||
const fileHandler = useMemo(() => new FileHandler(), []);
|
||||
const [localUrl, setLocalUrl] = useState<string | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { url, name, retry_local_url } = useMemo(() => data || {}, [data]);
|
||||
const { url: dataUrl, name, retry_local_url } = useMemo(() => data || {}, [data]);
|
||||
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
|
||||
const emptyRef = useRef<HTMLDivElement>(null);
|
||||
const [showToolbar, setShowToolbar] = useState(false);
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (!dataUrl) return '';
|
||||
|
||||
if (isURL(dataUrl)) {
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
const fileId = dataUrl;
|
||||
|
||||
return import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
}, [dataUrl, workspaceId]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
const classList = ['w-full'];
|
||||
|
||||
@@ -175,7 +188,17 @@ export const FileBlock = memo(
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
{showToolbar && url && <FileToolbar node={node} />}
|
||||
{showToolbar && url && (
|
||||
<FileToolbar
|
||||
node={{
|
||||
...node,
|
||||
data: {
|
||||
...data,
|
||||
url,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={ref} className={`absolute h-full w-full caret-transparent`}>
|
||||
{children}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React, { forwardRef, memo, Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useReadOnly } from 'slate-react';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { GalleryLayout } from '@/application/types';
|
||||
import { ReactComponent as GalleryIcon } from '@/assets/icons/gallery.svg';
|
||||
import Carousel from '@/components/editor/components/blocks/gallery/Carousel';
|
||||
import GalleryToolbar from '@/components/editor/components/blocks/gallery/GalleryToolbar';
|
||||
import ImageGallery from '@/components/editor/components/blocks/gallery/ImageGallery';
|
||||
import { EditorElementProps, GalleryBlockNode } from '@/components/editor/editor.type';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { GalleryPreview } from '@/components/_shared/gallery-preview';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { copyTextToClipboard } from '@/utils/copy';
|
||||
import React, { forwardRef, memo, Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useReadOnly } from 'slate-react';
|
||||
|
||||
const GalleryBlock = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<GalleryBlockNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspaceId } = useEditorContext();
|
||||
const { images, layout } = useMemo(() => node.data || {}, [node.data]);
|
||||
const [openPreview, setOpenPreview] = React.useState(false);
|
||||
const previewIndexRef = React.useRef(0);
|
||||
@@ -27,17 +31,23 @@ const GalleryBlock = memo(
|
||||
|
||||
const photos = useMemo(() => {
|
||||
return images.map((image) => {
|
||||
const url = new URL(image.url);
|
||||
let imageUrl = image.url;
|
||||
|
||||
if (!isURL(image.url)) {
|
||||
imageUrl = import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + image.url;
|
||||
}
|
||||
|
||||
const url = new URL(imageUrl);
|
||||
|
||||
url.searchParams.set('auto', 'format');
|
||||
url.searchParams.set('fit', 'crop');
|
||||
return {
|
||||
src: image.url,
|
||||
src: imageUrl,
|
||||
thumb: url.toString() + '&w=240&q=80',
|
||||
responsive: [url.toString() + '&w=480&q=80 480', url.toString() + '&w=800&q=80 800'].join(', '),
|
||||
};
|
||||
});
|
||||
}, [images]);
|
||||
}, [images, workspaceId]);
|
||||
|
||||
const handleOpenPreview = useCallback(() => {
|
||||
setOpenPreview(true);
|
||||
@@ -112,6 +122,7 @@ const GalleryBlock = memo(
|
||||
{openPreview && (
|
||||
<Suspense>
|
||||
<GalleryPreview
|
||||
workspaceId={workspaceId}
|
||||
images={photos}
|
||||
previewIndex={previewIndexRef.current}
|
||||
open={openPreview}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { ReactEditor, useReadOnly, useSelected, useSlateStatic } from 'slate-react';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { AlignType, BlockType, ImageBlockData, ImageType } from '@/application/types';
|
||||
@@ -7,11 +14,7 @@ import { EditorElementProps, ImageBlockNode } from '@/components/editor/editor.t
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { FileHandler } from '@/utils/file';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { ReactEditor, useReadOnly, useSelected, useSlateStatic } from 'slate-react';
|
||||
|
||||
import ImageEmpty from './ImageEmpty';
|
||||
import ImageRender from './ImageRender';
|
||||
|
||||
@@ -21,7 +24,7 @@ export const ImageBlock = memo(
|
||||
|
||||
const { blockId, data } = node;
|
||||
const retry_local_url = data?.retry_local_url;
|
||||
const { uploadFile } = useEditorContext();
|
||||
const { uploadFile, workspaceId } = useEditorContext();
|
||||
const editor = useSlateStatic() as YjsEditor;
|
||||
const [needRetry, setNeedRetry] = useState(false);
|
||||
const [localUrl, setLocalUrl] = useState<string | undefined>(undefined);
|
||||
@@ -30,7 +33,19 @@ export const ImageBlock = memo(
|
||||
const fileHandler = useMemo(() => new FileHandler(), []);
|
||||
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
|
||||
const selected = useSelected();
|
||||
const { url, align } = useMemo(() => data || {}, [data]);
|
||||
const { url: dataUrl, align } = useMemo(() => data || {}, [data]);
|
||||
const url = useMemo(() => {
|
||||
if (!dataUrl) return '';
|
||||
|
||||
if (isURL(dataUrl)) {
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
const fileId = dataUrl;
|
||||
|
||||
return import.meta.env.AF_BASE_URL + '/api/file_storage/' + workspaceId + '/v1/blob/' + fileId;
|
||||
}, [dataUrl, workspaceId]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const onFocusNode = useCallback(() => {
|
||||
ReactEditor.focus(editor);
|
||||
@@ -157,9 +172,30 @@ export const ImageBlock = memo(
|
||||
}`}
|
||||
>
|
||||
{url || needRetry ? (
|
||||
<ImageRender showToolbar={showToolbar} selected={selected} node={node} localUrl={localUrl} />
|
||||
<ImageRender
|
||||
showToolbar={showToolbar}
|
||||
selected={selected}
|
||||
node={{
|
||||
...node,
|
||||
data: {
|
||||
...data,
|
||||
url,
|
||||
},
|
||||
}}
|
||||
localUrl={localUrl}
|
||||
/>
|
||||
) : (
|
||||
<ImageEmpty node={node} onEscape={onFocusNode} containerRef={containerRef} />
|
||||
<ImageEmpty
|
||||
node={{
|
||||
...node,
|
||||
data: {
|
||||
...data,
|
||||
url,
|
||||
},
|
||||
}}
|
||||
onEscape={onFocusNode}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
{needRetry && (
|
||||
<div className={'absolute bottom-2 right-4 flex items-center gap-2'}>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { Divider } from '@mui/material';
|
||||
import React, { Suspense } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { ReactComponent as CopyIcon } from '@/assets/icons/copy.svg';
|
||||
@@ -6,21 +12,17 @@ import { ReactComponent as PreviewIcon } from '@/assets/icons/expand.svg';
|
||||
import ActionButton from '@/components/editor/components/toolbar/selection-toolbar/actions/ActionButton';
|
||||
import Align from '@/components/editor/components/toolbar/selection-toolbar/actions/Align';
|
||||
import { ImageBlockNode } from '@/components/editor/editor.type';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { GalleryPreview } from '@/components/_shared/gallery-preview';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { copyTextToClipboard } from '@/utils/copy';
|
||||
import { Divider } from '@mui/material';
|
||||
import React, { Suspense } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Element } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
|
||||
function ImageToolbar({ node }: { node: ImageBlockNode }) {
|
||||
const editor = useSlateStatic() as YjsEditor;
|
||||
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
|
||||
const { t } = useTranslation();
|
||||
const [openPreview, setOpenPreview] = React.useState(false);
|
||||
|
||||
const { workspaceId } = useEditorContext();
|
||||
const onOpenPreview = () => {
|
||||
setOpenPreview(true);
|
||||
};
|
||||
@@ -60,6 +62,7 @@ function ImageToolbar({ node }: { node: ImageBlockNode }) {
|
||||
{openPreview && (
|
||||
<Suspense>
|
||||
<GalleryPreview
|
||||
workspaceId={workspaceId}
|
||||
images={[{ src: node.data.url || '' }]}
|
||||
previewIndex={0}
|
||||
open={openPreview}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Element, Text } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
|
||||
import { Mention, MentionType } from '@/application/types';
|
||||
import { useLeafSelected } from '@/components/editor/components/leaf/leaf.hooks';
|
||||
import MentionDate from '@/components/editor/components/leaf/mention/MentionDate';
|
||||
import MentionExternalLink from '@/components/editor/components/leaf/mention/MentionExternalLink';
|
||||
import MentionPage from '@/components/editor/components/leaf/mention/MentionPage';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Element, Text } from 'slate';
|
||||
import { useReadOnly, useSlateStatic } from 'slate-react';
|
||||
import { MentionPerson } from '@/components/editor/components/leaf/mention/MentionPerson';
|
||||
|
||||
export function MentionLeaf ({ mention, text, children }: {
|
||||
mention: Mention;
|
||||
text: Text;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export function MentionLeaf({ mention, text, children }: { mention: Mention; text: Text; children: React.ReactNode }) {
|
||||
const editor = useSlateStatic();
|
||||
const readonly = useReadOnly() || editor.isElementReadOnly(text as unknown as Element);
|
||||
const { type, date, page_id, reminder_id, reminder_option, block_id, url } = mention;
|
||||
const { type, date, page_id, reminder_id, reminder_option, block_id, url, person_id, person_name } = mention;
|
||||
|
||||
const reminder = useMemo(() => {
|
||||
return reminder_id ? { id: reminder_id ?? '', option: reminder_option ?? '' } : undefined;
|
||||
@@ -22,26 +20,21 @@ export function MentionLeaf ({ mention, text, children }: {
|
||||
|
||||
const content = useMemo(() => {
|
||||
if ([MentionType.PageRef, MentionType.childPage].includes(type) && page_id) {
|
||||
return <MentionPage
|
||||
text={text}
|
||||
type={type}
|
||||
pageId={page_id}
|
||||
blockId={block_id}
|
||||
/>;
|
||||
return <MentionPage text={text} type={type} pageId={page_id} blockId={block_id} />;
|
||||
}
|
||||
|
||||
if (type === MentionType.Date && date) {
|
||||
return <MentionDate
|
||||
date={date}
|
||||
reminder={reminder}
|
||||
/>;
|
||||
return <MentionDate date={date} reminder={reminder} />;
|
||||
}
|
||||
|
||||
if (type === MentionType.externalLink && url) {
|
||||
return <MentionExternalLink url={url} />;
|
||||
}
|
||||
|
||||
}, [type, page_id, date, text, block_id, reminder, url]);
|
||||
if (type === MentionType.Person && person_id) {
|
||||
return <MentionPerson type={type} personId={person_id} person_name={person_name} />;
|
||||
}
|
||||
}, [type, page_id, date, text, block_id, reminder, url, person_id, person_name]);
|
||||
|
||||
// check if the mention is selected
|
||||
const { isSelected, select, isCursorBefore } = useLeafSelected(text);
|
||||
@@ -57,31 +50,25 @@ export function MentionLeaf ({ mention, text, children }: {
|
||||
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
|
||||
return <>
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
left: isCursorBefore ? 0 : 'auto',
|
||||
right: isCursorBefore ? 'auto' : 0,
|
||||
top: isCursorBefore ? 0 : 'auto',
|
||||
bottom: isCursorBefore ? 'auto' : 0,
|
||||
}}
|
||||
className={'absolute bottom-0 right-0 overflow-hidden !text-transparent'}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
left: isCursorBefore ? 0 : 'auto',
|
||||
right: isCursorBefore ? 'auto' : 0,
|
||||
top: isCursorBefore ? 0 : 'auto',
|
||||
bottom: isCursorBefore ? 'auto' : 0,
|
||||
}}
|
||||
className={'absolute right-0 bottom-0 !text-transparent overflow-hidden'}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
<span
|
||||
ref={ref}
|
||||
onClick={select}
|
||||
contentEditable={false}
|
||||
className={className}
|
||||
>
|
||||
{content}
|
||||
|
||||
</span>
|
||||
|
||||
</>;
|
||||
<span ref={ref} onClick={select} contentEditable={false} className={className}>
|
||||
{content}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MentionLeaf;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MentionType } from '@/application/types';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
|
||||
export function MentionPerson({ personId, person_name }: { type: MentionType; personId: string; person_name?: string }) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(person_name);
|
||||
const { getMentionUser } = useEditorContext();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const user = await getMentionUser?.(personId);
|
||||
|
||||
if (user) {
|
||||
setName(user.name);
|
||||
} else {
|
||||
setIsDeleted(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setName(person_name || 'Anonymous');
|
||||
}
|
||||
};
|
||||
|
||||
void fetchUser();
|
||||
}, [getMentionUser, personId, person_name]);
|
||||
|
||||
return (
|
||||
<span contentEditable={false} data-mention-id={personId} className='mention-person'>
|
||||
<span className='mr-0.5 text-text-tertiary'>@</span>
|
||||
<span className='text-text-secondary'>{isDeleted ? t('deleted') : name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user