fix: modified bullted icon style
@ -72,6 +72,12 @@ http {
|
|||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /covers/ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
error_page 404 /404.html;
|
error_page 404 /404.html;
|
||||||
location = /404.html {
|
location = /404.html {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_1.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_2.png
Normal file
After Width: | Height: | Size: 731 KiB |
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_3.png
Normal file
After Width: | Height: | Size: 465 KiB |
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_4.png
Normal file
After Width: | Height: | Size: 526 KiB |
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_5.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
frontend/appflowy_web_app/public/covers/m_cover_image_6.png
Normal file
After Width: | Height: | Size: 765 KiB |
Before Width: | Height: | Size: 1.1 MiB |
@ -56,12 +56,13 @@ const fetchMetaData = async (url) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BASE_URL = process.env.AF_BASE_URL || 'https://beta.appflowy.cloud';
|
|
||||||
const createServer = async (req) => {
|
const createServer = async (req) => {
|
||||||
const timer = logRequestTimer(req);
|
const timer = logRequestTimer(req);
|
||||||
const reqUrl = new URL(req.url);
|
const reqUrl = new URL(req.url);
|
||||||
|
|
||||||
logger.info(`Request URL: ${reqUrl.pathname}`);
|
const hostname = req.headers.get('host');
|
||||||
|
|
||||||
|
logger.info(`Request URL: ${hostname}${reqUrl.pathname}`);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
namespace,
|
namespace,
|
||||||
@ -85,7 +86,11 @@ const createServer = async (req) => {
|
|||||||
let metaData;
|
let metaData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
metaData = await fetchMetaData(`${BASE_URL}/api/workspace/published/${namespace}/${publishName}`);
|
const isBeta = hostname.startsWith('beta');
|
||||||
|
const isTest = hostname.startsWith('test');
|
||||||
|
const defaultUrl = 'https://beta.appflowy.cloud';
|
||||||
|
const baseUrl = isBeta ? 'https://beta.appflowy.cloud' : isTest ? 'https://test.appflowy.cloud' : defaultUrl;
|
||||||
|
metaData = await fetchMetaData(`${baseUrl}/api/workspace/published/${namespace}/${publishName}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error fetching meta data: ${error}`);
|
logger.error(`Error fetching meta data: ${error}`);
|
||||||
}
|
}
|
||||||
@ -105,8 +110,13 @@ const createServer = async (req) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const cover = metaData.view.extra ? JSON.parse(metaData.view.extra)?.cover : null;
|
const cover = metaData.view.extra ? JSON.parse(metaData.view.extra)?.cover : null;
|
||||||
if (cover && ['unsplash', 'custom'].includes(cover.type)) {
|
if (cover) {
|
||||||
image = cover.value;
|
if (['unsplash', 'custom'].includes(cover.type)) {
|
||||||
|
image = cover.value;
|
||||||
|
} else if (cover.type === 'built_in') {
|
||||||
|
image = `/covers/m_cover_image_${cover.value}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
@ -152,7 +162,6 @@ const start = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.info(`Server is running on port 3000`);
|
logger.info(`Server is running on port 3000`);
|
||||||
logger.info(`Base API URL: ${process.env.AF_BASE_URL}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -61,6 +61,7 @@ export class AFClientService implements AFService {
|
|||||||
const name = `${namespace}_${publishName}`;
|
const name = `${namespace}_${publishName}`;
|
||||||
|
|
||||||
const isLoaded = this.publishViewLoaded.has(name);
|
const isLoaded = this.publishViewLoaded.has(name);
|
||||||
|
|
||||||
const doc = await getPublishView(
|
const doc = await getPublishView(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -7,7 +7,7 @@ describe('convert yjs data to slate content', () => {
|
|||||||
it('should return undefined if root block is not exist', () => {
|
it('should return undefined if root block is not exist', () => {
|
||||||
const doc = new Y.Doc();
|
const doc = new Y.Doc();
|
||||||
|
|
||||||
expect(() => yDocToSlateContent(doc)).toThrowError();
|
expect(() => yDocToSlateContent(doc)).toBeUndefined();
|
||||||
|
|
||||||
const doc2 = withTestingYDoc('1');
|
const doc2 = withTestingYDoc('1');
|
||||||
const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc2);
|
const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc2);
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="9" cy="9" r="2.75" fill="#454545"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 151 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="9" cy="9" r="2.75" stroke="#454545"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 153 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="5" height="5" transform="translate(6.5 6.5)" fill="#454545"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 178 B |
@ -4,23 +4,25 @@ import React, { createContext, useEffect, useState } from 'react';
|
|||||||
import { AFService, AFServiceConfig } from '@/application/services/services.type';
|
import { AFService, AFServiceConfig } from '@/application/services/services.type';
|
||||||
import { getService } from '@/application/services';
|
import { getService } from '@/application/services';
|
||||||
|
|
||||||
|
const hostName = window.location.hostname;
|
||||||
|
const isProd = !hostName.includes('localhost');
|
||||||
|
const isBeta = isProd && hostName.includes('beta');
|
||||||
|
const isTest = isProd && hostName.includes('test');
|
||||||
|
const baseAPIHost = isProd
|
||||||
|
? isBeta
|
||||||
|
? 'beta.appflowy.cloud'
|
||||||
|
: isTest
|
||||||
|
? 'test.appflowy.cloud'
|
||||||
|
: 'beta.appflowy.cloud'
|
||||||
|
: 'test.appflowy.cloud';
|
||||||
|
const baseURL = `https://${baseAPIHost}`;
|
||||||
|
const gotrueURL = `${baseURL}/gotrue`;
|
||||||
|
|
||||||
const defaultConfig: AFServiceConfig = {
|
const defaultConfig: AFServiceConfig = {
|
||||||
cloudConfig: {
|
cloudConfig: {
|
||||||
baseURL: import.meta.env.AF_BASE_URL
|
baseURL,
|
||||||
? import.meta.env.AF_BASE_URL
|
gotrueURL,
|
||||||
: import.meta.env.DEV
|
wsURL: `wss://${baseAPIHost}/ws/v1`,
|
||||||
? 'https://test.appflowy.cloud'
|
|
||||||
: 'https://beta.appflowy.cloud',
|
|
||||||
gotrueURL: import.meta.env.AF_GOTRUE_URL
|
|
||||||
? import.meta.env.AF_GOTRUE_URL
|
|
||||||
: import.meta.env.DEV
|
|
||||||
? 'https://test.appflowy.cloud/gotrue'
|
|
||||||
: 'https://beta.appflowy.cloud/gotrue',
|
|
||||||
wsURL: import.meta.env.AF_WS_URL
|
|
||||||
? import.meta.env.AF_WS_URL
|
|
||||||
: import.meta.env.DEV
|
|
||||||
? 'wss://test.appflowy.cloud/ws/v1'
|
|
||||||
: 'wss://beta.appflowy.cloud/ws/v1',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { useDecorate } from '@/components/editor/components/blocks/code/useDecorate';
|
import { useDecorate } from '@/components/editor/components/blocks/code/useDecorate';
|
||||||
import { Leaf } from '@/components/editor/components/leaf';
|
import { Leaf } from '@/components/editor/components/leaf';
|
||||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||||
import React, { useCallback } from 'react';
|
import React, { Suspense, useCallback } from 'react';
|
||||||
import { NodeEntry } from 'slate';
|
import { NodeEntry } from 'slate';
|
||||||
import { Editable, ReactEditor, RenderElementProps } from 'slate-react';
|
import { Editable, ReactEditor, RenderElementProps } from 'slate-react';
|
||||||
import { Element } from './components/element';
|
import { Element } from './components/element';
|
||||||
|
import { Skeleton } from '@mui/material';
|
||||||
|
|
||||||
const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
||||||
const { readOnly } = useEditorContext();
|
const { readOnly } = useEditorContext();
|
||||||
@ -17,7 +18,14 @@ const EditorEditable = ({ editor }: { editor: ReactEditor }) => {
|
|||||||
[codeDecorate]
|
[codeDecorate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []);
|
const renderElement = useCallback(
|
||||||
|
(props: RenderElementProps) => (
|
||||||
|
<Suspense fallback={<Skeleton width={'100%'} height={24} />}>
|
||||||
|
<Element {...props} />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -28,11 +28,11 @@ export function BulletedListIcon({ block, className }: { block: BulletedListNode
|
|||||||
const dataLetter = useMemo(() => {
|
const dataLetter = useMemo(() => {
|
||||||
switch (letter) {
|
switch (letter) {
|
||||||
case Letter.Disc:
|
case Letter.Disc:
|
||||||
return '•';
|
return 'disc';
|
||||||
case Letter.Circle:
|
case Letter.Circle:
|
||||||
return '◦';
|
return 'circle';
|
||||||
case Letter.Square:
|
case Letter.Square:
|
||||||
return '▪';
|
return 'square';
|
||||||
}
|
}
|
||||||
}, [letter]);
|
}, [letter]);
|
||||||
|
|
||||||
@ -41,9 +41,8 @@ export function BulletedListIcon({ block, className }: { block: BulletedListNode
|
|||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
data-letter={dataLetter}
|
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`}
|
className={`${className} bulleted-icon ${dataLetter} flex min-w-[24px] justify-center pr-1 font-medium`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ export const LinkPreview = memo(
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
const url = node.data.url;
|
const url = node.data.url;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -17,14 +18,19 @@ export const LinkPreview = memo(
|
|||||||
setData(null);
|
setData(null);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
setNotFound(false);
|
||||||
const response = await axios.get(`https://api.microlink.io/?url=${url}`);
|
const response = await axios.get(`https://api.microlink.io/?url=${url}`);
|
||||||
|
|
||||||
if (response.data.statusCode !== 200) return;
|
if (response.data.statusCode !== 200) {
|
||||||
|
setNotFound(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
setData(data);
|
setData(data);
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
// don't do anything
|
setNotFound(true);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [url]);
|
}, [url]);
|
||||||
@ -37,17 +43,22 @@ export const LinkPreview = memo(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={`link-preview-block relative w-full cursor-pointer py-1`}
|
className={`link-preview-block relative w-full cursor-pointer py-1`}
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
{data ? (
|
className={
|
||||||
<div
|
'container-bg flex w-full cursor-pointer select-none items-center gap-4 overflow-hidden rounded border border-line-divider bg-fill-list-active p-3'
|
||||||
className={
|
}
|
||||||
'container-bg flex w-full cursor-pointer select-none items-center gap-4 overflow-hidden rounded border border-line-divider bg-fill-list-active p-3'
|
>
|
||||||
}
|
{notFound ? (
|
||||||
>
|
<div className={'flex w-full items-center justify-center'}>
|
||||||
|
<div className={'text-text-title'}>Could not load preview</div>
|
||||||
|
<div className={'text-sm text-text-caption'}>{url}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<img
|
<img
|
||||||
src={data.image.url}
|
src={data?.image.url}
|
||||||
alt={data.title}
|
alt={data?.title}
|
||||||
className={'container h-full w-[25%] rounded bg-cover bg-center'}
|
className={'container h-full min-h-[48px] w-[25%] rounded bg-cover bg-center'}
|
||||||
/>
|
/>
|
||||||
<div className={'flex flex-col justify-center gap-2 overflow-hidden'}>
|
<div className={'flex flex-col justify-center gap-2 overflow-hidden'}>
|
||||||
<div
|
<div
|
||||||
@ -55,22 +66,18 @@ export const LinkPreview = memo(
|
|||||||
'max-h-[48px] overflow-hidden whitespace-pre-wrap break-words text-base font-bold text-text-title'
|
'max-h-[48px] overflow-hidden whitespace-pre-wrap break-words text-base font-bold text-text-title'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{data.title}
|
{data?.title}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'max-h-[64px] overflow-hidden truncate whitespace-pre-wrap break-words text-sm text-text-title'
|
'max-h-[64px] overflow-hidden truncate whitespace-pre-wrap break-words text-sm text-text-title'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{data.description}
|
{data?.description}
|
||||||
</div>
|
</div>
|
||||||
<div className={'truncate whitespace-nowrap text-xs text-text-caption'}>{url}</div>
|
<div className={'truncate whitespace-nowrap text-xs text-text-caption'}>{url}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : (
|
|
||||||
<a href={node.data.url} className={'text-content-blue-400 underline'} target={'blank'}>
|
|
||||||
{node.data.url}
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
||||||
|
@ -16,7 +16,6 @@ import { Quote } from '@/components/editor/components/blocks/quote';
|
|||||||
import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table';
|
import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table';
|
||||||
import { Text } from '@/components/editor/components/blocks/text';
|
import { Text } from '@/components/editor/components/blocks/text';
|
||||||
import { ElementFallbackRender } from '@/components/error/ElementFallbackRender';
|
import { ElementFallbackRender } from '@/components/error/ElementFallbackRender';
|
||||||
import { Skeleton } from '@mui/material';
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { TodoList } from 'src/components/editor/components/blocks/todo-list';
|
import { TodoList } from 'src/components/editor/components/blocks/todo-list';
|
||||||
import { ToggleList } from 'src/components/editor/components/blocks/toggle-list';
|
import { ToggleList } from 'src/components/editor/components/blocks/toggle-list';
|
||||||
@ -25,7 +24,7 @@ import { Formula } from '@/components/editor/components/leaf/formula';
|
|||||||
import { Mention } from '@/components/editor/components/leaf/mention';
|
import { Mention } from '@/components/editor/components/leaf/mention';
|
||||||
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
||||||
import { renderColor } from '@/utils/color';
|
import { renderColor } from '@/utils/color';
|
||||||
import React, { FC, memo, Suspense, useMemo } from 'react';
|
import React, { FC, memo, useMemo } from 'react';
|
||||||
import { RenderElementProps } from 'slate-react';
|
import { RenderElementProps } from 'slate-react';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
|
||||||
@ -126,15 +125,13 @@ export const Element = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Skeleton width={'100%'} height={24} />}>
|
<ErrorBoundary fallbackRender={ElementFallbackRender}>
|
||||||
<ErrorBoundary fallbackRender={ElementFallbackRender}>
|
<div {...attributes} data-block-type={node.type} className={className}>
|
||||||
<div {...attributes} data-block-type={node.type} className={className}>
|
<Component style={style} className={`flex w-full flex-col`} node={node}>
|
||||||
<Component style={style} className={`flex w-full flex-col`} node={node}>
|
{children}
|
||||||
{children}
|
</Component>
|
||||||
</Component>
|
</div>
|
||||||
</div>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) => isEqual(prevProps.element, nextProps.element)
|
(prevProps, nextProps) => isEqual(prevProps.element, nextProps.element)
|
||||||
|
@ -197,10 +197,22 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bulleted-icon {
|
.bulleted-icon {
|
||||||
&:after {
|
background-repeat: no-repeat;
|
||||||
content: attr(data-letter);
|
background-size: 16px 16px;
|
||||||
font-weight: 500;
|
background-position: center;
|
||||||
|
|
||||||
|
&.disc {
|
||||||
|
background-image: url('../../assets/bulleted_list_icon_1.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.circle {
|
||||||
|
background-image: url('../../assets/bulleted_list_icon_2.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.square {
|
||||||
|
background-image: url('../../assets/bulleted_list_icon_3.svg');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.numbered-icon {
|
.numbered-icon {
|
||||||
|
@ -52,12 +52,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
font-family: 'Apple Color Emoji', 'Noto Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-icon {
|
.view-icon {
|
||||||
@apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em];
|
@apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em];
|
||||||
font-family: 'Apple Color Emoji', 'Noto Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|