mirror of
https://github.com/AppFlowy-IO/AppFlowy
synced 2026-05-24 09:38:25 +00:00
fix: open duplicate entrance and fixed some issues (#6216)
This commit is contained in:
parent
2b9b8c19a9
commit
a1793b53dd
36 changed files with 574 additions and 291 deletions
|
|
@ -128,7 +128,7 @@ const createServer = async (req: Request) => {
|
|||
try {
|
||||
if (metaData && metaData.view) {
|
||||
const view = metaData.view;
|
||||
const emoji = view.icon.value;
|
||||
const emoji = view.icon?.ty === 0 && view.icon?.value;
|
||||
const titleList = [];
|
||||
|
||||
if (emoji) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
"react-datepicker": "^4.23.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^14.1.0",
|
||||
|
|
@ -129,6 +130,7 @@
|
|||
"@types/react-custom-scrollbars": "^4.0.13",
|
||||
"@types/react-datepicker": "^4.19.3",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/react-measure": "^2.0.12",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,9 @@ dependencies:
|
|||
react-error-boundary:
|
||||
specifier: ^4.0.13
|
||||
version: 4.0.13(react@18.2.0)
|
||||
react-helmet:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.52.2
|
||||
version: 7.52.2(react@18.2.0)
|
||||
|
|
@ -316,6 +319,9 @@ devDependencies:
|
|||
'@types/react-dom':
|
||||
specifier: ^18.2.22
|
||||
version: 18.2.22
|
||||
'@types/react-helmet':
|
||||
specifier: ^6.1.11
|
||||
version: 6.1.11
|
||||
'@types/react-katex':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
|
|
@ -4216,6 +4222,12 @@ packages:
|
|||
dependencies:
|
||||
'@types/react': 18.2.66
|
||||
|
||||
/@types/react-helmet@6.1.11:
|
||||
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.66
|
||||
dev: true
|
||||
|
||||
/@types/react-katex@3.0.0:
|
||||
resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==}
|
||||
dependencies:
|
||||
|
|
@ -9541,6 +9553,18 @@ packages:
|
|||
/react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
/react-helmet@6.1.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
|
||||
peerDependencies:
|
||||
react: '>=16.3.0'
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-fast-compare: 3.2.2
|
||||
react-side-effect: 2.1.2(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-hook-form@7.52.2(react@18.2.0):
|
||||
resolution: {integrity: sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
|
@ -9747,6 +9771,14 @@ packages:
|
|||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-side-effect@2.1.2(react@18.2.0):
|
||||
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
|
||||
peerDependencies:
|
||||
react: ^16.3.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-swipeable-views-core@0.14.0:
|
||||
resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ViewMeta } from '@/application/db/tables/view_metas';
|
|||
import { View } from '@/application/types';
|
||||
import { useService } from '@/components/app/app.hooks';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { findView } from '@/components/publish/header/utils';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
|
@ -33,11 +34,6 @@ export const PublishProvider = ({
|
|||
publishName: string;
|
||||
isTemplateThumb?: boolean;
|
||||
}) => {
|
||||
const viewMeta = useLiveQuery(async () => {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
return db.view_metas.get(name);
|
||||
}, [namespace, publishName]);
|
||||
|
||||
const [outline, setOutline] = useState<View>();
|
||||
|
||||
|
|
@ -49,6 +45,19 @@ export const PublishProvider = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
const viewMeta = useLiveQuery(async () => {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
const view = await db.view_metas.get(name);
|
||||
|
||||
if (!view) return;
|
||||
|
||||
return {
|
||||
...view,
|
||||
name: findView(outline?.children || [], view.view_id)?.name || view.name,
|
||||
};
|
||||
}, [namespace, publishName, outline]);
|
||||
|
||||
useEffect(() => {
|
||||
db.view_metas.hook('creating', (primaryKey, obj) => {
|
||||
const subscriber = subscribers.get(primaryKey);
|
||||
|
|
@ -87,7 +96,7 @@ export const PublishProvider = ({
|
|||
const res = await service?.getPublishInfo(viewId);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('Not found');
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
const { namespace: viewNamespace, publishName } = res;
|
||||
|
|
@ -106,7 +115,6 @@ export const PublishProvider = ({
|
|||
|
||||
const loadOutline = useCallback(async () => {
|
||||
if (!service || !namespace) return;
|
||||
console.log('loadOutline', namespace);
|
||||
try {
|
||||
const res = await service?.getPublishOutline(namespace);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,13 @@ export interface DuplicatePublishView {
|
|||
viewId: string;
|
||||
}
|
||||
|
||||
export enum ViewIconType {
|
||||
Emoji = 0,
|
||||
Icon = 1,
|
||||
}
|
||||
|
||||
export interface ViewIcon {
|
||||
ty: number;
|
||||
ty: ViewIconType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
4
frontend/appflowy_web_app/src/assets/publish.svg
Normal file
4
frontend/appflowy_web_app/src/assets/publish.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 1.5C11.59 1.5 14.5 4.41 14.5 8C14.5 11.59 11.59 14.5 8 14.5C4.41 14.5 1.5 11.59 1.5 8C1.5 4.41 4.41 1.5 8 1.5ZM7.5 8.5L5.51067 8.50033C5.52467 8.82033 5.55167 9.13167 5.59067 9.43267L5.62167 9.656L5.66033 9.89433C5.969 11.6583 6.68733 12.984 7.50033 13.3787L7.5 8.5ZM10.4893 8.50033L8.5 8.5V13.3787C9.29467 12.9923 9.999 11.7167 10.3183 10.0113L10.3397 9.89433L10.3783 9.65633C10.4357 9.27323 10.4728 8.88736 10.4893 8.50033ZM4.50967 8.50033H2.52233C2.70433 10.517 3.97533 12.2203 5.74267 13.017C5.397 12.4813 5.112 11.823 4.90267 11.079L4.85533 10.9057L4.80033 10.6843L4.74967 10.4583C4.61437 9.81364 4.53408 9.15862 4.50967 8.50033ZM13.4777 8.50033H11.49C11.4679 9.08847 11.4017 9.67411 11.292 10.2523L11.25 10.4583L11.1993 10.6843L11.1443 10.906C10.931 11.7207 10.6293 12.4403 10.257 13.017C12.0243 12.2203 13.2953 10.517 13.477 8.50033H13.4777ZM5.74267 2.98267L5.698 3.003C3.95333 3.80833 2.70267 5.5 2.52233 7.5H4.50967C4.53367 6.89 4.60133 6.30167 4.70767 5.74767L4.74967 5.54167L4.80033 5.31567L4.85533 5.094C5.06867 4.27933 5.37033 3.55967 5.74267 2.983V2.98267ZM7.5 2.62133C6.70767 3.006 6.005 4.276 5.68433 5.97433L5.66033 6.10567L5.62167 6.34367C5.56424 6.72688 5.52719 7.11286 5.51067 7.5H7.5V2.62133ZM10.2573 2.983L10.2973 3.04633C10.624 3.56867 10.8947 4.20233 11.0957 4.91433L11.1447 5.09433L11.1997 5.31567L11.2503 5.54167C11.381 6.155 11.4633 6.81367 11.4903 7.5H13.4777C13.296 5.483 12.025 3.77967 10.2573 2.98333V2.983ZM8.5 2.62133V7.5H10.4893C10.4755 7.17927 10.4477 6.8593 10.406 6.541L10.3783 6.34367L10.3397 6.10567C10.031 4.342 9.313 3.01633 8.50033 2.62133H8.5Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -15,7 +15,7 @@ function AppFlowyPower ({
|
|||
width,
|
||||
boxShadow: 'var(--bg-footer) 0px -4px 14px 13px',
|
||||
}}
|
||||
className={'flex bg-bg-body sticky bottom-[-0.5px] w-full flex-col items-center justify-center'}
|
||||
className={'flex rounded-[16px] transform-gpu bg-bg-body sticky bottom-[-0.5px] w-full flex-col items-center justify-center'}
|
||||
>
|
||||
{divider && <Divider className={'w-full my-0'} />}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ function GalleryPreview ({
|
|||
|
||||
return (
|
||||
<Portal container={document.body}>
|
||||
<div className={'fixed inset-0 bg-black bg-opacity-80 z-50'} onClick={onClose}>
|
||||
<div className={'fixed inset-0 bg-black bg-opacity-80 z-[1400]'} onClick={onClose}>
|
||||
|
||||
<TransformWrapper
|
||||
initialScale={1}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { ViewIcon, ViewIconType } from '@/application/types';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
function ViewHelmet ({
|
||||
name,
|
||||
icon,
|
||||
}: {
|
||||
name?: string;
|
||||
icon?: ViewIcon
|
||||
}) {
|
||||
|
||||
useEffect(() => {
|
||||
const setFavicon = async () => {
|
||||
try {
|
||||
let url = '/appflowy.svg';
|
||||
|
||||
if (icon && icon.ty === ViewIconType.Emoji && icon.value) {
|
||||
const emojiCode = icon?.value?.codePointAt(0)?.toString(16); // Convert emoji to hex code
|
||||
const baseUrl = 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u';
|
||||
|
||||
const response = await fetch(`${baseUrl}${emojiCode}.svg`);
|
||||
const svgText = await response.text();
|
||||
const blob = new Blob([svgText], { type: 'image/svg+xml' });
|
||||
|
||||
url = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
const link = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement || document.createElement('link');
|
||||
|
||||
link.type = 'image/svg+xml';
|
||||
link.rel = 'icon';
|
||||
link.href = url;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
} catch (error) {
|
||||
console.error('Error setting favicon:', error);
|
||||
}
|
||||
};
|
||||
|
||||
void setFavicon();
|
||||
|
||||
return () => {
|
||||
const link = document.querySelector('link[rel*=\'icon\']');
|
||||
|
||||
if (link) {
|
||||
document.getElementsByTagName('head')[0].removeChild(link);
|
||||
}
|
||||
};
|
||||
}, [icon]);
|
||||
|
||||
if (!name) return null;
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{name} | AppFlowy</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
export default ViewHelmet;
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Skeleton, Box } from '@mui/material';
|
||||
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
|
||||
|
||||
export const BreadcrumbsSkeleton = () => {
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Skeleton variant="circular" width={20} height={20} />
|
||||
<Skeleton variant="text" width={60} height={20} />
|
||||
<Skeleton variant="text" width={20} height={20} sx={{ mx: 1 }} />
|
||||
<RightIcon className={'h-5 w-5 text-text-caption'} />
|
||||
<Skeleton variant="circular" width={20} height={20} />
|
||||
<Skeleton variant="text" width={80} height={20} />
|
||||
<Skeleton variant="text" width={20} height={20} sx={{ mx: 1 }} />
|
||||
<RightIcon className={'h-5 w-5 text-text-caption'} />
|
||||
<Skeleton variant="circular" width={20} height={20} />
|
||||
<Skeleton variant="text" width={100} height={20} />
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import { FC, useMemo } from 'react';
|
||||
import { ReactComponent as CircleIcon } from '@/assets/bulleted_list_icon_1.svg';
|
||||
|
||||
export interface TagProps {
|
||||
color?: string;
|
||||
label?: string;
|
||||
size?: 'small' | 'medium';
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
|
||||
export const Tag: FC<TagProps> = ({ color, size = 'small', label, badge }) => {
|
||||
const className = useMemo(() => {
|
||||
const classList = ['rounded-md', 'font-medium', 'leading-[18px]'];
|
||||
const classList = ['rounded-[8px]', 'font-medium', 'leading-[1.5em]', 'flex items-center gap-1 max-w-full'];
|
||||
|
||||
if (color) classList.push(`text-text-title`);
|
||||
if (size === 'small') classList.push('px-2', 'py-[2px]');
|
||||
if (size === 'small') classList.push('px-2', 'py-1');
|
||||
if (size === 'medium') classList.push('px-3', 'py-1');
|
||||
if (badge) classList.push('pr-4');
|
||||
return classList.join(' ');
|
||||
}, [color, size]);
|
||||
}, [color, size, badge]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -23,7 +26,16 @@ export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
|
|||
}}
|
||||
className={className}
|
||||
>
|
||||
{label}
|
||||
{badge &&
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: `var(${badge})`,
|
||||
}}
|
||||
className={`w-5 h-5`}
|
||||
/>
|
||||
}
|
||||
<div className={'truncate'}>{label}</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardPro
|
|||
style={{
|
||||
minHeight: '38px',
|
||||
}}
|
||||
className="relative flex flex-col gap-2 rounded-[8px] border border-line-divider p-3 text-xs"
|
||||
className="relative shadow-sm flex flex-col gap-2 overflow-hidden rounded-[8px] border border-line-divider p-3 text-xs"
|
||||
>
|
||||
{showFields.map((field, index) => {
|
||||
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const Column = memo(
|
|||
rowId: row.id,
|
||||
};
|
||||
}) || [],
|
||||
[rows]
|
||||
[rows],
|
||||
);
|
||||
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows });
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export const Column = memo(
|
|||
|
||||
return <ListItem fieldId={fieldId} onResize={onResizeCallback} item={item} style={style} />;
|
||||
},
|
||||
[fieldId, onResize]
|
||||
[fieldId, onResize],
|
||||
);
|
||||
|
||||
const getItemSize = useCallback(
|
||||
|
|
@ -65,13 +65,18 @@ export const Column = memo(
|
|||
if (!row) return 0;
|
||||
return rowHeight(index);
|
||||
},
|
||||
[rowHeight, rows]
|
||||
[rowHeight, rows],
|
||||
);
|
||||
const rowCount = rows?.length || 0;
|
||||
|
||||
return (
|
||||
<div key={id} className='column flex w-[230px] flex-col gap-4'>
|
||||
<div className='column-header flex h-[24px] items-center text-sm font-medium'>{header}</div>
|
||||
<div key={id} className="column flex w-[230px] flex-col gap-4">
|
||||
<div
|
||||
className="column-header flex overflow-hidden items-center gap-2 text-sm font-medium whitespace-nowrap"
|
||||
>
|
||||
<div className={'max-w-[180px] w-auto overflow-hidden'}>{header}</div>
|
||||
<span className={'text-text-caption font-medium'}>{rowCount}</span>
|
||||
</div>
|
||||
|
||||
<div className={'w-full flex-1 overflow-hidden'}>
|
||||
<AutoSizer>
|
||||
|
|
@ -95,5 +100,5 @@ export const Column = memo(
|
|||
</div>
|
||||
);
|
||||
},
|
||||
(prev, next) => JSON.stringify(prev) === JSON.stringify(next)
|
||||
(prev, next) => JSON.stringify(prev) === JSON.stringify(next),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs';
|
||||
import { Tag } from '@/components/_shared/tag';
|
||||
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||
import { SelectOptionBadgeColorMap, SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
||||
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
||||
|
||||
export function useRenderColumn(id: string, fieldId: string) {
|
||||
export function useRenderColumn (id: string, fieldId: string) {
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||
const fieldName = field?.get(YjsDatabaseKey.name) || '';
|
||||
|
|
@ -34,11 +35,19 @@ export function useRenderColumn(id: string, fieldId: string) {
|
|||
if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) {
|
||||
const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id);
|
||||
|
||||
const label = option?.name || `No ${fieldName}`;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
label={option?.name || `No ${fieldName}`}
|
||||
color={option?.color ? SelectOptionColorMap[option?.color] : 'transparent'}
|
||||
/>
|
||||
<Tooltip title={label} enterNextDelay={1000} enterDelay={1000}>
|
||||
<span>
|
||||
<Tag
|
||||
label={label}
|
||||
color={option?.color ? SelectOptionColorMap[option?.color] : 'transparent'}
|
||||
badge={option?.color ? SelectOptionBadgeColorMap[option?.color] : undefined}
|
||||
/>
|
||||
</span>
|
||||
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,16 @@ export const SelectOptionColorMap = {
|
|||
[SelectOptionColor.Aqua]: '--tint-aqua',
|
||||
[SelectOptionColor.Blue]: '--tint-blue',
|
||||
};
|
||||
|
||||
export const SelectOptionBadgeColorMap = {
|
||||
[SelectOptionColor.Purple]: '--badge-purple',
|
||||
[SelectOptionColor.Pink]: '--badge-pink',
|
||||
[SelectOptionColor.LightPink]: '--badge-red',
|
||||
[SelectOptionColor.Orange]: '--badge-orange',
|
||||
[SelectOptionColor.Yellow]: '--badge-yellow',
|
||||
[SelectOptionColor.Lime]: '--badge-lime',
|
||||
[SelectOptionColor.Green]: '--badge-green',
|
||||
[SelectOptionColor.Aqua]: '--badge-aqua',
|
||||
[SelectOptionColor.Blue]: '--badge-blue',
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ import {
|
|||
} from '@/application/database-yjs';
|
||||
import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||
function RelationItems ({ style, cell, fieldId }: {
|
||||
cell: RelationCell;
|
||||
fieldId: string;
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const viewId = useContext(DatabaseContext)?.iidIndex;
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const relatedDatabaseId = field ? parseRelationTypeOption(field).database_id : null;
|
||||
|
|
@ -28,7 +33,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
|||
|
||||
const [rowIds, setRowIds] = useState([] as string[]);
|
||||
|
||||
// const navigateToRow = useNavigateToRow();
|
||||
const navigateToView = useContext(DatabaseContext)?.navigateToView;
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewId) return;
|
||||
|
|
@ -50,7 +55,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
|||
|
||||
setRowIds(ids.filter((id) => rows?.has(id)));
|
||||
},
|
||||
[cell.data]
|
||||
[cell.data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -108,11 +113,18 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
|||
return (
|
||||
<div
|
||||
key={rowId}
|
||||
// onClick={(e) => {
|
||||
// e.stopPropagation();
|
||||
// navigateToRow?.(rowId);
|
||||
// }}
|
||||
className={'underline'}
|
||||
onClick={async (e) => {
|
||||
if (!relatedViewId) return;
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await navigateToView?.(relatedViewId);
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
}}
|
||||
className={`underline ${relatedViewId ? 'cursor-pointer hover:text-content-blue-400' : ''}`}
|
||||
|
||||
>
|
||||
<RelationPrimaryValue fieldId={relatedFieldId} rowDoc={rowDoc} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,12 @@ export const Document = ({
|
|||
viewMeta,
|
||||
isTemplateThumb,
|
||||
}: DocumentProps) => {
|
||||
|
||||
return (
|
||||
<div className={'mb-16 flex h-full w-full flex-col items-center min-h-[500px]'}>
|
||||
<div style={{
|
||||
minHeight: `calc(100vh - 48px)`,
|
||||
}} className={'mb-16 flex h-full w-full flex-col items-center'}
|
||||
>
|
||||
<ViewMetaPreview {...viewMeta} />
|
||||
<Suspense fallback={<DocumentSkeleton />}>
|
||||
<div className={'flex justify-center w-full'}>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,11 @@ export const DatabaseBlock = memo(
|
|||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
||||
{children}
|
||||
</div>
|
||||
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col`}>
|
||||
<div
|
||||
contentEditable={false}
|
||||
style={style}
|
||||
className={`container-bg appflowy-scroller overflow-y-auto overflow-x-hidden relative flex w-full flex-col`}
|
||||
>
|
||||
{selectedViewId && doc ? (
|
||||
<>
|
||||
<Database
|
||||
|
|
@ -151,7 +155,7 @@ export const DatabaseBlock = memo(
|
|||
</>
|
||||
);
|
||||
}),
|
||||
(prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id
|
||||
(prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id,
|
||||
);
|
||||
|
||||
export default DatabaseBlock;
|
||||
|
|
|
|||
|
|
@ -42,12 +42,14 @@ function Comment ({ comment }: CommentProps) {
|
|||
<div className={'flex items-center gap-2'}>
|
||||
<div className={'flex items-center gap-4'}>
|
||||
<Avatar {...avatar} className={`h-8 w-8`} />
|
||||
<div className={'font-semibold'}>{comment.user?.name}</div>
|
||||
<Tooltip title={comment.user?.name} enterNextDelay={500} enterDelay={1000} placement={'top-start'}>
|
||||
<div className={'font-semibold max-w-[200px] truncate'}>{comment.user?.name}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip title={timeFormat} enterNextDelay={500} enterDelay={1000} placement={'top-start'}>
|
||||
<div className={'flex items-center gap-2 text-text-caption'}>
|
||||
<BulletedListIcon className={'h-3 w-3'} />
|
||||
<div className={'text-sm'}>{time}</div>
|
||||
<div className={'text-sm whitespace-nowrap'}>{time}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ function LoginProvider ({ redirectTo }: { redirectTo: string }) {
|
|||
variant={'outlined'}
|
||||
onClick={() => handleClick(option.value)}
|
||||
className={
|
||||
`flex h-[46px] w-[380px] items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium max-sm:w-full text-text-title`
|
||||
`flex h-[46px] w-full items-center justify-center gap-[10px] rounded-[12px] border border-line-divider text-sm font-medium max-sm:w-full text-text-title`
|
||||
}
|
||||
>
|
||||
<option.Icon className={'w-[24px] h-[24px]'} />
|
||||
|
|
@ -90,8 +90,8 @@ function LoginProvider ({ redirectTo }: { redirectTo: string }) {
|
|||
<Divider className={'flex-1'} />
|
||||
</Button>}
|
||||
|
||||
<Collapse in={expand}>
|
||||
<div className={'gap-[10px] flex-col flex'}>{options.slice(2).map(renderOption)}</div>
|
||||
<Collapse className={'w-full'} in={expand}>
|
||||
<div className={'gap-[10px] w-full flex-col flex'}>{options.slice(2).map(renderOption)}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { GetViewRowsMap, LoadView, LoadViewMeta, ViewLayout, YDoc } from '@/application/collab.type';
|
||||
import { usePublishContext } from '@/application/publish';
|
||||
import ViewHelmet from '@/components/_shared/helmet/ViewHelmet';
|
||||
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||
import { Document } from '@/components/document';
|
||||
import DatabaseView from '@/components/publish/DatabaseView';
|
||||
|
|
@ -60,27 +61,32 @@ function CollabView ({ doc }: CollabViewProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={className}
|
||||
>
|
||||
<View
|
||||
doc={doc}
|
||||
loadViewMeta={loadViewMeta}
|
||||
getViewRowsMap={getViewRowsMap}
|
||||
navigateToView={navigateToView}
|
||||
loadView={loadView}
|
||||
isTemplateThumb={isTemplateThumb}
|
||||
viewMeta={{
|
||||
icon,
|
||||
cover,
|
||||
viewId,
|
||||
name,
|
||||
layout: layout || ViewLayout.Document,
|
||||
visibleViewIds: visibleViewIds || [],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<ViewHelmet icon={icon} name={name} />
|
||||
|
||||
<div
|
||||
style={style}
|
||||
className={className}
|
||||
>
|
||||
<View
|
||||
doc={doc}
|
||||
loadViewMeta={loadViewMeta}
|
||||
getViewRowsMap={getViewRowsMap}
|
||||
navigateToView={navigateToView}
|
||||
loadView={loadView}
|
||||
isTemplateThumb={isTemplateThumb}
|
||||
viewMeta={{
|
||||
icon,
|
||||
cover,
|
||||
viewId,
|
||||
name,
|
||||
layout: layout || ViewLayout.Document,
|
||||
visibleViewIds: visibleViewIds || [],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { View } from '@/application/types';
|
||||
import { AFScroller } from '@/components/_shared/scroller';
|
||||
import BreadcrumbItem, { Crumb } from '@/components/publish/header/BreadcrumbItem';
|
||||
import BreadcrumbItem from '@/components/publish/header/BreadcrumbItem';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
|
||||
|
||||
export function Breadcrumb({ crumbs }: { crumbs: Crumb[] }) {
|
||||
export function Breadcrumb ({ crumbs }: { crumbs: View[] }) {
|
||||
const renderCrumb = useMemo(() => {
|
||||
return crumbs?.map((crumb, index) => {
|
||||
const isLast = index === crumbs.length - 1;
|
||||
const key = crumb.rowId ? `${crumb.viewId}-${crumb.rowId}` : `${crumb.viewId}`;
|
||||
const key = `${crumb.view_id}-${index}`;
|
||||
|
||||
return (
|
||||
<div className={`${isLast ? 'text-text-title' : 'text-text-caption'} flex items-center gap-2`} key={key}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ViewLayout } from '@/application/collab.type';
|
||||
import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
|
||||
import { usePublishContext } from '@/application/publish';
|
||||
import { View } from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { ViewIcon } from '@/components/_shared/view-icon';
|
||||
import SpaceIcon from '@/components/publish/header/SpaceIcon';
|
||||
|
|
@ -9,80 +10,66 @@ import { Tooltip } from '@mui/material';
|
|||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface Crumb {
|
||||
viewId: string;
|
||||
rowId?: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
layout: ViewLayout;
|
||||
extra?: string | null;
|
||||
}
|
||||
|
||||
function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) {
|
||||
const { viewId, icon, name, layout, extra } = crumb;
|
||||
|
||||
const extraObj: {
|
||||
is_space?: boolean;
|
||||
space_icon?: string;
|
||||
space_icon_color?: string;
|
||||
} = useMemo(() => {
|
||||
try {
|
||||
return extra ? JSON.parse(extra) : {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, [extra]);
|
||||
function BreadcrumbItem ({ crumb, disableClick = false }: { crumb: View; disableClick?: boolean }) {
|
||||
const { view_id, icon, name, layout, extra, is_published } = crumb;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const onNavigateToView = usePublishContext()?.toView;
|
||||
const isFlag = useMemo(() => {
|
||||
return icon ? isFlagEmoji(icon) : false;
|
||||
return icon ? isFlagEmoji(icon.value) : false;
|
||||
}, [icon]);
|
||||
|
||||
return (
|
||||
<Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-sm ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`}
|
||||
onClick={async () => {
|
||||
if (disableClick) return;
|
||||
try {
|
||||
await onNavigateToView?.(viewId);
|
||||
} catch (e) {
|
||||
if (extraObj.is_space) {
|
||||
notify.warning(t('publish.spaceHasNotBeenPublished'));
|
||||
return;
|
||||
}
|
||||
|
||||
notify.warning(t('publish.hasNotBeenPublished'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{extraObj && extraObj.is_space ? (
|
||||
<span
|
||||
className={'icon h-4 w-4'}
|
||||
style={{
|
||||
backgroundColor: extraObj.space_icon_color ? renderColor(extraObj.space_icon_color) : 'rgb(163, 74, 253)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<SpaceIcon value={extraObj.space_icon || ''} char={extraObj.space_icon ? undefined : name.slice(0, 1)} />
|
||||
<div
|
||||
className={`flex items-center gap-1.5 text-sm ${!disableClick && is_published ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`}
|
||||
onClick={async () => {
|
||||
if (disableClick || !is_published) return;
|
||||
try {
|
||||
await onNavigateToView?.(view_id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
notify.error(e.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{extra && extra.is_space ? (
|
||||
<span
|
||||
className={'icon h-4 w-4'}
|
||||
style={{
|
||||
backgroundColor: extra.space_icon_color ? renderColor(extra.space_icon_color) : 'rgb(163, 74, 253)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<SpaceIcon value={extra.space_icon || ''} char={extra.space_icon ? undefined : name.slice(0, 1)} />
|
||||
</span>
|
||||
) : (
|
||||
<span className={`${isFlag ? 'icon' : ''} flex h-5 w-5 items-center justify-center`}>
|
||||
{icon || <ViewIcon layout={layout} size={'small'} />}
|
||||
) : (
|
||||
<span className={`${isFlag ? 'icon' : ''} flex h-5 w-5 items-center justify-center`}>
|
||||
{icon?.value || <ViewIcon layout={layout} size={'small'} />}
|
||||
</span>
|
||||
)}
|
||||
|
||||
)}
|
||||
<Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
|
||||
<span
|
||||
className={
|
||||
'max-w-[250px] overflow-hidden truncate ' +
|
||||
(!disableClick ? 'hover:text-text-title hover:underline' : 'flex-1')
|
||||
(!disableClick && is_published ? 'hover:text-text-title hover:underline' : 'flex-1')
|
||||
}
|
||||
>
|
||||
{name || t('menuAppHeader.defaultNewPageName')}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
{!is_published && !extra?.is_space && (<Tooltip
|
||||
disableInteractive
|
||||
title={extra?.is_space ? t('publish.spaceHasNotBeenPublished') : t('publish.hasNotBeenPublished')}
|
||||
>
|
||||
<div
|
||||
className={'text-text-caption cursor-pointer hover:bg-fill-list-hover rounded h-5 w-5 flex items-center justify-center'}
|
||||
>
|
||||
<PublishIcon className={'h-4 w-4'} />
|
||||
</div>
|
||||
</Tooltip>)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { PublishViewInfo } from '@/application/collab.type';
|
||||
import { usePublishContext } from '@/application/publish';
|
||||
import { useCurrentUser } from '@/components/app/app.hooks';
|
||||
import { openOrDownload } from '@/components/publish/header/utils';
|
||||
import { View } from '@/application/types';
|
||||
import BreadcrumbSkeleton from '@/components/_shared/skeleton/BreadcrumbSkeleton';
|
||||
import { findAncestors, findView, openOrDownload } from '@/components/publish/header/utils';
|
||||
import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import { Divider, IconButton, Tooltip } from '@mui/material';
|
||||
|
|
@ -26,29 +28,38 @@ export function PublishViewHeader ({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const viewMeta = usePublishContext()?.viewMeta;
|
||||
const outline = usePublishContext()?.outline;
|
||||
const crumbs = useMemo(() => {
|
||||
const ancestors = viewMeta?.ancestor_views.slice(1) || [];
|
||||
if (!viewMeta || !outline) return [];
|
||||
const ancestors = findAncestors(outline.children, viewMeta?.view_id);
|
||||
|
||||
return ancestors.map((ancestor) => {
|
||||
let icon;
|
||||
if (ancestors) return ancestors;
|
||||
if (!viewMeta?.ancestor_views) return [];
|
||||
const parseToView = (ancestor: PublishViewInfo): View => {
|
||||
let extra = null;
|
||||
|
||||
try {
|
||||
const extra = ancestor?.extra ? JSON.parse(ancestor.extra) : {};
|
||||
|
||||
icon = extra.icon?.value || ancestor.icon?.value;
|
||||
extra = ancestor.extra ? JSON.parse(ancestor.extra) : null;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return {
|
||||
viewId: ancestor.view_id,
|
||||
view_id: ancestor.view_id,
|
||||
name: ancestor.name,
|
||||
icon: icon,
|
||||
icon: ancestor.icon,
|
||||
layout: ancestor.layout,
|
||||
extra: ancestor.extra,
|
||||
extra,
|
||||
is_published: true,
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}, [viewMeta]);
|
||||
};
|
||||
|
||||
const currentView = parseToView(viewMeta);
|
||||
|
||||
return viewMeta?.ancestor_views.slice(1).map(item => findView(outline.children, item.view_id) || parseToView(item)) || [currentView];
|
||||
}, [viewMeta, outline]);
|
||||
|
||||
const [openPopover, setOpenPopover] = React.useState(false);
|
||||
const isMobile = useMemo(() => {
|
||||
return getPlatform().isMobile;
|
||||
|
|
@ -97,10 +108,6 @@ export function PublishViewHeader ({
|
|||
return debounce(handleOpenPopover, 100);
|
||||
}, [handleOpenPopover, debounceClosePopover]);
|
||||
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
const isAppFlowyUser = currentUser?.email?.endsWith('@appflowy.io');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -108,12 +115,12 @@ export function PublishViewHeader ({
|
|||
background: 'var(--bg-header)',
|
||||
height: HEADER_HEIGHT,
|
||||
}}
|
||||
className={'appflowy-top-bar sticky top-0 z-10 flex px-5'}
|
||||
className={'appflowy-top-bar transform-gpu sticky top-0 z-10 flex px-5'}
|
||||
>
|
||||
<div className={'flex w-full items-center justify-between gap-4 overflow-hidden'}>
|
||||
{!openDrawer && (
|
||||
{!openDrawer && !isMobile && (
|
||||
<OutlinePopover
|
||||
{...isMobile ? undefined : {
|
||||
{...{
|
||||
onMouseEnter: handleOpenPopover,
|
||||
onMouseLeave: debounceClosePopover,
|
||||
}}
|
||||
|
|
@ -122,11 +129,7 @@ export function PublishViewHeader ({
|
|||
drawerWidth={drawerWidth}
|
||||
>
|
||||
<IconButton
|
||||
{...isMobile ? {
|
||||
onTouchEnd: () => {
|
||||
setOpenPopover(prev => !prev);
|
||||
},
|
||||
} : {
|
||||
{...{
|
||||
onMouseEnter: debounceOpenPopover,
|
||||
onMouseLeave: debounceClosePopover,
|
||||
onClick: () => {
|
||||
|
|
@ -136,19 +139,20 @@ export function PublishViewHeader ({
|
|||
}}
|
||||
|
||||
>
|
||||
<SideOutlined className={'h-4 w-4'} />
|
||||
<SideOutlined className={'h-4 w-4 text-text-caption'} />
|
||||
</IconButton>
|
||||
</OutlinePopover>
|
||||
)}
|
||||
|
||||
<div className={'h-full flex-1 overflow-hidden'}>
|
||||
<Breadcrumb crumbs={crumbs} />
|
||||
{!viewMeta ? <div className={'h-[48px] flex items-center'}><BreadcrumbSkeleton /></div> : <Breadcrumb
|
||||
crumbs={crumbs}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
|
||||
<MoreActions />
|
||||
{isAppFlowyUser && <Duplicate />}
|
||||
<Duplicate />
|
||||
<Divider
|
||||
orientation={'vertical'}
|
||||
className={'mx-2'}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoginModal } from '@/components/login';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useDuplicate } from '@/components/publish/header/duplicate/useDuplicate';
|
||||
import DuplicateModal from '@/components/publish/header/duplicate/DuplicateModal';
|
||||
import { ReactComponent as CopyIcon } from '@/assets/copy.svg';
|
||||
|
||||
export function Duplicate () {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -17,16 +19,31 @@ export function Duplicate () {
|
|||
});
|
||||
}, [setSearch]);
|
||||
|
||||
const isMobile = useMemo(() => {
|
||||
return getPlatform().isMobile;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{isMobile ? (
|
||||
<Tooltip title={t('publish.saveThisPage')}>
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
size={'small'}
|
||||
color={'inherit'}
|
||||
>
|
||||
<CopyIcon className={'w-5 h-5'} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : <Button
|
||||
onClick={handleClick}
|
||||
size={'small'}
|
||||
variant={'outlined'}
|
||||
color={'inherit'}
|
||||
variant={'contained'}
|
||||
color={'primary'}
|
||||
>
|
||||
{t('publish.saveThisPage')}
|
||||
</Button>
|
||||
</Button>}
|
||||
|
||||
<LoginModal
|
||||
redirectTo={url}
|
||||
open={loginOpen}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ export function useDuplicate () {
|
|||
}
|
||||
|
||||
export function useLoadWorkspaces () {
|
||||
const currentUser = useContext(AFConfigContext)?.currentUser;
|
||||
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated && Boolean(currentUser) || false;
|
||||
const [spaceLoading, setSpaceLoading] = useState<boolean>(false);
|
||||
const [workspaceLoading, setWorkspaceLoading] = useState<boolean>(false);
|
||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(() => {
|
||||
|
|
@ -61,6 +63,7 @@ export function useLoadWorkspaces () {
|
|||
const service = useContext(AFConfigContext)?.service;
|
||||
|
||||
const loadWorkspaces = useCallback(async () => {
|
||||
if (!isAuthenticated) return;
|
||||
setWorkspaceLoading(true);
|
||||
try {
|
||||
const workspaces = await service?.getWorkspaces();
|
||||
|
|
@ -80,10 +83,11 @@ export function useLoadWorkspaces () {
|
|||
} finally {
|
||||
setWorkspaceLoading(false);
|
||||
}
|
||||
}, [service]);
|
||||
}, [service, isAuthenticated]);
|
||||
|
||||
const loadSpaces = useCallback(
|
||||
async (selectedWorkspaceId: string) => {
|
||||
if (!isAuthenticated) return;
|
||||
setSpaceLoading(true);
|
||||
try {
|
||||
const folder = await service?.getWorkspaceFolder(selectedWorkspaceId);
|
||||
|
|
@ -113,7 +117,7 @@ export function useLoadWorkspaces () {
|
|||
setSpaceLoading(false);
|
||||
}
|
||||
},
|
||||
[service],
|
||||
[service, isAuthenticated],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,99 +1,51 @@
|
|||
export function openOrDownload() {
|
||||
const getDeviceType = () => {
|
||||
const ua = navigator.userAgent;
|
||||
import { View } from '@/application/types';
|
||||
import { getOS, openAppOrDownload } from '@/utils/open_schema';
|
||||
import { iosDownloadLink, androidDownloadLink, desktopDownloadLink, openAppFlowySchema } from '@/utils/url';
|
||||
|
||||
if (/(iPad|iPhone|iPod)/g.test(ua)) {
|
||||
return 'iOS';
|
||||
} else if (/Android/g.test(ua)) {
|
||||
return 'Android';
|
||||
} else {
|
||||
return 'Desktop';
|
||||
}
|
||||
};
|
||||
export function openOrDownload () {
|
||||
const os = getOS();
|
||||
const downloadUrl = os === 'ios' ? iosDownloadLink : os === 'android' ? androidDownloadLink : desktopDownloadLink;
|
||||
|
||||
const deviceType = getDeviceType();
|
||||
const isMobile = deviceType !== 'Desktop';
|
||||
const getFallbackLink = () => {
|
||||
if (deviceType === 'iOS') {
|
||||
return 'https://testflight.apple.com/join/6CexvkDz';
|
||||
} else if (deviceType === 'Android') {
|
||||
return 'https://play.google.com/store/apps/details?id=io.appflowy.appflowy';
|
||||
} else {
|
||||
return 'https://appflowy.io/download/#pop';
|
||||
}
|
||||
};
|
||||
|
||||
const getDuration = () => {
|
||||
switch (deviceType) {
|
||||
case 'iOS':
|
||||
return 250;
|
||||
default:
|
||||
return 1500;
|
||||
}
|
||||
};
|
||||
|
||||
const APPFLOWY_SCHEME = 'appflowy-flutter://';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = APPFLOWY_SCHEME;
|
||||
|
||||
const openSchema = () => {
|
||||
if (isMobile) return (window.location.href = APPFLOWY_SCHEME);
|
||||
document.body.appendChild(iframe);
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const openAppFlowy = () => {
|
||||
openSchema();
|
||||
|
||||
const initialTime = Date.now();
|
||||
let interactTime = initialTime;
|
||||
let waitTime = 0;
|
||||
const duration = getDuration();
|
||||
|
||||
const updateInteractTime = () => {
|
||||
interactTime = Date.now();
|
||||
};
|
||||
|
||||
document.removeEventListener('mousemove', updateInteractTime);
|
||||
document.removeEventListener('mouseenter', updateInteractTime);
|
||||
|
||||
const checkOpen = setInterval(() => {
|
||||
waitTime = Date.now() - initialTime;
|
||||
|
||||
if (waitTime > duration) {
|
||||
clearInterval(checkOpen);
|
||||
if (isMobile || Date.now() - interactTime < duration) {
|
||||
window.open(getFallbackLink(), '_current');
|
||||
}
|
||||
}
|
||||
}, 20);
|
||||
|
||||
if (!isMobile) {
|
||||
document.addEventListener('mouseenter', updateInteractTime);
|
||||
document.addEventListener('mousemove', updateInteractTime);
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
const isHidden = document.hidden;
|
||||
|
||||
if (isHidden) {
|
||||
clearInterval(checkOpen);
|
||||
}
|
||||
});
|
||||
|
||||
window.onpagehide = () => {
|
||||
clearInterval(checkOpen);
|
||||
};
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
clearInterval(checkOpen);
|
||||
};
|
||||
};
|
||||
|
||||
openAppFlowy();
|
||||
return openAppOrDownload({
|
||||
appScheme: openAppFlowySchema,
|
||||
downloadUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function findAncestors (data: View[], targetId: string, currentPath: View[] = []): View[] | null {
|
||||
for (const item of data) {
|
||||
const newPath = [...currentPath, item];
|
||||
|
||||
if (item.view_id === targetId) {
|
||||
return newPath;
|
||||
}
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
const result = findAncestors(item.children, targetId, newPath);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findView (data: View[], targetId: string): View | null {
|
||||
for (const item of data) {
|
||||
if (item.view_id === targetId) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
const result = findView(item.children, targetId);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ export function OutlineDrawer ({ open, width, onClose }: { open: boolean; width:
|
|||
<div className={'flex h-full relative min-h-full flex-col overflow-y-auto overflow-x-hidden appflowy-scroller'}>
|
||||
<div style={{
|
||||
backdropFilter: 'blur(4px)',
|
||||
}} className={'flex z-10 h-[48px] sticky top-0 items-center justify-between p-4'}
|
||||
}} className={'flex transform-gpu z-10 h-[48px] sticky top-0 items-center justify-between p-4'}
|
||||
>
|
||||
<div
|
||||
className={'flex cursor-pointer items-center gap-1 text-text-title'}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { PublishContext, usePublishContext } from '@/application/publish';
|
||||
import { View } from '@/application/types';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { ViewIcon } from '@/components/_shared/view-icon';
|
||||
import SpaceIcon from '@/components/publish/header/SpaceIcon';
|
||||
import { renderColor } from '@/utils/color';
|
||||
import { isFlagEmoji } from '@/utils/emoji';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import { ReactComponent as ChevronDownIcon } from '@/assets/chevron_down.svg';
|
||||
import { ReactComponent as PublishIcon } from '@/assets/publish.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function OutlineItem ({ view, level = 0, width }: { view: View; width: number; level?: number }) {
|
||||
|
|
@ -53,18 +54,9 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
|
|||
const { t } = useTranslation();
|
||||
|
||||
const navigateToView = useContext(PublishContext)?.toView;
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
|
||||
const toastWarning = useCallback((item: View) => {
|
||||
if (!item.is_published) {
|
||||
if (item.extra?.is_space) {
|
||||
notify.warning(t('publish.spaceHasNotBeenPublished'));
|
||||
return;
|
||||
}
|
||||
|
||||
notify.warning(t('publish.hasNotBeenPublished'));
|
||||
}
|
||||
}, [t]);
|
||||
const renderItem = (item: View) => {
|
||||
const renderItem = useCallback((item: View) => {
|
||||
const { icon, layout, name, view_id, extra } = item;
|
||||
|
||||
const isSpace = extra?.is_space;
|
||||
|
|
@ -81,23 +73,26 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
|
|||
}
|
||||
>
|
||||
{item.children?.length ? getIcon() : null}
|
||||
|
||||
<div
|
||||
onClick={async () => {
|
||||
if (!item.is_published) {
|
||||
toastWarning(item);
|
||||
if (isSpace || !item.is_published) {
|
||||
setIsExpanded(prev => !prev);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigateToView?.(view_id);
|
||||
} catch (e) {
|
||||
toastWarning(item);
|
||||
// do nothing
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
paddingLeft: item.children?.length ? 0 : 1.125 * (level + 1) + 'rem',
|
||||
}}
|
||||
className={'flex flex-1 cursor-pointer items-center gap-1.5 overflow-hidden'}
|
||||
className={`flex flex-1 cursor-pointer select-none items-center gap-1.5 overflow-hidden`}
|
||||
>
|
||||
{isSpace && extra ?
|
||||
<span
|
||||
|
|
@ -116,15 +111,33 @@ function OutlineItem ({ view, level = 0, width }: { view: View; width: number; l
|
|||
</div>
|
||||
}
|
||||
|
||||
<div className={'flex-1 truncate'}>{name}</div>
|
||||
<Tooltip title={name} enterDelay={1000} enterNextDelay={1000}>
|
||||
<div className={'flex-1 truncate'}>{name}</div>
|
||||
</Tooltip>
|
||||
{hovered && !item.is_published && !isSpace && (
|
||||
<Tooltip
|
||||
disableInteractive
|
||||
title={isSpace ? t('publish.spaceHasNotBeenPublished') : t('publish.hasNotBeenPublished')}
|
||||
>
|
||||
<div
|
||||
className={'text-text-caption ml-2 mr-4 cursor-pointer hover:bg-fill-list-hover rounded h-5 w-5 flex items-center justify-center'}
|
||||
>
|
||||
<PublishIcon className={'h-4 w-4'} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}, [hovered, getIcon, level, navigateToView, selected, t, width]);
|
||||
|
||||
const children = view.children || [];
|
||||
|
||||
if (!children.length && !view.is_published) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex h-fit w-full flex-col'}>
|
||||
{renderItem(view)}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,11 @@ export function OutlinePopover ({
|
|||
drawerWidth: number;
|
||||
}) {
|
||||
const viewMeta = usePublishContext()?.viewMeta;
|
||||
|
||||
const content = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
||||
className={'flex h-fit max-h-[590px] flex-col overflow-y-auto overflow-x-hidden appflowy-scroller'}
|
||||
>
|
||||
<Outline width={drawerWidth} />
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
--icon-disabled: #525A69;
|
||||
--icon-on-toolbar: white;
|
||||
--line-border: #59647a;
|
||||
--line-divider: #252F41;
|
||||
--line-divider: #384967;
|
||||
--line-on-toolbar: #99a6b8;
|
||||
--fill-default: #00bcf0;
|
||||
--fill-hover: #005174;
|
||||
|
|
@ -43,14 +43,23 @@
|
|||
--function-success-opacity: rgba(59, 168, 86, 0.1);
|
||||
--function-info: #2e9dbb;
|
||||
--tint-red: #56363F;
|
||||
--badge-red: #D32772;
|
||||
--tint-green: #3C5133;
|
||||
--badge-green: #3BA856;
|
||||
--tint-purple: #4D4078;
|
||||
--badge-purple: #5749CA;
|
||||
--tint-blue: #2C3B58;
|
||||
--badge-blue: #00BCF0;
|
||||
--tint-yellow: #695E3E;
|
||||
--badge-yellow: #E9B320;
|
||||
--tint-pink: #5E3C5E;
|
||||
--badge-pink: #BB4A97;
|
||||
--tint-lime: #394027;
|
||||
--badge-lime: #B6D94C;
|
||||
--tint-aqua: #1B3849;
|
||||
--badge-aqua: #00B8E5;
|
||||
--tint-orange: #5E3C3C;
|
||||
--badge-orange: #FF6D00;
|
||||
--shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.3);
|
||||
--scrollbar-track: #252F41;
|
||||
--scrollbar-thumb: #3c4557;
|
||||
|
|
|
|||
|
|
@ -46,14 +46,23 @@
|
|||
--function-success-opacity: rgba(102, 207, 128, 0.1);
|
||||
--function-info: #00bcf0;
|
||||
--tint-purple: #e8e0ff;
|
||||
--badge-purple: #5749ca;
|
||||
--tint-pink: #ffe7ee;
|
||||
--badge-pink: #bb4a97;
|
||||
--tint-red: #ffdddd;
|
||||
--badge-red: #f44336;
|
||||
--tint-lime: #f5ffdc;
|
||||
--badge-lime: #b6d94c;
|
||||
--tint-green: #ddffd6;
|
||||
--badge-green: #66cf80;
|
||||
--tint-aqua: #defff1;
|
||||
--badge-aqua: #00b8e5;
|
||||
--tint-blue: #e1fbff;
|
||||
--badge-blue: #00bcf0;
|
||||
--tint-orange: #ffefe3;
|
||||
--badge-orange: #ff8c00;
|
||||
--tint-yellow: #fff2cd;
|
||||
--badge-yellow: #ffcc00;
|
||||
--shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
|
||||
--scrollbar-thumb: #bdbdbd;
|
||||
--scrollbar-track: #e5e5e5;
|
||||
|
|
|
|||
92
frontend/appflowy_web_app/src/utils/open_schema.ts
Normal file
92
frontend/appflowy_web_app/src/utils/open_schema.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
type OS = 'ios' | 'android' | 'other';
|
||||
|
||||
interface AppConfig {
|
||||
appScheme: string;
|
||||
universalLink?: string;
|
||||
intentUrl?: string;
|
||||
downloadUrl: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export const getOS = (): OS => {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (/iPad|iPhone|iPod/.test(ua)) return 'ios';
|
||||
if (/Android/.test(ua)) return 'android';
|
||||
return 'other';
|
||||
};
|
||||
|
||||
const isWebView = (): boolean => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
return /(webview|wv)/i.test(ua);
|
||||
};
|
||||
|
||||
const createHiddenIframe = (): HTMLIFrameElement => {
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
iframe.style.display = 'none';
|
||||
document.body.appendChild(iframe);
|
||||
return iframe;
|
||||
};
|
||||
|
||||
const removeIframe = (iframe: HTMLIFrameElement): void => {
|
||||
document.body.removeChild(iframe);
|
||||
};
|
||||
|
||||
const redirectToUrl = (url: string): void => {
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
export const openAppOrDownload = (config: AppConfig): void => {
|
||||
const { appScheme, universalLink, intentUrl, downloadUrl, timeout = 3000 } = config;
|
||||
const os = getOS();
|
||||
const iframe = createHiddenIframe();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
removeIframe(iframe);
|
||||
redirectToUrl(downloadUrl);
|
||||
}, timeout);
|
||||
|
||||
const handleVisibilityChange = (): void => {
|
||||
if (!document.hidden) {
|
||||
clearTimeout(timer);
|
||||
removeIframe(iframe);
|
||||
redirectToUrl(downloadUrl);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
const openApp = () => {
|
||||
switch (os) {
|
||||
case 'ios':
|
||||
if (isWebView() || !universalLink) {
|
||||
iframe.src = appScheme;
|
||||
} else {
|
||||
redirectToUrl(universalLink);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'android':
|
||||
if (isWebView() || !intentUrl) {
|
||||
iframe.src = appScheme;
|
||||
} else {
|
||||
redirectToUrl(intentUrl);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
iframe.src = appScheme;
|
||||
}
|
||||
};
|
||||
|
||||
openApp();
|
||||
|
||||
iframe.onload = () => {
|
||||
clearTimeout(timer);
|
||||
removeIframe(iframe);
|
||||
redirectToUrl(downloadUrl);
|
||||
};
|
||||
};
|
||||
|
|
@ -7,13 +7,18 @@ export const downloadPage = 'https://appflowy.io/download';
|
|||
|
||||
export const openAppFlowySchema = 'appflowy-flutter://';
|
||||
|
||||
export function isValidUrl(input: string) {
|
||||
export const iosDownloadLink = 'https://testflight.apple.com/join/6CexvkDz';
|
||||
export const androidDownloadLink = 'https://play.google.com/store/apps/details?id=io.appflowy.appflowy';
|
||||
|
||||
export const desktopDownloadLink = 'https://appflowy.io/download/#pop';
|
||||
|
||||
export function isValidUrl (input: string) {
|
||||
return isURL(input, { require_protocol: true, require_host: false });
|
||||
}
|
||||
|
||||
// Process the URL to make sure it's a valid URL
|
||||
// If it's not a valid URL(eg: 'appflowy.io' or '192.168.1.2'), we'll add 'https://' to the URL
|
||||
export function processUrl(input: string) {
|
||||
export function processUrl (input: string) {
|
||||
let processedUrl = input;
|
||||
|
||||
if (isValidUrl(input)) {
|
||||
|
|
@ -40,7 +45,7 @@ export function processUrl(input: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
export async function openUrl(url: string, target: string = '_current') {
|
||||
export async function openUrl (url: string, target: string = '_current') {
|
||||
const platform = getPlatform();
|
||||
|
||||
const newUrl = processUrl(url);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Fri, 06 Sep 2024 04:24:51 GMT
|
||||
* Generated on Fri, 06 Sep 2024 10:13:48 GMT
|
||||
* Generated from $pnpm css:variables
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Fri, 06 Sep 2024 04:24:51 GMT
|
||||
* Generated on Fri, 06 Sep 2024 10:13:48 GMT
|
||||
* Generated from $pnpm css:variables
|
||||
*/
|
||||
|
||||
|
|
@ -76,6 +76,17 @@ module.exports = {
|
|||
"orange": "var(--tint-orange)",
|
||||
"yellow": "var(--tint-yellow)"
|
||||
},
|
||||
"badge": {
|
||||
"purple": "var(--badge-purple)",
|
||||
"pink": "var(--badge-pink)",
|
||||
"red": "var(--badge-red)",
|
||||
"lime": "var(--badge-lime)",
|
||||
"green": "var(--badge-green)",
|
||||
"aqua": "var(--badge-aqua)",
|
||||
"blue": "var(--badge-blue)",
|
||||
"orange": "var(--badge-orange)",
|
||||
"yellow": "var(--badge-yellow)"
|
||||
},
|
||||
"scrollbar": {
|
||||
"thumb": "var(--scrollbar-thumb)",
|
||||
"track": "var(--scrollbar-track)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue