mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
milestone: publish alpha version (#637)
- document folder - full-text search - blob storage - basic edgeless support Co-authored-by: tzhangchi <terry.zhangchi@outlook.com> Co-authored-by: QiShaoXuan <qishaoxuan777@gmail.com> Co-authored-by: DiamondThree <diamond.shx@gmail.com> Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Yifeng Wang <doodlewind@toeverything.info> Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com>
This commit is contained in:
9
packages/app/.env.local.template
Normal file
9
packages/app/.env.local.template
Normal file
@@ -0,0 +1,9 @@
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY=
|
||||
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
|
||||
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
|
||||
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
|
||||
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
|
||||
NEXT_PUBLIC_FIREBASE_APP_ID=
|
||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
|
||||
# absolute path to the block suite directory
|
||||
LOCAL_BLOCK_SUITE=
|
||||
@@ -1,18 +0,0 @@
|
||||
// https://eslint.org/docs/latest/user-guide/configuring
|
||||
// "off" or 0 - turn the rule off
|
||||
// "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code)
|
||||
// "error" or 2 - turn the rule on as an error (exit code will be 1)
|
||||
|
||||
/** @type { import('eslint').Linter.Config } */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
|
||||
reportUnusedDisableDirectives: true,
|
||||
};
|
||||
7
packages/app/CHANGELOG.md
Normal file
7
packages/app/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @affine/app
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- cc72448: add changeset config
|
||||
@@ -1,10 +1,13 @@
|
||||
// @ts-check
|
||||
/* eslint @typescript-eslint/no-var-requires: "off" */
|
||||
const { getGitVersion, getCommitHash } = require('./scripts/gitInfo');
|
||||
const { dependencies } = require('./package.json');
|
||||
const path = require('node:path');
|
||||
const printer = require('./scripts/printer').printer;
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
productionBrowserSourceMaps: true,
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
swcMinify: false,
|
||||
publicRuntimeConfig: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
@@ -13,8 +16,58 @@ const nextConfig = {
|
||||
CI: process.env.CI || null,
|
||||
VERSION: getGitVersion(),
|
||||
COMMIT_HASH: getCommitHash(),
|
||||
EDITOR_VERSION: dependencies['@blocksuite/editor'],
|
||||
},
|
||||
webpack: config => {
|
||||
config.resolve.alias['yjs'] = require.resolve('yjs');
|
||||
config.module.rules.push({
|
||||
test: /\.md$/i,
|
||||
loader: 'raw-loader',
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
// XXX not test yet
|
||||
rewrites: async () => {
|
||||
if (process.env.NODE_API_SERVER === 'ac') {
|
||||
let destinationAC = 'http://100.85.73.88:12001/api/:path*';
|
||||
printer.info('API request proxy to [AC Server] ' + destinationAC);
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: destinationAC,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
let destinationStandard = 'http://100.77.180.48:11001/api/:path*';
|
||||
printer.info(
|
||||
'API request proxy to [Standard Server] ' + destinationStandard
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: destinationStandard,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
basePath: process.env.BASE_PATH,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';
|
||||
const withDebugLocal = require('next-debug-local')(
|
||||
{
|
||||
'@blocksuite/editor': path.resolve(baseDir, 'packages', 'editor'),
|
||||
'@blocksuite/blocks': path.resolve(baseDir, 'packages', 'blocks'),
|
||||
'@blocksuite/store': path.resolve(baseDir, 'packages', 'store'),
|
||||
},
|
||||
{
|
||||
enable: path.isAbsolute(process.env.LOCAL_BLOCK_SUITE ?? ''),
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = withDebugLocal(nextConfig);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"name": "@pathfinder/app",
|
||||
"version": "0.1.0",
|
||||
"name": "@affine/app",
|
||||
"version": "0.3.1",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 8080",
|
||||
"dev:ac": "NODE_API_SERVER=ac next dev -p 8080",
|
||||
"build": "next build",
|
||||
"export": "next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocksuite/blocks": "0.3.0-alpha.4",
|
||||
"@blocksuite/editor": "0.3.0-alpha.4",
|
||||
"@blocksuite/store": "0.3.0-alpha.4",
|
||||
"@affine/data-services": "workspace:*",
|
||||
"@blocksuite/blocks": "0.3.0-20221230100352-5dfe65e",
|
||||
"@blocksuite/editor": "0.3.0-20221230100352-5dfe65e",
|
||||
"@blocksuite/icons": "^2.0.2",
|
||||
"@blocksuite/store": "0.3.0-20221230100352-5dfe65e",
|
||||
"@emotion/css": "^11.10.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/server": "^11.10.0",
|
||||
@@ -21,24 +24,43 @@
|
||||
"@mui/base": "^5.0.0-alpha.87",
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
||||
"@toeverything/pathfinder-logger": "workspace:@affine/logger@*",
|
||||
"cmdk": "^0.1.20",
|
||||
"css-spring": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"i18next": "^21.9.1",
|
||||
"lit": "^2.3.1",
|
||||
"next": "13.0.1",
|
||||
"next": "13.1.0",
|
||||
"next-debug-local": "^0.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"quill": "^1.3.7",
|
||||
"quill-cursors": "^4.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^11.18.4",
|
||||
"yjs": "^13.5.43"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.18",
|
||||
"@types/react": "18.0.20",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"chalk-next": "^6.1.5",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"typescript": "4.8.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:@next/next/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "warn"
|
||||
},
|
||||
"reportUnusedDisableDirectives": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ select,
|
||||
keygen,
|
||||
legend {
|
||||
color: var(--affine-text-color);
|
||||
background-color: unset;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
font-family: var(--affine-font-family);
|
||||
|
||||
21
packages/app/scripts/__tests__/printer.spec.ts
Normal file
21
packages/app/scripts/__tests__/printer.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { printer } from './../printer';
|
||||
const chalk = require('chalk');
|
||||
describe('printer', () => {
|
||||
test('test debug', () => {
|
||||
expect(printer.debug('test debug')).toBe(
|
||||
chalk.green`debug` + chalk.white(' - test debug')
|
||||
);
|
||||
});
|
||||
|
||||
test('test info', () => {
|
||||
expect(printer.info('test info')).toBe(
|
||||
chalk.rgb(19, 167, 205)`info` + chalk.white(' - test info')
|
||||
);
|
||||
});
|
||||
test('test warn', () => {
|
||||
expect(printer.warn('test warn')).toBe(
|
||||
chalk.yellow`warn` + chalk.white(' - test warn')
|
||||
);
|
||||
});
|
||||
});
|
||||
20
packages/app/scripts/printer.js
Normal file
20
packages/app/scripts/printer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const chalk = require('chalk');
|
||||
const printer = {
|
||||
debug: msg => {
|
||||
const result = chalk.green`debug` + chalk.white(' - ' + msg);
|
||||
console.log(result);
|
||||
return result;
|
||||
},
|
||||
info: msg => {
|
||||
const result = chalk.rgb(19, 167, 205)`info` + chalk.white(' - ' + msg);
|
||||
console.log(result);
|
||||
return result;
|
||||
},
|
||||
warn: msg => {
|
||||
const result = chalk.yellow`warn` + chalk.white(' - ' + msg);
|
||||
console.log(result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { printer };
|
||||
11
packages/app/src/components/404/index.tsx
Normal file
11
packages/app/src/components/404/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NotFoundTitle, PageContainer } from './styles';
|
||||
|
||||
export const NotfoundPage = () => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<NotFoundTitle>404 - Page Not Found</NotFoundTitle>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotfoundPage;
|
||||
21
packages/app/src/components/404/styles.ts
Normal file
21
packages/app/src/components/404/styles.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const PageContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: 'calc(100vh)',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
};
|
||||
});
|
||||
|
||||
export const NotFoundTitle = styled('h1')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
top: 'calc(50% - 100px)',
|
||||
height: '100px',
|
||||
fontSize: '60px',
|
||||
lineHeight: '100px',
|
||||
color: theme.colors.textColor,
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
@@ -1,119 +0,0 @@
|
||||
import type { DOMAttributes, CSSProperties } from 'react';
|
||||
type IconProps = {
|
||||
style?: CSSProperties;
|
||||
} & DOMAttributes<SVGElement>;
|
||||
|
||||
export const RightArrow = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.667 10.0007L8.33366 6.66732L8.33366 13.334L11.667 10.0007Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const Export2Markdown = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.99963 1.66707V1.66699H10.833L16.6663 7.50033V7.50037L16.6663 7.50041V15.8337C16.6663 17.2145 15.5471 18.3337 14.1663 18.3337H5.83301C4.4523 18.3337 3.33301 17.2145 3.33301 15.8337V4.16707C3.33301 2.78636 4.4523 1.66707 5.83301 1.66707H9.99963ZM9.99963 3.00041H5.83301C5.18868 3.00041 4.66634 3.52274 4.66634 4.16707V15.8337C4.66634 16.4781 5.18868 17.0004 5.83301 17.0004H14.1663C14.8107 17.0004 15.333 16.4781 15.333 15.8337V8.33366H12.4996C11.1189 8.33366 9.99963 7.21437 9.99963 5.83366V3.00041ZM11.333 4.05265L14.2806 7.00033H12.4996C11.8553 7.00033 11.333 6.47799 11.333 5.83366V4.05265Z"
|
||||
/>
|
||||
<path d="M11.245 14.1671L11.2683 11.2446H11.2508L10.1775 14.1671H9.4775L8.43333 11.2446H8.41583L8.43917 14.1671H7.5V10.0371H8.9175L9.85667 12.6854H9.88L10.7783 10.0371H12.2192V14.1671H11.245Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const Export2HTML = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path d="M5.35987 18.3335V16.7055H3.84187V18.3335H2.90137V14.4395H3.84187V15.9135H5.35987V14.4395H6.30037V18.3335H5.35987Z" />
|
||||
<path d="M8.76806 15.2425V18.3335H7.82756V15.2425H6.72756V14.4395H9.86806V15.2425H8.76806Z" />
|
||||
<path d="M13.8284 18.3335L13.8504 15.578H13.8339L12.8219 18.3335H12.1619L11.1774 15.578H11.1609L11.1829 18.3335H10.2974V14.4395H11.6339L12.5194 16.9365H12.5414L13.3884 14.4395H14.7469V18.3335H13.8284Z" />
|
||||
<path d="M15.5503 18.3335V14.4395H16.4963V17.514H18.0033V18.3335H15.5503Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.99963 1.66707V1.66699H10.833L11.333 2.16699L16.1663 7.00032L16.6663 7.50033V7.50037L16.6663 7.50041V13.3337H15.333V8.33366H15.333H12.4996C11.1189 8.33366 9.99963 7.21437 9.99963 5.83366V3.00041H5.83301C5.18868 3.00041 4.66634 3.52274 4.66634 4.16707V13.3337H3.33301V4.16707C3.33301 2.78636 4.4523 1.66707 5.83301 1.66707H9.99963ZM11.333 4.05265V5.83366C11.333 6.47799 11.8553 7.00032 12.4996 7.00032H14.2806L11.333 4.05265Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const LogoIcon = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10.552 2 4 21h3.838l4.168-13.14L16.176 21H20L13.447 2h-2.895Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoreIcon = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<circle cx="12" cy="5.5" r="1.5" />
|
||||
<circle cx="12" cy="12" r="1.5" />
|
||||
<circle cx="12" cy="18.5" r="1.5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const ExportIcon = ({ style = {}, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 3.19995C12.2121 3.19995 12.4156 3.28424 12.5656 3.43427L16.5656 7.43427L15.4343 8.56564L12.8 5.93132V14H11.2V5.93132L8.56564 8.56564L7.43427 7.43427L11.4343 3.43427C11.5843 3.28424 11.7878 3.19995 12 3.19995ZM3.79995 12V16.7992C3.79995 17.3724 3.80057 17.7543 3.82454 18.0476C3.84775 18.3317 3.88879 18.4616 3.93074 18.544C4.04579 18.7698 4.22937 18.9533 4.45516 19.0684C4.5375 19.1103 4.66747 19.1514 4.9515 19.1746C5.24487 19.1985 5.6267 19.1992 6.19995 19.1992H17.8C18.3732 19.1992 18.755 19.1985 19.0484 19.1746C19.3324 19.1514 19.4624 19.1103 19.5447 19.0684C19.7705 18.9533 19.9541 18.7698 20.0692 18.544C20.1111 18.4616 20.1522 18.3317 20.1754 18.0476C20.1993 17.7543 20.2 17.3724 20.2 16.7992V12H21.8V16.8314C21.8 17.364 21.8 17.8116 21.77 18.1779C21.7388 18.5609 21.6708 18.9249 21.4948 19.2703C21.2263 19.7972 20.798 20.2255 20.2711 20.494C19.9256 20.67 19.5617 20.738 19.1787 20.7693C18.8124 20.7992 18.3648 20.7992 17.8322 20.7992H6.16775C5.63509 20.7992 5.18749 20.7992 4.82121 20.7693C4.43823 20.738 4.07426 20.67 3.72878 20.494C3.20193 20.2255 2.77358 19.7972 2.50513 19.2703C2.3291 18.9249 2.26115 18.5609 2.22986 18.1779C2.19993 17.8116 2.19994 17.364 2.19995 16.8313L2.19995 12H3.79995Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
LogoIcon,
|
||||
MoreIcon,
|
||||
ExportIcon,
|
||||
Export2Markdown,
|
||||
Export2HTML,
|
||||
RightArrow,
|
||||
} from './icons';
|
||||
import {
|
||||
StyledHeader,
|
||||
StyledTitle,
|
||||
StyledTitleWrapper,
|
||||
StyledLogo,
|
||||
StyledHeaderRightSide,
|
||||
IconButton,
|
||||
StyledHeaderContainer,
|
||||
StyledBrowserWarning,
|
||||
StyledCloseButton,
|
||||
StyledMenuItemWrapper,
|
||||
} from './styles';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
import EditorModeSwitch from '@/components/editor-mode-switch';
|
||||
import { EdgelessIcon, PaperIcon } from '../editor-mode-switch/icons';
|
||||
import ThemeModeSwitch from '@/components/theme-mode-switch';
|
||||
import { useModal } from '@/components/global-modal-provider';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor, mode, setMode } = useEditor();
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setMode(mode === 'page' ? 'edgeless' : 'page');
|
||||
}}
|
||||
>
|
||||
<StyledMenuItemWrapper>
|
||||
{mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
|
||||
Convert to {mode === 'page' ? 'Edgeless' : 'Page'}
|
||||
</StyledMenuItemWrapper>
|
||||
</MenuItem>
|
||||
<Menu
|
||||
placement="left-start"
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportHtml();
|
||||
}}
|
||||
>
|
||||
<StyledMenuItemWrapper>
|
||||
<Export2HTML />
|
||||
Export to HTML
|
||||
</StyledMenuItemWrapper>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportMarkdown();
|
||||
}}
|
||||
>
|
||||
<StyledMenuItemWrapper>
|
||||
<Export2Markdown />
|
||||
Export to Markdown
|
||||
</StyledMenuItemWrapper>
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MenuItem>
|
||||
<StyledMenuItemWrapper>
|
||||
<ExportIcon />
|
||||
Export
|
||||
<RightArrow />
|
||||
</StyledMenuItemWrapper>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BrowserWarning = ({ onClose }: { onClose: () => void }) => {
|
||||
return (
|
||||
<StyledBrowserWarning>
|
||||
{getWarningMessage()}
|
||||
<StyledCloseButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</StyledCloseButton>
|
||||
</StyledBrowserWarning>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const [showWarning, setShowWarning] = useState(shouldShowWarning());
|
||||
|
||||
const { contactModalHandler } = useModal();
|
||||
const { editor } = useEditor();
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
setTitle(editor.model.title || '');
|
||||
editor.model.propsUpdated.on(() => {
|
||||
setTitle(editor.model.title);
|
||||
});
|
||||
}
|
||||
}, [editor]);
|
||||
return (
|
||||
<StyledHeaderContainer hasWarning={showWarning} data-tauri-drag-region>
|
||||
<BrowserWarning
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
<StyledHeader hasWarning={showWarning} data-tauri-drag-region>
|
||||
<StyledLogo
|
||||
data-testid="left-top-corner-logo"
|
||||
onClick={() => {
|
||||
contactModalHandler(true);
|
||||
}}
|
||||
>
|
||||
<LogoIcon />
|
||||
</StyledLogo>
|
||||
{title ? (
|
||||
<StyledTitle
|
||||
data-tauri-drag-region
|
||||
onMouseEnter={() => {
|
||||
setIsHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<EditorModeSwitch
|
||||
isHover={isHover}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
<StyledTitleWrapper>{title}</StyledTitleWrapper>
|
||||
</StyledTitle>
|
||||
) : null}
|
||||
|
||||
<StyledHeaderRightSide>
|
||||
<ThemeModeSwitch />
|
||||
<Menu content={<PopoverContent />} placement="bottom-end">
|
||||
<IconButton>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledHeaderRightSide>
|
||||
</StyledHeader>
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export const GithubIcon = () => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const DiscordIcon = (props: any) => {
|
||||
export const DiscordIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="22"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Modal from '@/ui/modal';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { Modal, ModalCloseButton, ModalWrapper } from '@/ui/modal';
|
||||
import {
|
||||
LogoIcon,
|
||||
DocIcon,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
} from './icons';
|
||||
import logo from './affine-text-logo.png';
|
||||
import {
|
||||
StyledModalWrapper,
|
||||
StyledBigLink,
|
||||
StyledSmallLink,
|
||||
StyledSubTitle,
|
||||
@@ -22,9 +20,9 @@ import {
|
||||
StyledLogo,
|
||||
StyledModalHeader,
|
||||
StyledModalHeaderLeft,
|
||||
StyledCloseButton,
|
||||
StyledModalFooter,
|
||||
} from './style';
|
||||
import bg from '@/components/contact-modal/bg.png';
|
||||
|
||||
const linkList = [
|
||||
{
|
||||
@@ -62,9 +60,9 @@ const rightLinkList = [
|
||||
},
|
||||
{
|
||||
icon: <DocIcon />,
|
||||
title: 'Check Our Docs',
|
||||
subTitle: 'docs.AFFiNE.pro',
|
||||
link: 'https://docs.affine.pro',
|
||||
title: 'AFFiNE Community',
|
||||
subTitle: 'community.affine.pro',
|
||||
link: 'https://community.affine.pro',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -75,20 +73,24 @@ type TransitionsModalProps = {
|
||||
|
||||
export const ContactModal = ({ open, onClose }: TransitionsModalProps) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper data-testid="contact-us-modal-content">
|
||||
<Modal open={open} onClose={onClose} data-testid="contact-us-modal-content">
|
||||
<ModalWrapper
|
||||
width={860}
|
||||
height={540}
|
||||
style={{ backgroundImage: `url(${bg.src})` }}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledLogo src={logo.src} alt="" />
|
||||
<span>Alpha</span>
|
||||
</StyledModalHeaderLeft>
|
||||
<StyledCloseButton
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon width={12} height={12} />
|
||||
</StyledCloseButton>
|
||||
/>
|
||||
</StyledModalHeader>
|
||||
|
||||
<StyledContent>
|
||||
@@ -107,10 +109,7 @@ export const ContactModal = ({ open, onClose }: TransitionsModalProps) => {
|
||||
})}
|
||||
</StyledLeftContainer>
|
||||
<StyledRightContainer>
|
||||
<StyledSubTitle>
|
||||
Get in touch! <br />
|
||||
Join our community.
|
||||
</StyledSubTitle>
|
||||
<StyledSubTitle>Get in touch!</StyledSubTitle>
|
||||
{linkList.map(({ icon, title, link }) => {
|
||||
return (
|
||||
<StyledSmallLink key={title} href={link} target="_blank">
|
||||
@@ -134,7 +133,7 @@ export const ContactModal = ({ open, onClose }: TransitionsModalProps) => {
|
||||
</p>
|
||||
<p>Copyright © 2022 Toeverything</p>
|
||||
</StyledModalFooter>
|
||||
</StyledModalWrapper>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { absoluteCenter, displayFlex, styled } from '@/styles';
|
||||
import bg from './bg.png';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '860px',
|
||||
height: '540px',
|
||||
backgroundColor: theme.colors.popoverBackground,
|
||||
backgroundImage: `url(${bg.src})`,
|
||||
borderRadius: '20px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledBigLink = styled('a')(({ theme }) => {
|
||||
return {
|
||||
width: '320px',
|
||||
width: '334px',
|
||||
height: '100px',
|
||||
marginBottom: '48px',
|
||||
paddingLeft: '114px',
|
||||
paddingLeft: '90px',
|
||||
fontSize: '24px',
|
||||
lineHeight: '36px',
|
||||
fontWeight: '600',
|
||||
@@ -46,7 +29,7 @@ export const StyledBigLink = styled('a')(({ theme }) => {
|
||||
height: '50px',
|
||||
marginRight: '40px',
|
||||
color: theme.colors.primaryColor,
|
||||
...absoluteCenter({ vertical: true, position: { left: '32px' } }),
|
||||
...absoluteCenter({ vertical: true, position: { left: '20px' } }),
|
||||
},
|
||||
p: {
|
||||
width: '100%',
|
||||
@@ -102,6 +85,7 @@ export const StyledSmallLink = styled('a')(({ theme }) => {
|
||||
});
|
||||
export const StyledSubTitle = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '190px',
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
color: theme.colors.textColor,
|
||||
@@ -136,7 +120,7 @@ export const StyledLogo = styled('img')({
|
||||
width: 'auto',
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
height: '20px',
|
||||
marginTop: '36px',
|
||||
@@ -162,40 +146,6 @@ export const StyledModalHeaderLeft = styled('div')(({ theme }) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCloseButton = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
color: theme.colors.iconColor,
|
||||
cursor: 'pointer',
|
||||
...displayFlex('center', 'center'),
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '0',
|
||||
|
||||
// TODO: we need to add @emotion/babel-plugin
|
||||
'::after': {
|
||||
content: '""',
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
borderRadius: '6px',
|
||||
...absoluteCenter({ horizontal: true, vertical: true }),
|
||||
},
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
'::after': {
|
||||
background: theme.colors.hoverBackground,
|
||||
},
|
||||
},
|
||||
svg: {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalFooter = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontSize: '14px',
|
||||
|
||||
97
packages/app/src/components/delete-workspace/index.tsx
Normal file
97
packages/app/src/components/delete-workspace/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workSpaceName: string;
|
||||
}
|
||||
|
||||
export const DeleteModal = ({
|
||||
open,
|
||||
onClose,
|
||||
workSpaceName,
|
||||
}: LoginModalProps) => {
|
||||
const [canDelete, setCanDelete] = useState<boolean>(true);
|
||||
const InputChange = (value: string) => {
|
||||
if (value === workSpaceName) {
|
||||
setCanDelete(false);
|
||||
} else {
|
||||
setCanDelete(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={620} height={334}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Delete Workspace</ContentTitle>
|
||||
<div>
|
||||
This action cannot be undone. This will permanently delete{' '}
|
||||
{workSpaceName} workspace name along with all its content.
|
||||
</div>
|
||||
|
||||
<Input
|
||||
onChange={InputChange}
|
||||
placeholder="Please type “delete” to confirm"
|
||||
></Input>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Button
|
||||
style={{ marginRight: '12px' }}
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button shape="circle" type="danger" disabled={canDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
padding: '0 48px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
textAlign: 'center',
|
||||
});
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
} from './icons';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import useHistoryUpdated from '@/hooks/use-history-update';
|
||||
|
||||
const toolbarList1 = [
|
||||
{
|
||||
@@ -24,6 +26,15 @@ const toolbarList1 = [
|
||||
icon: <SelectIcon />,
|
||||
toolTip: 'Select',
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'default',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
flavor: 'text',
|
||||
@@ -34,8 +45,19 @@ const toolbarList1 = [
|
||||
{
|
||||
flavor: 'shape',
|
||||
icon: <ShapeIcon />,
|
||||
toolTip: 'Shape (coming soon)',
|
||||
disable: true,
|
||||
toolTip: 'Shape',
|
||||
disable: false,
|
||||
callback: () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine.switch-mouse-mode', {
|
||||
detail: {
|
||||
type: 'shape',
|
||||
color: 'black',
|
||||
shape: 'rectangle',
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
flavor: 'sticky',
|
||||
@@ -57,34 +79,19 @@ const toolbarList1 = [
|
||||
disable: true,
|
||||
},
|
||||
];
|
||||
const toolbarList2 = [
|
||||
{
|
||||
flavor: 'undo',
|
||||
icon: <UndoIcon />,
|
||||
toolTip: 'Undo',
|
||||
disable: false,
|
||||
},
|
||||
{
|
||||
flavor: 'redo',
|
||||
icon: <RedoIcon />,
|
||||
toolTip: 'Redo',
|
||||
disable: false,
|
||||
},
|
||||
];
|
||||
|
||||
const UndoRedo = () => {
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const { page } = editor;
|
||||
const { currentPage } = useAppState();
|
||||
const onHistoryUpdated = useHistoryUpdated();
|
||||
|
||||
page.signals.historyUpdated.on(() => {
|
||||
useEffect(() => {
|
||||
onHistoryUpdated(page => {
|
||||
setCanUndo(page.canUndo);
|
||||
setCanRedo(page.canRedo);
|
||||
});
|
||||
}, [editor]);
|
||||
}, [onHistoryUpdated]);
|
||||
|
||||
return (
|
||||
<StyledToolbarWrapper>
|
||||
@@ -92,7 +99,7 @@ const UndoRedo = () => {
|
||||
<StyledToolbarItem
|
||||
disable={!canUndo}
|
||||
onClick={() => {
|
||||
editor?.page?.undo();
|
||||
currentPage?.undo();
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
@@ -102,7 +109,7 @@ const UndoRedo = () => {
|
||||
<StyledToolbarItem
|
||||
disable={!canRedo}
|
||||
onClick={() => {
|
||||
editor?.page?.redo();
|
||||
currentPage?.redo();
|
||||
}}
|
||||
>
|
||||
<RedoIcon />
|
||||
@@ -113,7 +120,7 @@ const UndoRedo = () => {
|
||||
};
|
||||
|
||||
export const EdgelessToolbar = () => {
|
||||
const { mode } = useEditor();
|
||||
const { mode } = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<Slide
|
||||
@@ -122,22 +129,25 @@ export const EdgelessToolbar = () => {
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
>
|
||||
<StyledEdgelessToolbar>
|
||||
<StyledEdgelessToolbar aria-label="edgeless-toolbar">
|
||||
<StyledToolbarWrapper>
|
||||
{toolbarList1.map(({ icon, toolTip, flavor, disable }, index) => {
|
||||
return (
|
||||
<Tooltip key={index} content={toolTip} placement="right-start">
|
||||
<StyledToolbarItem
|
||||
disable={disable}
|
||||
onClick={() => {
|
||||
console.log('flavor', flavor);
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</StyledToolbarItem>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{toolbarList1.map(
|
||||
({ icon, toolTip, flavor, disable, callback }, index) => {
|
||||
return (
|
||||
<Tooltip key={index} content={toolTip} placement="right-start">
|
||||
<StyledToolbarItem
|
||||
disable={disable}
|
||||
onClick={() => {
|
||||
console.log('click toolbar button:', flavor);
|
||||
callback?.();
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</StyledToolbarItem>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</StyledToolbarWrapper>
|
||||
<UndoRedo />
|
||||
</StyledEdgelessToolbar>
|
||||
|
||||
@@ -2,12 +2,12 @@ import { styled, displayFlex } from '@/styles';
|
||||
|
||||
export const StyledEdgelessToolbar = styled.div(({ theme }) => ({
|
||||
height: '320px',
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
left: '12px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
zIndex: theme.zIndex.modal,
|
||||
zIndex: theme.zIndex.modal - 1,
|
||||
}));
|
||||
|
||||
export const StyledToolbarWrapper = styled.div(({ theme }) => ({
|
||||
|
||||
@@ -11,9 +11,10 @@ import type {
|
||||
AnimateRadioProps,
|
||||
AnimateRadioItemProps,
|
||||
} from './type';
|
||||
import { useTheme } from '@/styles';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
import { EdgelessIcon, PaperIcon } from './icons';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
|
||||
const PaperItem = ({ active }: { active?: boolean }) => {
|
||||
const {
|
||||
@@ -66,7 +67,9 @@ export const EditorModeSwitch = ({
|
||||
style = {},
|
||||
}: AnimateRadioProps) => {
|
||||
const { mode: themeMode } = useTheme();
|
||||
const { mode, setMode } = useEditor();
|
||||
const { changePageMode } = usePageHelper();
|
||||
const { trash, mode = 'page', id = '' } = useCurrentPageMeta() || {};
|
||||
|
||||
const modifyRadioItemStatus = (): RadioItemStatus => {
|
||||
return {
|
||||
left: isHover
|
||||
@@ -99,6 +102,7 @@ export const EditorModeSwitch = ({
|
||||
data-testid="editor-mode-switcher"
|
||||
shrink={!isHover}
|
||||
style={style}
|
||||
disabled={!!trash}
|
||||
>
|
||||
<AnimateRadioItem
|
||||
isLeft={true}
|
||||
@@ -107,7 +111,7 @@ export const EditorModeSwitch = ({
|
||||
active={mode === 'page'}
|
||||
status={radioItemStatus.left}
|
||||
onClick={() => {
|
||||
setMode('page');
|
||||
changePageMode(id, 'page');
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setRadioItemStatus({
|
||||
@@ -123,11 +127,12 @@ export const EditorModeSwitch = ({
|
||||
<AnimateRadioItem
|
||||
isLeft={false}
|
||||
label="Edgeless"
|
||||
data-testid="switch-edgeless-item"
|
||||
icon={<EdgelessItem />}
|
||||
active={mode === 'edgeless'}
|
||||
status={radioItemStatus.right}
|
||||
onClick={() => {
|
||||
setMode('edgeless');
|
||||
changePageMode(id, 'edgeless');
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setRadioItemStatus({
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
import { displayFlex, keyframes, styled } from '@/styles';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import spring, { toString } from 'css-spring';
|
||||
import type { ItemStatus } from './type';
|
||||
|
||||
const ANIMATE_DURATION = 500;
|
||||
|
||||
export const StyledAnimateRadioContainer = styled('div')<{ shrink: boolean }>(
|
||||
({ shrink, theme }) => {
|
||||
const animateScaleStretch = keyframes`${toString(
|
||||
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const animateScaleShrink = keyframes(
|
||||
`${toString(
|
||||
spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' })
|
||||
)}`
|
||||
);
|
||||
const shrinkStyle = shrink
|
||||
? {
|
||||
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
|
||||
background: 'transparent',
|
||||
}
|
||||
: {
|
||||
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
|
||||
};
|
||||
export const StyledAnimateRadioContainer = styled('div')<{
|
||||
shrink: boolean;
|
||||
disabled: boolean;
|
||||
}>(({ shrink, theme, disabled }) => {
|
||||
const animateScaleStretch = keyframes`${toString(
|
||||
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
|
||||
)}`;
|
||||
const animateScaleShrink = keyframes(
|
||||
`${toString(
|
||||
spring({ width: '160px' }, { width: '36px' }, { preset: 'gentle' })
|
||||
)}`
|
||||
);
|
||||
const shrinkStyle = shrink
|
||||
? {
|
||||
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
|
||||
background: 'transparent',
|
||||
}
|
||||
: {
|
||||
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
|
||||
};
|
||||
|
||||
return {
|
||||
height: '36px',
|
||||
borderRadius: '18px',
|
||||
background: theme.colors.hoverBackground,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`,
|
||||
border: '1px solid transparent',
|
||||
return {
|
||||
height: '36px',
|
||||
borderRadius: '18px',
|
||||
background: disabled ? 'transparent' : theme.colors.hoverBackground,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`,
|
||||
border: '1px solid transparent',
|
||||
|
||||
...shrinkStyle,
|
||||
':hover': {
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
...(disabled ? { pointerEvents: 'none' } : shrinkStyle),
|
||||
':hover': {
|
||||
border: disabled ? '' : `1px solid ${theme.colors.primaryColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMiddleLine = styled('div')<{
|
||||
hidden: boolean;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type EditorContextValue = {
|
||||
editor: EditorContainer | null;
|
||||
mode: EditorContainer['mode'];
|
||||
setEditor: (editor: EditorContainer) => void;
|
||||
setMode: (mode: EditorContainer['mode']) => void;
|
||||
};
|
||||
|
||||
type EditorContextProps = PropsWithChildren<{}>;
|
||||
|
||||
export const EditorContext = createContext<EditorContextValue>({
|
||||
editor: null,
|
||||
mode: 'page',
|
||||
setEditor: () => {},
|
||||
setMode: () => {},
|
||||
});
|
||||
|
||||
export const useEditor = () => useContext(EditorContext);
|
||||
|
||||
export const EditorProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<EditorContextProps>) => {
|
||||
const [editor, setEditor] = useState<EditorContainer | null>(null);
|
||||
const [mode, setMode] = useState<EditorContainer['mode']>('page');
|
||||
|
||||
useEffect(() => {
|
||||
const event = new CustomEvent('affine.switch-mode', { detail: mode });
|
||||
window.dispatchEvent(event);
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, setEditor, mode, setMode }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorProvider;
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
import '@blocksuite/blocks';
|
||||
import '@blocksuite/blocks/style';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { createEditor, BlockSchema } from '@blocksuite/editor';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { forwardRef, Suspense, useEffect, useRef } from 'react';
|
||||
import pkg from '../../package.json';
|
||||
import exampleMarkdown from './example-markdown';
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const BlockSuiteEditor = forwardRef<EditorContainer>(({}, ref) => {
|
||||
const containerElement = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (!containerElement.current) {
|
||||
return;
|
||||
}
|
||||
const workspace = new Workspace({});
|
||||
const page = workspace.createPage('page0').register(BlockSchema);
|
||||
const editor = createEditor(page);
|
||||
containerElement.current.appendChild(editor);
|
||||
if (ref) {
|
||||
if ('current' in ref) {
|
||||
ref.current = editor;
|
||||
} else {
|
||||
ref(editor);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
editor.remove();
|
||||
};
|
||||
}, [ref]);
|
||||
return <div id="editor" style={{ height: '100%' }} ref={containerElement} />;
|
||||
});
|
||||
|
||||
export const Editor = () => {
|
||||
const editorRef = useRef<EditorContainer>(null);
|
||||
const { setEditor } = useEditor();
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
setEditor(editorRef.current);
|
||||
const { page } = editorRef.current as EditorContainer;
|
||||
const pageId = page.addBlock({
|
||||
flavour: 'affine:page',
|
||||
title: 'Welcome to the AFFiNE Alpha',
|
||||
});
|
||||
const groupId = page.addBlock({ flavour: 'affine:group' }, pageId);
|
||||
editorRef.current.clipboard.importMarkdown(exampleMarkdown, `${groupId}`);
|
||||
page.resetHistory();
|
||||
}, [setEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
const version = pkg.dependencies['@blocksuite/editor'].substring(1);
|
||||
console.log(`BlockSuite live demo ${version}`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Error!</div>}>
|
||||
<BlockSuiteEditor ref={editorRef} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'editor-container': EditorContainer;
|
||||
}
|
||||
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
// TODO fix types on react
|
||||
'editor-container': EditorContainer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Editor;
|
||||
@@ -1,62 +0,0 @@
|
||||
const exampleMarkdown = `The AFFiNE Alpha is here! You can also view our [Official Website](https://affine.pro/)!
|
||||
|
||||
**What's different in AFFiNE Alpha?**
|
||||
|
||||
1. A much ~smoother editing~ experience, with much ~greater stability~;
|
||||
2. More complete ~Markdown~ support and improved ~keyboard shortcuts~;
|
||||
3. New features such as ~dark mode~;
|
||||
* **Switch between view styles using the** ☀ **and** 🌙.
|
||||
4. ~Clean and modern UI/UX~ design.
|
||||
|
||||
**Looking for Markdown syntax or keyboard shortcuts?**
|
||||
|
||||
* Find the (?) in the bottom right, then the ️⌨️, to view a full list of \`Keyboard Shortcuts\`
|
||||
|
||||
**Have an enjoyable editing experience !!!** 😃
|
||||
|
||||
Have some feedback or just want to get in touch? Use the (?), then 🎧 to get in touch and join our communities.
|
||||
|
||||
**Still in development**
|
||||
|
||||
There are also some things you should consider about this AFFiNE Alpha including some ~limitations~:
|
||||
|
||||
* Single page editing - currently editing multiple docs/pages is not supported;
|
||||
* Changes are not automatically stored, to save changes you should export your data. Options can be found by going to the top right and finding the \`⋮\` icon;
|
||||
* Without an import/open feature you are still able to access your data by copying it back in.
|
||||
|
||||
**Keyboard Shortcuts:**
|
||||
|
||||
1. Undo: \`⌘+Z\` or \`Ctrl+Z\`
|
||||
2. Redo: \`⌘+⇧+Z\` or \`Ctrl+Shift+Z\`
|
||||
3. Bold: \`⌘+B\` or \`Ctrl+B\`
|
||||
4. Italic: \`⌘+I\` or \`Ctrl+I\`
|
||||
5. Underline: \`⌘+U\` or \`Ctrl+U\`
|
||||
6. Strikethrough: \`⌘+⇧+S\` or \`Ctrl+Shift+S\`
|
||||
7. Inline code: \`⌘+E\` or \`Ctrl+E\`
|
||||
8. Link: \`⌘+K\` or \`Ctrl+K\`
|
||||
9. Body text: \`⌘+⌥+0\` or \`Ctrl+Shift+0\`
|
||||
10. Heading 1: \`⌘+⌥+1\` or \`Ctrl+Shift+1\`
|
||||
11. Heading 2: \`⌘+⌥+2\` or \`Ctrl+Shift+2\`
|
||||
12. Heading 3: \`⌘+⌥+3\` or \`Ctrl+Shift+3\`
|
||||
13. Heading 4: \`⌘+⌥+4\` or \`Ctrl+Shift+4\`
|
||||
14. Heading 5: \`⌘+⌥+5\` or \`Ctrl+Shift+5\`
|
||||
15. Heading 6: \`⌘+⌥+6\` or \`Ctrl+Shift+6\`
|
||||
16. Increase indent: \`Tab\`
|
||||
17. Reduce indent: \`⇧+Tab\` or \`Shift+Tab\`
|
||||
|
||||
**Markdown Syntax:**
|
||||
|
||||
1. Bold: \`**text**\`
|
||||
2. Italic: \`*text*\`
|
||||
3. Underline: \`~text~\`
|
||||
4. Strikethrough: \`~~text~~\`
|
||||
5. Inline code: \`\` \`text\` \`\`
|
||||
6. Heading 1: \`# text\`
|
||||
7. Heading 2: \`## text\`
|
||||
8. Heading 3: \`### text\`
|
||||
9. Heading 4: \`#### text\`
|
||||
10. Heading 5: \`##### text\`
|
||||
11. Heading 6: \`###### text\`
|
||||
`;
|
||||
|
||||
export default exampleMarkdown;
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
StyledFAQ,
|
||||
StyledIconWrapper,
|
||||
StyledFAQWrapper,
|
||||
StyledTransformIcon,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useEditor } from '@/components/editor-provider';
|
||||
import { useModal } from '@/components/global-modal-provider';
|
||||
import { useTheme } from '@/styles';
|
||||
|
||||
export const FAQ = () => {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const { mode } = useTheme();
|
||||
const { mode: editorMode } = useEditor();
|
||||
const { shortcutsModalHandler, contactModalHandler } = useModal();
|
||||
const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFAQ
|
||||
className=""
|
||||
onMouseEnter={() => {
|
||||
setShowContent(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<Grow in={showContent}>
|
||||
<StyledFAQWrapper>
|
||||
<Tooltip content="Contact Us" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
contactModalHandler(true);
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
<Tooltip content="Keyboard Shortcuts" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
shortcutsModalHandler(true);
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
</StyledFAQWrapper>
|
||||
</Grow>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper
|
||||
data-testid="faq-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
>
|
||||
<HelpIcon />
|
||||
</StyledIconWrapper>
|
||||
<StyledTransformIcon in={showContent}>
|
||||
<CloseIcon />
|
||||
</StyledTransformIcon>
|
||||
</div>
|
||||
</StyledFAQ>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const routesLIst: any = [
|
||||
{
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
element: <HelpIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
47
packages/app/src/components/file-upload/index.tsx
Normal file
47
packages/app/src/components/file-upload/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Button } from '@/ui/button';
|
||||
import { FC, useRef, ChangeEvent, ReactElement } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
interface Props {
|
||||
uploadType?: string;
|
||||
children?: ReactElement;
|
||||
accept?: string;
|
||||
fileChange: (file: File) => void;
|
||||
}
|
||||
export const Upload: FC<Props> = props => {
|
||||
const { fileChange, accept } = props;
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
const _chooseFile = () => {
|
||||
if (input_ref.current) {
|
||||
input_ref.current.click();
|
||||
}
|
||||
};
|
||||
const _handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
fileChange(file);
|
||||
if (input_ref.current) {
|
||||
input_ref.current.value = '';
|
||||
}
|
||||
};
|
||||
return (
|
||||
<UploadStyle onClick={_chooseFile}>
|
||||
{props.children ?? <Button>Upload</Button>}
|
||||
<input
|
||||
ref={input_ref}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
onChange={_handleInputChange}
|
||||
accept={accept}
|
||||
/>
|
||||
</UploadStyle>
|
||||
);
|
||||
};
|
||||
|
||||
const UploadStyle = styled('div')(() => {
|
||||
return {
|
||||
display: 'inline-block',
|
||||
};
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import ShortcutsModal from './shortcuts-modal';
|
||||
import ContactModal from '@/components/contact-modal';
|
||||
|
||||
type ModalContextValue = {
|
||||
shortcutsModalHandler: (visible: boolean) => void;
|
||||
contactModalHandler: (visible: boolean) => void;
|
||||
};
|
||||
type ModalContextProps = PropsWithChildren<{}>;
|
||||
|
||||
export const ModalContext = createContext<ModalContextValue>({
|
||||
shortcutsModalHandler: () => {},
|
||||
contactModalHandler: () => {},
|
||||
});
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
|
||||
export const ModalProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<ModalContextProps>) => {
|
||||
const [openContactModal, setOpenContactModal] = useState(false);
|
||||
const [openShortcutsModal, setOpenShortcutsModal] = useState(false);
|
||||
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
shortcutsModalHandler: visible => {
|
||||
setOpenShortcutsModal(visible);
|
||||
},
|
||||
contactModalHandler: visible => {
|
||||
setOpenContactModal(visible);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ContactModal
|
||||
open={openContactModal}
|
||||
onClose={() => {
|
||||
setOpenContactModal(false);
|
||||
}}
|
||||
></ContactModal>
|
||||
<ShortcutsModal
|
||||
open={openShortcutsModal}
|
||||
onClose={() => {
|
||||
setOpenShortcutsModal(false);
|
||||
}}
|
||||
></ShortcutsModal>
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalProvider;
|
||||
77
packages/app/src/components/header/editor-header.tsx
Normal file
77
packages/app/src/components/header/editor-header.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
StyledSearchArrowWrapper,
|
||||
StyledSwitchWrapper,
|
||||
StyledTitle,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
import { Content } from '@/ui/layout';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import EditorModeSwitch from '@/components/editor-mode-switch';
|
||||
import QuickSearchButton from './quick-search-button';
|
||||
import Header from './header';
|
||||
import usePropsUpdated from '@/hooks/use-props-updated';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
|
||||
export const EditorHeader = () => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const { editor } = useAppState();
|
||||
const { trash: isTrash = false } = useCurrentPageMeta() || {};
|
||||
const onPropsUpdated = usePropsUpdated();
|
||||
|
||||
useEffect(() => {
|
||||
onPropsUpdated(editor => {
|
||||
setTitle(editor.model?.title || 'Untitled');
|
||||
});
|
||||
}, [onPropsUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
// If first time in, need to wait for editor to be inserted into DOM
|
||||
setTitle(editor?.model?.title || 'Untitled');
|
||||
}, 300);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
rightItems={
|
||||
isTrash
|
||||
? ['trashButtonGroup']
|
||||
: ['syncUser', 'themeModeSwitch', 'editorOptionMenu']
|
||||
}
|
||||
>
|
||||
{title && (
|
||||
<StyledTitle
|
||||
onMouseEnter={() => {
|
||||
if (isTrash) return;
|
||||
|
||||
setIsHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isTrash) return;
|
||||
|
||||
setIsHover(false);
|
||||
}}
|
||||
>
|
||||
<StyledTitleWrapper>
|
||||
<StyledSwitchWrapper>
|
||||
<EditorModeSwitch
|
||||
isHover={isHover}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
/>
|
||||
</StyledSwitchWrapper>
|
||||
<Content ellipsis={true}>{title}</Content>
|
||||
<StyledSearchArrowWrapper>
|
||||
<QuickSearchButton />
|
||||
</StyledSearchArrowWrapper>
|
||||
</StyledTitleWrapper>
|
||||
</StyledTitle>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorHeader;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
ExportIcon,
|
||||
ExportToHtmlIcon,
|
||||
ExportToMarkdownIcon,
|
||||
FavouritedIcon,
|
||||
FavouritesIcon,
|
||||
MoreVerticalIcon,
|
||||
PaperIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
import { toast } from '@/ui/toast';
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor } = useAppState();
|
||||
const { toggleFavoritePage, toggleDeletePage } = usePageHelper();
|
||||
const { changePageMode } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
const {
|
||||
mode = 'page',
|
||||
id = '',
|
||||
favorite = false,
|
||||
title = '',
|
||||
} = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onClick={() => {
|
||||
toggleFavoritePage(id);
|
||||
toast(!favorite ? 'Removed to Favourites' : 'Added to Favourites');
|
||||
}}
|
||||
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
>
|
||||
{favorite ? 'Remove' : 'Add'} to favourites
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onClick={() => {
|
||||
changePageMode(id, mode === 'page' ? 'edgeless' : 'page');
|
||||
}}
|
||||
>
|
||||
Convert to {mode === 'page' ? 'Edgeless' : 'Page'}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
placement="left-start"
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportHtml();
|
||||
}}
|
||||
icon={<ExportToHtmlIcon />}
|
||||
>
|
||||
Export to HTML
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
editor && editor.contentParser.onExportMarkdown();
|
||||
}}
|
||||
icon={<ExportToMarkdownIcon />}
|
||||
>
|
||||
Export to Markdown
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<MenuItem icon={<ExportIcon />} isDir={true}>
|
||||
Export
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-delete"
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete page?',
|
||||
content: `${title || 'Untitled'} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && toggleDeletePage(id);
|
||||
toast('Moved to Trash');
|
||||
});
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorOptionMenu = () => {
|
||||
return (
|
||||
<Menu content={<PopoverContent />} placement="bottom-end">
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditorOptionMenu;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const SyncUser = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
const appState = useAppState();
|
||||
|
||||
return appState.user ? (
|
||||
<IconButton iconSize="middle" disabled>
|
||||
<CloudInsyncIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
iconSize="middle"
|
||||
data-testid="cloud-unsync-icon"
|
||||
onClick={triggerLoginModal}
|
||||
>
|
||||
<CloudUnsyncedIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncUser;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@/styles';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
import { MoonIcon, SunIcon } from './icons';
|
||||
import { StyledThemeModeSwitch, StyledSwitchItem } from './style';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { displayFlex, keyframes, styled } from '@/styles';
|
||||
import { CSSProperties } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import spring, { toString } from 'css-spring';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button } from '@/ui/button';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
|
||||
export const TrashButtonGroup = () => {
|
||||
const { permanentlyDeletePage } = usePageHelper();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { toggleDeletePage } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
const router = useRouter();
|
||||
const { id = '' } = useCurrentPageMeta() || {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
style={{ marginRight: '24px' }}
|
||||
onClick={() => {
|
||||
toggleDeletePage(id);
|
||||
}}
|
||||
>
|
||||
Restore it
|
||||
</Button>
|
||||
<Button
|
||||
bold={true}
|
||||
shape="round"
|
||||
type="danger"
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Permanently delete',
|
||||
content:
|
||||
"Once deleted, you can't undo this action. Do you confirm?",
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
if (confirm) {
|
||||
router.push(`/workspace/${currentWorkspaceId}/all`);
|
||||
permanentlyDeletePage(id);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete permanently
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrashButtonGroup;
|
||||
71
packages/app/src/components/header/header.tsx
Normal file
71
packages/app/src/components/header/header.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React, { PropsWithChildren, ReactNode, useState } from 'react';
|
||||
import {
|
||||
StyledHeader,
|
||||
StyledHeaderRightSide,
|
||||
StyledHeaderContainer,
|
||||
StyledBrowserWarning,
|
||||
StyledCloseButton,
|
||||
} from './styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import EditorOptionMenu from './header-right-items/editor-option-menu';
|
||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||
// import SyncUser from './header-right-items/sync-user';
|
||||
|
||||
const BrowserWarning = ({
|
||||
show,
|
||||
onClose,
|
||||
}: {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<StyledBrowserWarning show={show}>
|
||||
{getWarningMessage()}
|
||||
<StyledCloseButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</StyledCloseButton>
|
||||
</StyledBrowserWarning>
|
||||
);
|
||||
};
|
||||
|
||||
type HeaderRightItemNames =
|
||||
| 'editorOptionMenu'
|
||||
| 'trashButtonGroup'
|
||||
| 'themeModeSwitch'
|
||||
| 'syncUser';
|
||||
|
||||
const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
|
||||
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
||||
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
||||
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
||||
syncUser: null,
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
rightItems = ['syncUser'],
|
||||
children,
|
||||
}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => {
|
||||
const [showWarning, setShowWarning] = useState(shouldShowWarning());
|
||||
return (
|
||||
<StyledHeaderContainer hasWarning={showWarning}>
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
<StyledHeader hasWarning={showWarning} data-testid="editor-header-items">
|
||||
{children}
|
||||
<StyledHeaderRightSide>
|
||||
{rightItems.map(itemName => {
|
||||
return HeaderRightItems[itemName];
|
||||
})}
|
||||
</StyledHeaderRightSide>
|
||||
</StyledHeader>
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
3
packages/app/src/components/header/index.tsx
Normal file
3
packages/app/src/components/header/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './header';
|
||||
export * from './editor-header';
|
||||
export * from './page-list-header';
|
||||
21
packages/app/src/components/header/page-list-header.tsx
Normal file
21
packages/app/src/components/header/page-list-header.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import Header from './header';
|
||||
import { StyledPageListTittleWrapper } from './styles';
|
||||
import QuickSearchButton from './quick-search-button';
|
||||
|
||||
export type PageListHeaderProps = PropsWithChildren<{
|
||||
icon?: ReactNode;
|
||||
}>;
|
||||
export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
|
||||
return (
|
||||
<Header>
|
||||
<StyledPageListTittleWrapper>
|
||||
{icon}
|
||||
{children}
|
||||
<QuickSearchButton style={{ marginLeft: '5px' }} />
|
||||
</StyledPageListTittleWrapper>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageListHeader;
|
||||
29
packages/app/src/components/header/quick-search-button.tsx
Normal file
29
packages/app/src/components/header/quick-search-button.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconButton, IconButtonProps } from '@/ui/button';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { ArrowDownIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
export const QuickSearchButton = ({
|
||||
onClick,
|
||||
...props
|
||||
}: Omit<IconButtonProps, 'children'>) => {
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
|
||||
return (
|
||||
<Tooltip content="Switch to" placement="bottom">
|
||||
<IconButton
|
||||
data-testid="header-quickSearchButton"
|
||||
{...props}
|
||||
onClick={e => {
|
||||
onClick?.(e);
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearchButton;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
import { MenuItem } from '@/ui/menu';
|
||||
|
||||
export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning }) => {
|
||||
@@ -10,14 +9,14 @@ export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
|
||||
}
|
||||
);
|
||||
export const StyledHeader = styled.div<{ hasWarning: boolean }>(
|
||||
({ hasWarning, theme }) => {
|
||||
({ hasWarning }) => {
|
||||
return {
|
||||
height: '60px',
|
||||
width: '100vw',
|
||||
...displayFlex('space-between', 'center'),
|
||||
width: '100%',
|
||||
...displayFlex('flex-end', 'center'),
|
||||
background: 'var(--affine-page-background)',
|
||||
transition: 'background-color 0.5s',
|
||||
position: 'fixed',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: hasWarning ? '36px' : '0',
|
||||
padding: '0 22px',
|
||||
@@ -41,20 +40,10 @@ export const StyledTitle = styled('div')(({ theme }) => ({
|
||||
|
||||
export const StyledTitleWrapper = styled('div')({
|
||||
maxWidth: '720px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const StyledLogo = styled('div')(({ theme }) => ({
|
||||
color: theme.colors.primaryColor,
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
cursor: 'pointer',
|
||||
marginLeft: '-22px',
|
||||
...displayFlex('center', 'center'),
|
||||
}));
|
||||
});
|
||||
|
||||
export const StyledHeaderRightSide = styled('div')({
|
||||
height: '100%',
|
||||
@@ -62,54 +51,23 @@ export const StyledHeaderRightSide = styled('div')({
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const StyledMenuItemWrapper = styled.div(({ theme }) => {
|
||||
return {
|
||||
height: '32px',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
svg: {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
marginRight: '14px',
|
||||
},
|
||||
'svg:nth-child(2)': {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const IconButton = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
...displayFlex('center', 'center'),
|
||||
color: theme.colors.iconColor,
|
||||
borderRadius: '5px',
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
background: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledBrowserWarning = styled.div(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.warningBackground,
|
||||
color: theme.colors.warningColor,
|
||||
height: '36px',
|
||||
width: '100vw',
|
||||
fontSize: theme.font.sm,
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
top: '0',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
export const StyledBrowserWarning = styled.div<{ show: boolean }>(
|
||||
({ theme, show }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.warningBackground,
|
||||
color: theme.colors.warningColor,
|
||||
height: '36px',
|
||||
width: '100vw',
|
||||
fontSize: theme.font.sm,
|
||||
position: 'fixed',
|
||||
left: '0',
|
||||
top: '0',
|
||||
display: show ? 'flex' : 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledCloseButton = styled.div(({ theme }) => {
|
||||
return {
|
||||
@@ -130,3 +88,36 @@ export const StyledCloseButton = styled.div(({ theme }) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSwitchWrapper = styled.div(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '100%',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSearchArrowWrapper = styled.div(() => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: 'calc(100% + 4px)',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.sm,
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
86
packages/app/src/components/help-island/index.tsx
Normal file
86
packages/app/src/components/help-island/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
StyledIsland,
|
||||
StyledIconWrapper,
|
||||
StyledIslandWrapper,
|
||||
StyledTransformIcon,
|
||||
} from './style';
|
||||
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
|
||||
export type IslandItemNames = 'contact' | 'shortcuts';
|
||||
export const HelpIsland = ({
|
||||
showList = ['contact', 'shortcuts'],
|
||||
}: {
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const { mode } = useTheme();
|
||||
const { mode: editorMode } = useCurrentPageMeta() || {};
|
||||
const { triggerShortcutsModal, triggerContactModal } = useModal();
|
||||
const isEdgelessDark = mode === 'dark' && editorMode === 'edgeless';
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
className=""
|
||||
onMouseEnter={() => {
|
||||
setShowContent(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowContent(false);
|
||||
}}
|
||||
>
|
||||
<Grow in={showContent}>
|
||||
<StyledIslandWrapper>
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content="Contact Us" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerContactModal();
|
||||
}}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip content="Keyboard Shortcuts" placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
onClick={() => {
|
||||
setShowContent(false);
|
||||
triggerShortcutsModal();
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledIslandWrapper>
|
||||
</Grow>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<StyledIconWrapper
|
||||
isEdgelessDark={isEdgelessDark}
|
||||
data-testid="faq-icon"
|
||||
>
|
||||
<HelpIcon />
|
||||
</StyledIconWrapper>
|
||||
<StyledTransformIcon in={showContent}>
|
||||
<CloseIcon />
|
||||
</StyledTransformIcon>
|
||||
</div>
|
||||
</StyledIsland>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpIsland;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
|
||||
export const StyledFAQ = styled('div')(({ theme }) => {
|
||||
export const StyledIsland = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
@@ -12,24 +12,24 @@ export const StyledFAQ = styled('div')(({ theme }) => {
|
||||
zIndex: theme.zIndex.popover,
|
||||
};
|
||||
});
|
||||
export const StyledTransformIcon = styled.div<{ in: boolean }>(
|
||||
({ in: isIn, theme }) => ({
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
opacity: isIn ? 1 : 0,
|
||||
backgroundColor: isIn
|
||||
? theme.colors.hoverBackground
|
||||
: theme.colors.pageBackground,
|
||||
})
|
||||
);
|
||||
export const StyledTransformIcon = styled('div', {
|
||||
shouldForwardProp: prop => prop !== 'in',
|
||||
})<{ in: boolean }>(({ in: isIn, theme }) => ({
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
margin: 'auto',
|
||||
...displayFlex('center', 'center'),
|
||||
opacity: isIn ? 1 : 0,
|
||||
backgroundColor: isIn
|
||||
? theme.colors.hoverBackground
|
||||
: theme.colors.pageBackground,
|
||||
}));
|
||||
export const StyledIconWrapper = styled('div')<{ isEdgelessDark: boolean }>(
|
||||
({ theme, isEdgelessDark }) => {
|
||||
return {
|
||||
@@ -57,7 +57,7 @@ export const StyledIconWrapper = styled('div')<{ isEdgelessDark: boolean }>(
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledFAQWrapper = styled('div')(({ theme }) => {
|
||||
export const StyledIslandWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
65
packages/app/src/components/import/index.tsx
Normal file
65
packages/app/src/components/import/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { StyledButtonWrapper, StyledTitle } from './styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import { Wrapper, Content } from '@/ui/layout';
|
||||
import Loading from '@/components/loading';
|
||||
import { useEffect, useState } from 'react';
|
||||
type ImportModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
export const ImportModal = ({ open, onClose }: ImportModalProps) => {
|
||||
const [status, setStatus] = useState<'unImported' | 'importing'>('importing');
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'importing') {
|
||||
setTimeout(() => {
|
||||
setStatus('unImported');
|
||||
}, 1500);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={460} minHeight={240}>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledTitle>Import</StyledTitle>
|
||||
|
||||
{status === 'unImported' && (
|
||||
<StyledButtonWrapper>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStatus('importing');
|
||||
}}
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStatus('importing');
|
||||
}}
|
||||
>
|
||||
HTML
|
||||
</Button>
|
||||
</StyledButtonWrapper>
|
||||
)}
|
||||
|
||||
{status === 'importing' && (
|
||||
<Wrapper
|
||||
wrap={true}
|
||||
justifyContent="center"
|
||||
style={{ marginTop: 22, paddingBottom: '32px' }}
|
||||
>
|
||||
<Loading size={25}></Loading>
|
||||
<Content align="center" weight="500">
|
||||
OOOOPS! Sorry forgot to remind you that we are working on the
|
||||
import function
|
||||
</Content>
|
||||
</Wrapper>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportModal;
|
||||
25
packages/app/src/components/import/styles.ts
Normal file
25
packages/app/src/components/import/styles.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledTitle = styled.div(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.h6,
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
marginTop: '45px',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonWrapper = styled.div(() => {
|
||||
return {
|
||||
width: '280px',
|
||||
margin: '24px auto 0',
|
||||
button: {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
':not(:last-child)': {
|
||||
marginBottom: '16px',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
251
packages/app/src/components/invite-members/index.tsx
Normal file
251
packages/app/src/components/invite-members/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { EmailIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { useState } from 'react';
|
||||
import { inviteMember, getUserByEmail } from '@affine/data-services';
|
||||
import { Avatar } from '@mui/material';
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceId: string;
|
||||
onInviteSuccess: () => void;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const debounce = <T extends (...args: any) => any>(
|
||||
fn: T,
|
||||
time?: number,
|
||||
immediate?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): ((...args: any) => any) => {
|
||||
let timeoutId: null | number;
|
||||
let defaultImmediate = immediate || false;
|
||||
const delay = time || 300;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (...args: any) => {
|
||||
if (defaultImmediate) {
|
||||
fn.apply(this, args);
|
||||
defaultImmediate = false;
|
||||
return;
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
timeoutId = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/;
|
||||
export const InviteMembers = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaceId,
|
||||
onInviteSuccess,
|
||||
}: LoginModalProps) => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [showMember, setShowMember] = useState<boolean>(false);
|
||||
const [showTip, setShowTip] = useState<boolean>(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [userData, setUserData] = useState<any>({});
|
||||
const inputChange = (value: string) => {
|
||||
setEmail(value);
|
||||
setShowMember(true);
|
||||
if (gmailReg.test(value)) {
|
||||
setShowTip(false);
|
||||
debounce(
|
||||
() => {
|
||||
getUserByEmail({
|
||||
email: value,
|
||||
workspace_id: workspaceId,
|
||||
}).then(data => {
|
||||
if (data?.name) {
|
||||
setUserData(data);
|
||||
setShowTip(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
300,
|
||||
true
|
||||
)();
|
||||
} else {
|
||||
setShowTip(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={460} height={236}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Invite members</ContentTitle>
|
||||
<InviteBox>
|
||||
<Input
|
||||
width={360}
|
||||
value={email}
|
||||
onChange={inputChange}
|
||||
onBlur={() => {
|
||||
setShowMember(false);
|
||||
}}
|
||||
placeholder="Search mail (Gmail support only)"
|
||||
></Input>
|
||||
{showMember ? (
|
||||
<Members>
|
||||
{showTip ? (
|
||||
<NoFind>Non-Gmail is not supported</NoFind>
|
||||
) : (
|
||||
<Member>
|
||||
{userData?.avatar_url ? (
|
||||
<Avatar src={userData?.avatar_url}></Avatar>
|
||||
) : (
|
||||
<MemberIcon>
|
||||
<EmailIcon></EmailIcon>
|
||||
</MemberIcon>
|
||||
)}
|
||||
<Email>{email}</Email>
|
||||
{/* <div>invited</div> */}
|
||||
</Member>
|
||||
)}
|
||||
</Members>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</InviteBox>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Button
|
||||
shape="circle"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
inviteMember({ id: workspaceId, email: email })
|
||||
.then(() => {
|
||||
onClose();
|
||||
onInviteSuccess && onInviteSuccess();
|
||||
})
|
||||
.catch(err => {
|
||||
// toast('Invite failed');
|
||||
console.log(err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
padding: '0 48px',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const InviteBox = styled('div')({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
const Members = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
background: theme.colors.pageBackground,
|
||||
textAlign: 'left',
|
||||
zIndex: 1,
|
||||
borderRadius: '0px 10px 10px 10px',
|
||||
height: '56px',
|
||||
padding: '8px 12px',
|
||||
input: {
|
||||
'&::placeholder': {
|
||||
color: theme.colors.placeHolderColor,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const NoFind = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
width: '100%',
|
||||
};
|
||||
});
|
||||
|
||||
const Member = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '40px',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
};
|
||||
});
|
||||
|
||||
const MemberIcon = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
color: theme.colors.primaryColor,
|
||||
background: '#F5F5F5',
|
||||
marginRight: '8px',
|
||||
textAlign: 'center',
|
||||
lineHeight: '45px',
|
||||
// icon size
|
||||
fontSize: '20px',
|
||||
overflow: 'hidden',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(({ theme }) => {
|
||||
return {
|
||||
flex: '1',
|
||||
color: theme.colors.popoverColor,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
@@ -1,14 +1,3 @@
|
||||
import { StyledLoading, StyledLoadingItem } from './styled';
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<StyledLoading>
|
||||
<StyledLoadingItem />
|
||||
<StyledLoadingItem />
|
||||
<StyledLoadingItem />
|
||||
<StyledLoadingItem />
|
||||
</StyledLoading>
|
||||
);
|
||||
};
|
||||
|
||||
import Loading from './loading';
|
||||
export * from './page-loading';
|
||||
export default Loading;
|
||||
|
||||
20
packages/app/src/components/loading/loading.tsx
Normal file
20
packages/app/src/components/loading/loading.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
StyledLoadingWrapper,
|
||||
StyledLoading,
|
||||
StyledLoadingItem,
|
||||
} from './styled';
|
||||
|
||||
export const Loading = ({ size = 40 }: { size?: number }) => {
|
||||
return (
|
||||
<StyledLoadingWrapper size={size}>
|
||||
<StyledLoading>
|
||||
<StyledLoadingItem size={size} />
|
||||
<StyledLoadingItem size={size} />
|
||||
<StyledLoadingItem size={size} />
|
||||
<StyledLoadingItem size={size} />
|
||||
</StyledLoading>
|
||||
</StyledLoadingWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
31
packages/app/src/components/loading/page-loading.tsx
Normal file
31
packages/app/src/components/loading/page-loading.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { styled } from '@/styles';
|
||||
import Loading from './loading';
|
||||
|
||||
// Used for the full page loading
|
||||
const StyledLoadingContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: '#6880FF',
|
||||
h1: {
|
||||
fontSize: '2em',
|
||||
marginTop: '15px',
|
||||
fontWeight: '600',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const PageLoading = ({ text = 'Loading...' }: { text?: string }) => {
|
||||
return (
|
||||
<StyledLoadingContainer>
|
||||
<div className="wrapper">
|
||||
<Loading />
|
||||
<h1>{text}</h1>
|
||||
</div>
|
||||
</StyledLoadingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLoading;
|
||||
@@ -1,14 +1,19 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
// Inspired by https://codepen.io/graphilla/pen/rNvBMYY
|
||||
const loadingItemSize = '40px';
|
||||
export const StyledLoadingWrapper = styled.div<{ size?: number }>(
|
||||
({ size = 40 }) => {
|
||||
return {
|
||||
width: size * 4,
|
||||
height: size * 4,
|
||||
position: 'relative',
|
||||
};
|
||||
}
|
||||
);
|
||||
export const StyledLoading = styled.div`
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: rotateX(55deg) rotateZ(-45deg)
|
||||
translate(calc(${loadingItemSize} * -2), 0);
|
||||
margin-bottom: calc(3 * ${loadingItemSize});
|
||||
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
top: 25%;
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translate(var(--sx), var(--sy));
|
||||
@@ -21,24 +26,14 @@ export const StyledLoading = styled.div`
|
||||
transform: translate(var(--ex), var(--ey));
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
20% {
|
||||
content: '.';
|
||||
}
|
||||
40% {
|
||||
content: '..';
|
||||
}
|
||||
80%,
|
||||
100% {
|
||||
content: '...';
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledLoadingItem = styled.div`
|
||||
export const StyledLoadingItem = styled.div<{ size: number }>(
|
||||
({ size = 40 }) => {
|
||||
return `
|
||||
position: absolute;
|
||||
width: ${loadingItemSize};
|
||||
height: ${loadingItemSize};
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
background: #9dacf9;
|
||||
animation: slide 0.9s cubic-bezier(0.65, 0.53, 0.59, 0.93) infinite;
|
||||
|
||||
@@ -92,3 +87,5 @@ export const StyledLoadingItem = styled.div`
|
||||
--ey: -50%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
118
packages/app/src/components/login-modal/LoginOptionButton.tsx
Normal file
118
packages/app/src/components/login-modal/LoginOptionButton.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { signInWithGoogle } from '@affine/data-services';
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { GoogleIcon, StayLogOutIcon } from './icons';
|
||||
|
||||
export const GoogleLoginButton = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
return (
|
||||
<StyledGoogleButton
|
||||
onClick={() => {
|
||||
signInWithGoogle()
|
||||
.then(() => {
|
||||
triggerLoginModal();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('sign google error', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ButtonWrapper>
|
||||
<IconWrapper>
|
||||
<GoogleIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Continue with Google</Title>
|
||||
<Description>Set up an AFFiNE account to sync data</Description>
|
||||
</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledGoogleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const StayLogOutButton = () => {
|
||||
return (
|
||||
<StyledStayLogOutButton>
|
||||
<ButtonWrapper>
|
||||
<IconWrapper>
|
||||
<StayLogOutIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Stay logged out</Title>
|
||||
<Description>All changes are saved locally</Description>
|
||||
</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledStayLogOutButton>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledGoogleButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
background: '#6880FF',
|
||||
color: '#fff',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
background: '#516BF4',
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const StyledStayLogOutButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
borderColor: '#6880FF',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ButtonWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
});
|
||||
|
||||
const IconWrapper = styled('div')({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
flex: '0 48px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
const TextWrapper = styled('div')({
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
});
|
||||
|
||||
const Title = styled('h1')(() => {
|
||||
return {
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
fontWeight: 500,
|
||||
};
|
||||
});
|
||||
|
||||
const Description = styled('p')(() => {
|
||||
return {
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
};
|
||||
});
|
||||
6
packages/app/src/components/login-modal/google.svg
Normal file
6
packages/app/src/components/login-modal/google.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#FFC107"/>
|
||||
<path d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z" fill="#FF3D00"/>
|
||||
<path d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z" fill="#4CAF50"/>
|
||||
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#1976D2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
42
packages/app/src/components/login-modal/icons.tsx
Normal file
42
packages/app/src/components/login-modal/icons.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { CloudUnsyncedIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import GoogleSvg from './google.svg';
|
||||
|
||||
export const GoogleIcon = () => {
|
||||
return (
|
||||
<GoogleIconWrapper>
|
||||
<picture>
|
||||
<img src={GoogleSvg.src} alt="Google" />
|
||||
</picture>
|
||||
</GoogleIconWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleIconWrapper = styled('div')(({ theme }) => ({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: theme.colors.pageBackground,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
export const StayLogOutIcon = () => {
|
||||
return (
|
||||
<StayLogOutWrapper>
|
||||
<CloudUnsyncedIcon />
|
||||
</StayLogOutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const StayLogOutWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
background: theme.colors.hoverBackground,
|
||||
};
|
||||
});
|
||||
68
packages/app/src/components/login-modal/index.tsx
Normal file
68
packages/app/src/components/login-modal/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { TextButton } from '@/ui/button';
|
||||
import { GoogleLoginButton, StayLogOutButton } from './LoginOptionButton';
|
||||
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="login-modal">
|
||||
<ModalWrapper width={620} height={334}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Currently not logged in</ContentTitle>
|
||||
<GoogleLoginButton />
|
||||
<StayLogOutButton />
|
||||
</Content>
|
||||
<Footer>
|
||||
<TextButton icon={<StyledResetIcon />}>Clear local data</TextButton>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
});
|
||||
|
||||
const StyledResetIcon = styled(ResetIcon)({
|
||||
marginRight: '12px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
});
|
||||
@@ -1,14 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import Modal from '@/ui/modal';
|
||||
import Modal, { ModalCloseButton, ModalWrapper } from '@/ui/modal';
|
||||
import getIsMobile from '@/utils/get-is-mobile';
|
||||
import {
|
||||
ModalWrapper,
|
||||
StyledButton,
|
||||
StyledCloseButton,
|
||||
StyledContent,
|
||||
StyledTitle,
|
||||
} from './styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { StyledButton, StyledContent, StyledTitle } from './styles';
|
||||
import bg from './bg.png';
|
||||
export const MobileModal = () => {
|
||||
const [showModal, setShowModal] = useState(getIsMobile());
|
||||
return (
|
||||
@@ -18,14 +12,18 @@ export const MobileModal = () => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
<ModalWrapper>
|
||||
<StyledCloseButton
|
||||
<ModalWrapper
|
||||
width={348}
|
||||
height={388}
|
||||
style={{ backgroundImage: `url(${bg.src})` }}
|
||||
>
|
||||
<ModalCloseButton
|
||||
size={[30, 30]}
|
||||
iconSize={[20, 20]}
|
||||
onClick={() => {
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</StyledCloseButton>
|
||||
/>
|
||||
|
||||
<StyledTitle>Ooops!</StyledTitle>
|
||||
<StyledContent>
|
||||
|
||||
@@ -1,38 +1,6 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
import bg from './bg.png';
|
||||
|
||||
export const ModalWrapper = styled.div(({ theme }) => {
|
||||
return {
|
||||
width: '348px',
|
||||
height: '388px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '28px',
|
||||
position: 'relative',
|
||||
backgroundImage: `url(${bg.src})`,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCloseButton = styled.div(({ theme }) => {
|
||||
return {
|
||||
width: '66px',
|
||||
height: '66px',
|
||||
color: theme.colors.iconColor,
|
||||
cursor: 'pointer',
|
||||
...displayFlex('center', 'center'),
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '0',
|
||||
|
||||
svg: {
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTitle = styled.div(({ theme }) => {
|
||||
export const StyledTitle = styled.div(() => {
|
||||
return {
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: '20px',
|
||||
@@ -42,7 +10,7 @@ export const StyledTitle = styled.div(({ theme }) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledContent = styled.div(({ theme }) => {
|
||||
export const StyledContent = styled.div(() => {
|
||||
return {
|
||||
padding: '0 40px',
|
||||
marginTop: '32px',
|
||||
|
||||
27
packages/app/src/components/page-list/date-cell.tsx
Normal file
27
packages/app/src/components/page-list/date-cell.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import dayjs from 'dayjs';
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { TableCell } from '@/ui/table';
|
||||
import React from 'react';
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
export const DateCell = ({
|
||||
pageMeta,
|
||||
dateKey,
|
||||
backupKey = '',
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
dateKey: keyof PageMeta;
|
||||
backupKey?: keyof PageMeta;
|
||||
}) => {
|
||||
// dayjs().format('L LT');
|
||||
const value = pageMeta[dateKey] ?? pageMeta[backupKey];
|
||||
return (
|
||||
<TableCell ellipsis={true}>
|
||||
{value ? dayjs(value as string).format('YYYY-MM-DD HH:mm') : '--'}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateCell;
|
||||
17
packages/app/src/components/page-list/empty.tsx
Normal file
17
packages/app/src/components/page-list/empty.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Empty } from '@/ui/empty';
|
||||
export const PageListEmpty = () => {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Empty
|
||||
width={800}
|
||||
height={300}
|
||||
sx={{ marginTop: '100px', marginBottom: '30px' }}
|
||||
/>
|
||||
<p>Tips: Click Add to Favourites/Trash and the page will appear here.</p>
|
||||
<p>(Designer is grappling with designing)</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageListEmpty;
|
||||
145
packages/app/src/components/page-list/index.tsx
Normal file
145
packages/app/src/components/page-list/index.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import {
|
||||
FavouritedIcon,
|
||||
FavouritesIcon,
|
||||
PaperIcon,
|
||||
EdgelessIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import {
|
||||
StyledTableContainer,
|
||||
StyledTableRow,
|
||||
StyledTitleLink,
|
||||
StyledTitleWrapper,
|
||||
} from './styles';
|
||||
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/ui/table';
|
||||
import { OperationCell, TrashOperationCell } from './operation-cell';
|
||||
import Empty from './empty';
|
||||
import { Content } from '@/ui/layout';
|
||||
import React from 'react';
|
||||
import DateCell from '@/components/page-list/date-cell';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { Tooltip } from '@/ui/tooltip';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
import { useTheme } from '@/providers/themeProvider';
|
||||
|
||||
const FavoriteTag = ({
|
||||
pageMeta: { favorite, id },
|
||||
}: {
|
||||
pageMeta: PageMeta;
|
||||
}) => {
|
||||
const { toggleFavoritePage } = usePageHelper();
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Tooltip
|
||||
content={favorite ? 'Favourited' : 'Favourite'}
|
||||
placement="top-start"
|
||||
>
|
||||
<IconButton
|
||||
darker={true}
|
||||
iconSize={[20, 20]}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
toggleFavoritePage(id);
|
||||
toast(!favorite ? 'Removed to Favourites' : 'Added to Favourites');
|
||||
}}
|
||||
style={{
|
||||
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
|
||||
}}
|
||||
className="favorite-button"
|
||||
>
|
||||
{favorite ? (
|
||||
<FavouritedIcon data-testid="favourited-icon" />
|
||||
) : (
|
||||
<FavouritesIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageList = ({
|
||||
pageList,
|
||||
showFavoriteTag = false,
|
||||
isTrash = false,
|
||||
}: {
|
||||
pageList: PageMeta[];
|
||||
showFavoriteTag?: boolean;
|
||||
isTrash?: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
if (pageList.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell proportion={0.5}>Title</TableCell>
|
||||
<TableCell proportion={0.2}>Created</TableCell>
|
||||
<TableCell proportion={0.2}>
|
||||
{isTrash ? 'Moved to Trash' : 'Updated'}
|
||||
</TableCell>
|
||||
<TableCell proportion={0.1}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{pageList.map((pageMeta, index) => {
|
||||
return (
|
||||
<StyledTableRow
|
||||
key={`${pageMeta.id}-${index}`}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/workspace/${currentWorkspaceId}/${pageMeta.id}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<StyledTitleWrapper>
|
||||
<StyledTitleLink>
|
||||
{pageMeta.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PaperIcon />
|
||||
)}
|
||||
<Content ellipsis={true} color="inherit">
|
||||
{pageMeta.title || 'Untitled'}
|
||||
</Content>
|
||||
</StyledTitleLink>
|
||||
{showFavoriteTag && <FavoriteTag pageMeta={pageMeta} />}
|
||||
</StyledTitleWrapper>
|
||||
</TableCell>
|
||||
<DateCell pageMeta={pageMeta} dateKey="createDate" />
|
||||
<DateCell
|
||||
pageMeta={pageMeta}
|
||||
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
|
||||
backupKey={isTrash ? 'trashDate' : 'createDate'}
|
||||
/>
|
||||
<TableCell
|
||||
style={{ padding: 0 }}
|
||||
data-testid={`more-actions-${pageMeta.id}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{isTrash ? (
|
||||
<TrashOperationCell pageMeta={pageMeta} />
|
||||
) : (
|
||||
<OperationCell pageMeta={pageMeta} />
|
||||
)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageList;
|
||||
109
packages/app/src/components/page-list/operation-cell.tsx
Normal file
109
packages/app/src/components/page-list/operation-cell.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { Wrapper } from '@/ui/layout';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import {
|
||||
MoreVerticalIcon,
|
||||
RestoreIcon,
|
||||
DeleteIcon,
|
||||
FavouritesIcon,
|
||||
FavouritedIcon,
|
||||
OpenInNewIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
|
||||
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
const { id, favorite } = pageMeta;
|
||||
const { openPage } = usePageHelper();
|
||||
const { toggleFavoritePage, toggleDeletePage } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const OperationMenu = (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
toggleFavoritePage(id);
|
||||
toast(!favorite ? 'Removed to Favourites' : 'Added to Favourites');
|
||||
}}
|
||||
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
|
||||
>
|
||||
{favorite ? 'Remove' : 'Add'} to favourites
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
openPage(id, {}, true);
|
||||
}}
|
||||
icon={<OpenInNewIcon />}
|
||||
>
|
||||
Open in new tab
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete page?',
|
||||
content: `${pageMeta.title || 'Untitled'} will be moved to Trash`,
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && toggleDeletePage(id);
|
||||
toast('Moved to Trash');
|
||||
});
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Wrapper alignItems="center" justifyContent="center">
|
||||
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
|
||||
<IconButton darker={true}>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
|
||||
const { id } = pageMeta;
|
||||
const { openPage, getPageMeta } = usePageHelper();
|
||||
const { toggleDeletePage, permanentlyDeletePage } = usePageHelper();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<IconButton
|
||||
darker={true}
|
||||
style={{ marginRight: '12px' }}
|
||||
onClick={() => {
|
||||
toggleDeletePage(id);
|
||||
toast(`${getPageMeta(id)?.title || 'Untitled'} restored`);
|
||||
openPage(id);
|
||||
}}
|
||||
>
|
||||
<RestoreIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
darker={true}
|
||||
onClick={() => {
|
||||
confirm({
|
||||
title: 'Delete permanently?',
|
||||
content: "Once deleted, you can't undo this action.",
|
||||
confirmText: 'Delete',
|
||||
confirmType: 'danger',
|
||||
}).then(confirm => {
|
||||
confirm && permanentlyDeletePage(id);
|
||||
toast('Permanently deleted');
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
51
packages/app/src/components/page-list/styles.ts
Normal file
51
packages/app/src/components/page-list/styles.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
import { TableRow } from '@/ui/table';
|
||||
|
||||
export const StyledTableContainer = styled.div(() => {
|
||||
return {
|
||||
height: 'calc(100vh - 60px)',
|
||||
padding: '78px 72px',
|
||||
overflowY: 'auto',
|
||||
};
|
||||
});
|
||||
export const StyledTitleWrapper = styled.div(({ theme }) => {
|
||||
return {
|
||||
...displayFlex('flex-start', 'center'),
|
||||
a: {
|
||||
color: 'inherit',
|
||||
},
|
||||
'a:visited': {
|
||||
color: 'unset',
|
||||
},
|
||||
'a:hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledTitleLink = styled.div(({ theme }) => {
|
||||
return {
|
||||
maxWidth: '80%',
|
||||
marginRight: '18px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
color: theme.colors.textColor,
|
||||
'>svg': {
|
||||
fontSize: '24px',
|
||||
marginRight: '12px',
|
||||
color: theme.colors.iconColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTableRow = styled(TableRow)(() => {
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
'.favorite-button': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.favorite-button': {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
18
packages/app/src/components/provider-composer.ts
Normal file
18
packages/app/src/components/provider-composer.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { cloneElement, FC, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
export const ProviderComposer: FC<
|
||||
PropsWithChildren<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
contexts: any;
|
||||
}>
|
||||
> = ({ contexts, children }) =>
|
||||
contexts.reduceRight(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(kids: ReactNode, parent: any) =>
|
||||
cloneElement(parent, {
|
||||
children: kids,
|
||||
}),
|
||||
children
|
||||
);
|
||||
|
||||
export default ProviderComposer;
|
||||
24
packages/app/src/components/quick-search/config.ts
Normal file
24
packages/app/src/components/quick-search/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons';
|
||||
|
||||
export const config = (currentWorkspaceId: string) => {
|
||||
const List = [
|
||||
{
|
||||
title: 'All pages',
|
||||
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
|
||||
icon: AllPagesIcon,
|
||||
},
|
||||
{
|
||||
title: 'Favourites',
|
||||
href: currentWorkspaceId
|
||||
? `/workspace/${currentWorkspaceId}/favorite`
|
||||
: '',
|
||||
icon: FavouritesIcon,
|
||||
},
|
||||
{
|
||||
title: 'Trash',
|
||||
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
|
||||
icon: TrashIcon,
|
||||
},
|
||||
];
|
||||
return List;
|
||||
};
|
||||
35
packages/app/src/components/quick-search/footer.tsx
Normal file
35
packages/app/src/components/quick-search/footer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@blocksuite/icons';
|
||||
import { StyledModalFooterContent } from './style';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { Command } from 'cmdk';
|
||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||
|
||||
export const Footer = (props: { query: string }) => {
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const { openPage, createPage } = usePageHelper();
|
||||
const query = props.query;
|
||||
|
||||
return (
|
||||
<Command.Item
|
||||
data-testid="quickSearch-addNewPage"
|
||||
onSelect={async () => {
|
||||
const pageId = await createPage({ title: query });
|
||||
if (pageId) {
|
||||
openPage(pageId);
|
||||
}
|
||||
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<StyledModalFooterContent>
|
||||
<AddIcon />
|
||||
{query ? (
|
||||
<span>New "{query}" page</span>
|
||||
) : (
|
||||
<span>New page</span>
|
||||
)}
|
||||
</StyledModalFooterContent>
|
||||
</Command.Item>
|
||||
);
|
||||
};
|
||||
118
packages/app/src/components/quick-search/index.tsx
Normal file
118
packages/app/src/components/quick-search/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Modal, ModalWrapper } from '@/ui/modal';
|
||||
import {
|
||||
StyledContent,
|
||||
StyledModalHeader,
|
||||
StyledModalFooter,
|
||||
StyledModalDivider,
|
||||
StyledShortcut,
|
||||
} from './style';
|
||||
import { Input } from './input';
|
||||
import { Results } from './results';
|
||||
import { Footer } from './footer';
|
||||
import { Command } from 'cmdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { getUaHelper } from '@/utils';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
type TransitionsModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
const isMac = () => {
|
||||
return getUaHelper().isMacOs;
|
||||
};
|
||||
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreatePage, setShowCreatePage] = useState(true);
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const { currentWorkspaceId, workspacesMeta } = useAppState();
|
||||
|
||||
const currentWorkspace = workspacesMeta.find(
|
||||
meta => String(meta.id) === String(currentWorkspaceId)
|
||||
);
|
||||
const isPublic = currentWorkspace?.public;
|
||||
|
||||
// Add ‘⌘+K’ shortcut keys as switches
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
|
||||
const selection = window.getSelection();
|
||||
if (selection?.toString()) {
|
||||
triggerQuickSearchModal(false);
|
||||
return;
|
||||
}
|
||||
if (selection?.isCollapsed) {
|
||||
triggerQuickSearchModal(!open);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!open) {
|
||||
setQuery('');
|
||||
}
|
||||
document.addEventListener('keydown', down, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', down, { capture: true });
|
||||
}, [open, triggerQuickSearchModal]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
wrapperPosition={['top', 'center']}
|
||||
data-testid="quickSearch"
|
||||
>
|
||||
<ModalWrapper
|
||||
width={620}
|
||||
style={{
|
||||
maxHeight: '80vh',
|
||||
minHeight: '350px',
|
||||
top: '12vh',
|
||||
}}
|
||||
>
|
||||
<Command
|
||||
shouldFilter={false}
|
||||
//Handle KeyboardEvent conflicts with blocksuite
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowRight'
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledModalHeader>
|
||||
<Input query={query} setQuery={setQuery} setLoading={setLoading} />
|
||||
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>
|
||||
</StyledModalHeader>
|
||||
<StyledModalDivider />
|
||||
<Command.List>
|
||||
<StyledContent>
|
||||
<Results
|
||||
query={query}
|
||||
loading={loading}
|
||||
setLoading={setLoading}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
</StyledContent>
|
||||
{isPublic ? (
|
||||
<></>
|
||||
) : showCreatePage ? (
|
||||
<>
|
||||
<StyledModalDivider />
|
||||
<StyledModalFooter>
|
||||
<Footer query={query} />
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
) : null}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickSearch;
|
||||
90
packages/app/src/components/quick-search/input.tsx
Normal file
90
packages/app/src/components/quick-search/input.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { StyledInputContent, StyledLabel } from './style';
|
||||
import { Command } from 'cmdk';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
export const Input = (props: {
|
||||
query: string;
|
||||
setQuery: Dispatch<SetStateAction<string>>;
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const [isComposition, setIsComposition] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { currentWorkspaceId, workspacesMeta, currentWorkspace } =
|
||||
useAppState();
|
||||
const isPublic = workspacesMeta.find(
|
||||
meta => String(meta.id) === String(currentWorkspaceId)
|
||||
)?.public;
|
||||
useEffect(() => {
|
||||
inputRef.current?.addEventListener(
|
||||
'blur',
|
||||
() => {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
true
|
||||
);
|
||||
return inputRef.current?.focus();
|
||||
}, [inputRef]);
|
||||
useEffect(() => {
|
||||
return setInputValue(props.query);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return (
|
||||
<StyledInputContent>
|
||||
<StyledLabel htmlFor=":r5:">
|
||||
<SearchIcon />
|
||||
</StyledLabel>
|
||||
<Command.Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onCompositionStart={() => {
|
||||
setIsComposition(true);
|
||||
}}
|
||||
onCompositionEnd={e => {
|
||||
props.setQuery(e.data);
|
||||
setIsComposition(false);
|
||||
if (!props.query) {
|
||||
props.setLoading(true);
|
||||
}
|
||||
}}
|
||||
onValueChange={str => {
|
||||
setInputValue(str);
|
||||
if (!isComposition) {
|
||||
props.setQuery(str);
|
||||
if (!props.query) {
|
||||
props.setLoading(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'a' && e.metaKey) {
|
||||
e.stopPropagation();
|
||||
inputRef.current?.select();
|
||||
return;
|
||||
}
|
||||
if (isComposition) {
|
||||
if (
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowUp' ||
|
||||
e.key === 'Enter'
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
isPublic
|
||||
? `Search in ${currentWorkspace?.meta.name}`
|
||||
: 'Quick Search...'
|
||||
}
|
||||
/>
|
||||
</StyledInputContent>
|
||||
);
|
||||
};
|
||||
95
packages/app/src/components/quick-search/noResultSVG.tsx
Normal file
95
packages/app/src/components/quick-search/noResultSVG.tsx
Normal file
File diff suppressed because one or more lines are too long
102
packages/app/src/components/quick-search/results.tsx
Normal file
102
packages/app/src/components/quick-search/results.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Command } from 'cmdk';
|
||||
import { StyledListItem, StyledNotFound } from './style';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { useRouter } from 'next/router';
|
||||
import { config } from './config';
|
||||
import { NoResultSVG } from './noResultSVG';
|
||||
import usePageHelper from '@/hooks/use-page-helper';
|
||||
import usePageMetaList from '@/hooks/use-page-meta-list';
|
||||
export const Results = (props: {
|
||||
query: string;
|
||||
loading: boolean;
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const query = props.query;
|
||||
const loading = props.loading;
|
||||
const setLoading = props.setLoading;
|
||||
const setShowCreatePage = props.setShowCreatePage;
|
||||
const { triggerQuickSearchModal } = useModal();
|
||||
const pageMetaList = usePageMetaList();
|
||||
const { openPage } = usePageHelper();
|
||||
const router = useRouter();
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
const { search } = usePageHelper();
|
||||
const List = config(currentWorkspaceId);
|
||||
const [results, setResults] = useState(new Map<string, string | undefined>());
|
||||
useEffect(() => {
|
||||
setResults(search(query));
|
||||
setLoading(false);
|
||||
//Save the Map<BlockId, PageId> obtained from the search as state
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query, setResults, setLoading]);
|
||||
const pageIds = [...results.values()];
|
||||
|
||||
const resultsPageMeta = pageMetaList.filter(
|
||||
page => pageIds.indexOf(page.id) > -1 && !page.trash
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCreatePage(resultsPageMeta.length ? false : true);
|
||||
//Determine whether to display the ‘+ New page’
|
||||
}, [resultsPageMeta, setShowCreatePage]);
|
||||
return loading ? null : (
|
||||
<>
|
||||
{query ? (
|
||||
resultsPageMeta.length ? (
|
||||
<Command.Group heading={`Find ${resultsPageMeta.length} results`}>
|
||||
{resultsPageMeta.map(result => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={result.id}
|
||||
onSelect={() => {
|
||||
openPage(result.id);
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
value={result.id}
|
||||
>
|
||||
<StyledListItem>
|
||||
{result.mode === 'edgeless' ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PaperIcon />
|
||||
)}
|
||||
<span>{result.title}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
) : (
|
||||
<StyledNotFound>
|
||||
<span>Find 0 result</span>
|
||||
<NoResultSVG />
|
||||
</StyledNotFound>
|
||||
)
|
||||
) : (
|
||||
<Command.Group heading="Switch to">
|
||||
{List.map(link => {
|
||||
return (
|
||||
<Command.Item
|
||||
key={link.title}
|
||||
value={link.title}
|
||||
onSelect={() => {
|
||||
router.push(link.href);
|
||||
triggerQuickSearchModal();
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
<link.icon />
|
||||
<span>{link.title}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
153
packages/app/src/components/quick-search/style.ts
Normal file
153
packages/app/src/components/quick-search/style.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { displayFlex, styled } from '@/styles';
|
||||
|
||||
export const StyledContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
minHeight: '220px',
|
||||
maxHeight: '55vh',
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
marginBottom: '10px',
|
||||
...displayFlex('center', 'flex-start'),
|
||||
color: theme.colors.popoverColor,
|
||||
letterSpacing: '0.06em',
|
||||
'[cmdk-group-heading]': {
|
||||
margin: '5px 16px',
|
||||
fontSize: theme.font.sm,
|
||||
fontWeight: '500',
|
||||
},
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'background .15s, color .15s',
|
||||
borderRadius: '5px',
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledJumpTo = styled('div')(({ theme }) => {
|
||||
return {
|
||||
...displayFlex('center', 'start'),
|
||||
flexDirection: 'column',
|
||||
padding: '10px 10px 10px 0',
|
||||
fontSize: theme.font.sm,
|
||||
strong: {
|
||||
fontWeight: '500',
|
||||
marginBottom: '10px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledNotFound = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '612px',
|
||||
...displayFlex('center', 'center'),
|
||||
flexDirection: 'column',
|
||||
padding: '5px 16px',
|
||||
fontSize: theme.font.sm,
|
||||
span: {
|
||||
width: '100%',
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
'>svg': {
|
||||
marginTop: '10px',
|
||||
fontSize: '150px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledInputContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '13px 0',
|
||||
...displayFlex('space-between', 'center'),
|
||||
input: {
|
||||
width: '492px',
|
||||
height: '22px',
|
||||
padding: '0 12px',
|
||||
fontSize: theme.font.base,
|
||||
...displayFlex('space-between', 'center'),
|
||||
letterSpacing: '0.06em',
|
||||
color: theme.colors.popoverColor,
|
||||
|
||||
'::placeholder': {
|
||||
color: theme.colors.placeHolderColor,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledShortcut = styled('div')(({ theme }) => {
|
||||
return { color: theme.colors.placeHolderColor, fontSize: theme.font.sm };
|
||||
});
|
||||
|
||||
export const StyledLabel = styled('label')(({ theme }) => {
|
||||
return {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
height: '48px',
|
||||
margin: '12px 24px 0px 24px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
export const StyledModalDivider = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: '0',
|
||||
margin: '6px 16px 6.5px 16px',
|
||||
position: 'relative',
|
||||
borderTop: `0.5px solid ${theme.colors.placeHolderColor}`,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalFooter = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '22px',
|
||||
marginBottom: '8px',
|
||||
textAlign: 'center',
|
||||
...displayFlex('center', 'center'),
|
||||
color: theme.colors.popoverColor,
|
||||
'[aria-selected="true"]': {
|
||||
transition: 'background .15s, color .15s',
|
||||
borderRadius: '5px',
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledModalFooterContent = styled.button(({ theme }) => {
|
||||
return {
|
||||
width: '612px',
|
||||
height: '32px',
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '22px',
|
||||
textAlign: 'center',
|
||||
...displayFlex('center', 'center'),
|
||||
color: 'inherit',
|
||||
borderRadius: '5px',
|
||||
transition: 'background .15s, color .15s',
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledListItem = styled.button(({ theme }) => {
|
||||
return {
|
||||
width: '612px',
|
||||
height: '32px',
|
||||
fontSize: theme.font.sm,
|
||||
color: 'inherit',
|
||||
paddingLeft: '12px',
|
||||
borderRadius: '5px',
|
||||
transition: 'background .15s, color .15s',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
'>svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -6,6 +6,7 @@ export const macKeyboardShortcuts = {
|
||||
Underline: '⌘+U',
|
||||
Strikethrough: '⌘+⇧+S',
|
||||
'Inline code': ' ⌘+E',
|
||||
'Code block': '⌘+⌥+C',
|
||||
Link: '⌘+K',
|
||||
'Body text': '⌘+⌥+0',
|
||||
'Heading 1': '⌘+⌥+1',
|
||||
@@ -23,7 +24,9 @@ export const macMarkdownShortcuts = {
|
||||
Italic: '*Text* ',
|
||||
Underline: '~Text~ ',
|
||||
Strikethrough: '~~Text~~ ',
|
||||
Divider: '***',
|
||||
'Inline code': '`Text` ',
|
||||
'Code block': '``` Space',
|
||||
'Heading 1': '# Text',
|
||||
'Heading 2': '## Text',
|
||||
'Heading 3': '### Text',
|
||||
@@ -40,6 +43,7 @@ export const windowsKeyboardShortcuts = {
|
||||
Underline: 'Ctrl+U',
|
||||
Strikethrough: 'Ctrl+Shift+S',
|
||||
'Inline code': ' Ctrl+E',
|
||||
'Code block': 'Ctrl+Alt+C',
|
||||
Link: 'Ctrl+K',
|
||||
'Body text': 'Ctrl+Shift+0',
|
||||
'Heading 1': 'Ctrl+Shift+1',
|
||||
@@ -57,6 +61,7 @@ export const winMarkdownShortcuts = {
|
||||
Underline: '~Text~ ',
|
||||
Strikethrough: '~~Text~~ ',
|
||||
'Inline code': '`Text` ',
|
||||
'Code block': '``` Text',
|
||||
'Heading 1': '# Text',
|
||||
'Heading 2': '## Text',
|
||||
'Heading 3': '### Text',
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
StyledShortcutsModal,
|
||||
StyledSubTitle,
|
||||
StyledTitle,
|
||||
CloseButton,
|
||||
} from './style';
|
||||
import {
|
||||
macKeyboardShortcuts,
|
||||
@@ -14,15 +13,16 @@ import {
|
||||
windowsKeyboardShortcuts,
|
||||
winMarkdownShortcuts,
|
||||
} from '@/components/shortcuts-modal/config';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { getUaHelper } from '@/utils';
|
||||
type ModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const isMac = () => {
|
||||
return /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
return getUaHelper().isMacOs;
|
||||
};
|
||||
|
||||
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
@@ -42,13 +42,15 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
Shortcuts
|
||||
</StyledTitle>
|
||||
|
||||
<CloseButton
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
size={[24, 24]}
|
||||
iconSize={[15, 15]}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</CloseButton>
|
||||
/>
|
||||
</StyledModalHeader>
|
||||
<StyledSubTitle style={{ marginTop: 0 }}>
|
||||
Keyboard Shortcuts
|
||||
|
||||
@@ -38,7 +38,7 @@ export const StyledSubTitle = styled.div(({ theme }) => ({
|
||||
marginTop: '28px',
|
||||
padding: '0 16px',
|
||||
}));
|
||||
export const StyledModalHeader = styled.div(({ theme }) => ({
|
||||
export const StyledModalHeader = styled.div(() => ({
|
||||
...displayFlex('space-between', 'center'),
|
||||
paddingTop: '8px 4px 0 4px',
|
||||
width: '100%',
|
||||
@@ -57,22 +57,3 @@ export const StyledListItem = styled.div(({ theme }) => ({
|
||||
fontSize: theme.font.sm,
|
||||
padding: '0 16px',
|
||||
}));
|
||||
|
||||
export const CloseButton = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '5px',
|
||||
color: theme.colors.iconColor,
|
||||
cursor: 'pointer',
|
||||
...displayFlex('center', 'center'),
|
||||
svg: {
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
},
|
||||
':hover': {
|
||||
background: theme.colors.hoverBackground,
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export const tagName = 'simple-counter';
|
||||
|
||||
// Adapt React in order to be able to use custom tags properly
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
[tagName]: PersonInfoProps;
|
||||
@@ -49,10 +50,10 @@ export class Counter extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _increment(e: Event) {
|
||||
private _increment() {
|
||||
this.count++;
|
||||
}
|
||||
private _subtract(e: Event) {
|
||||
private _subtract() {
|
||||
this.count--;
|
||||
}
|
||||
}
|
||||
|
||||
35
packages/app/src/components/workspace-layout/index.tsx
Normal file
35
packages/app/src/components/workspace-layout/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import HelpIsland from '@/components/help-island';
|
||||
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
|
||||
import { useRouter } from 'next/router';
|
||||
import { StyledPage, StyledWrapper } from './styles';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
|
||||
import { PageLoading } from '@/components/loading';
|
||||
|
||||
export const WorkspaceDefender = ({ children }: PropsWithChildren) => {
|
||||
const { workspaceLoaded } = useEnsureWorkspace();
|
||||
return <>{workspaceLoaded ? children : <PageLoading />}</>;
|
||||
};
|
||||
|
||||
export const WorkspaceLayout = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<StyledPage>
|
||||
<WorkSpaceSliderBar />
|
||||
<StyledWrapper>
|
||||
{children}
|
||||
<HelpIsland showList={router.query.pageId ? undefined : ['contact']} />
|
||||
</StyledWrapper>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
export const Layout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<WorkspaceDefender>
|
||||
<WorkspaceLayout>{children}</WorkspaceLayout>
|
||||
</WorkspaceDefender>
|
||||
);
|
||||
};
|
||||
export default Layout;
|
||||
18
packages/app/src/components/workspace-layout/styles.ts
Normal file
18
packages/app/src/components/workspace-layout/styles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledPage = styled('div')(({ theme }) => {
|
||||
return {
|
||||
height: '100vh',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
transition: 'background-color .5s',
|
||||
display: 'flex',
|
||||
flexGrow: '1',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWrapper = styled('div')(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import Modal from '@/ui/modal';
|
||||
import Input from '@/ui/input';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledInputContent,
|
||||
StyledButtonContent,
|
||||
StyledWorkspaceName,
|
||||
} from './style';
|
||||
import { useState } from 'react';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { deleteWorkspace } from '@affine/data-services';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
}
|
||||
|
||||
export const WorkspaceDelete = ({
|
||||
open,
|
||||
onClose,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
nextWorkSpaceId,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const router = useRouter();
|
||||
|
||||
const handlerInputChange = (workspaceName: string) => {
|
||||
setDeleteStr(workspaceName);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Delete Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
This action cannot be undone. This will permanently delete (
|
||||
<StyledWorkspaceName>{workspaceName}</StyledWorkspaceName>) along with
|
||||
all its content.
|
||||
</StyledTextContent>
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
onChange={handlerInputChange}
|
||||
placeholder="Please type “Delete” to confirm"
|
||||
value={deleteStr}
|
||||
></Input>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={deleteStr.toLowerCase() !== 'delete'}
|
||||
onClick={handleDelete}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceDelete;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './delete';
|
||||
@@ -0,0 +1,75 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '40px 0 24px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
color: '#E8178A',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledCancelButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
|
||||
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
|
||||
// return {
|
||||
// width: '100px',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
// });
|
||||
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
StyledDeleteButtonContainer,
|
||||
StyledSettingAvatar,
|
||||
StyledSettingAvatarContent,
|
||||
StyledSettingInputContainer,
|
||||
} from './style';
|
||||
import { StyledSettingH2 } from '../style';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { uploadBlob, Workspace, WorkspaceType } from '@affine/data-services';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { WorkspaceDelete } from './delete';
|
||||
import { Workspace as StoreWorkspace } from '@blocksuite/store';
|
||||
import { debounce } from '@/utils';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
import { Upload } from '@/components/file-upload';
|
||||
|
||||
export const GeneralPage = ({
|
||||
workspace,
|
||||
owner,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
workspaces: Record<string, StoreWorkspace | null>;
|
||||
}) => {
|
||||
const {
|
||||
user,
|
||||
currentWorkspace,
|
||||
workspacesMeta,
|
||||
workspaces,
|
||||
refreshWorkspacesMeta,
|
||||
} = useAppState();
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
const [showLeave, setShowLeave] = useState<boolean>(false);
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [workspaceName, setWorkspaceName] = useState<string>(
|
||||
workspaces[workspace.id]?.meta.name ||
|
||||
(workspace.type === WorkspaceType.Private && user ? user.name : '')
|
||||
);
|
||||
const debouncedRefreshWorkspacesMeta = debounce(() => {
|
||||
refreshWorkspacesMeta();
|
||||
}, 100);
|
||||
const isOwner = user && owner.id === user.id;
|
||||
const handleChangeWorkSpaceName = (newName: string) => {
|
||||
setWorkspaceName(newName);
|
||||
currentWorkspace?.meta.setName(newName);
|
||||
workspaces[workspace.id]?.meta.setName(newName);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
};
|
||||
const currentWorkspaceIndex = workspacesMeta.findIndex(
|
||||
meta => meta.id === workspace.id
|
||||
);
|
||||
const nextWorkSpaceId =
|
||||
currentWorkspaceIndex === workspacesMeta.length - 1
|
||||
? workspacesMeta[currentWorkspaceIndex - 1]?.id
|
||||
: workspacesMeta[currentWorkspaceIndex + 1]?.id;
|
||||
const handleClickDelete = () => {
|
||||
setShowDelete(true);
|
||||
};
|
||||
const handleCloseDelete = () => {
|
||||
setShowDelete(false);
|
||||
};
|
||||
|
||||
const handleClickLeave = () => {
|
||||
setShowLeave(true);
|
||||
};
|
||||
const handleCloseLeave = () => {
|
||||
setShowLeave(false);
|
||||
};
|
||||
|
||||
const fileChange = async (file: File) => {
|
||||
setUploading(true);
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
const blobId = await uploadBlob({ blob }).finally(() => {
|
||||
setUploading(false);
|
||||
});
|
||||
if (blobId) {
|
||||
currentWorkspace?.meta.setAvatar(blobId);
|
||||
workspaces[workspace.id]?.meta.setAvatar(blobId);
|
||||
setUploading(false);
|
||||
debouncedRefreshWorkspacesMeta();
|
||||
}
|
||||
};
|
||||
|
||||
return workspace ? (
|
||||
<div>
|
||||
<StyledSettingH2 marginTop={56}>Workspace Avatar</StyledSettingH2>
|
||||
<StyledSettingAvatarContent>
|
||||
<StyledSettingAvatar
|
||||
alt="workspace avatar"
|
||||
src={
|
||||
workspaces[workspace.id]?.meta.avatar
|
||||
? '/api/blob/' + workspaces[workspace.id]?.meta.avatar
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{workspaces[workspace.id]?.meta.name[0]}
|
||||
</StyledSettingAvatar>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={fileChange}
|
||||
>
|
||||
<Button loading={uploading}>Upload</Button>
|
||||
</Upload>
|
||||
{/* TODO: add upload logic */}
|
||||
{/* {isOwner ? (
|
||||
<StyledAvatarUploadBtn shape="round">upload</StyledAvatarUploadBtn>
|
||||
) : null} */}
|
||||
{/* <Button shape="round">remove</Button> */}
|
||||
</StyledSettingAvatarContent>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Name</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
value={workspaceName}
|
||||
placeholder="Workspace Name"
|
||||
disabled={!isOwner}
|
||||
maxLength={14}
|
||||
minLength={1}
|
||||
onChange={handleChangeWorkSpaceName}
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<StyledSettingH2 marginTop={36}>Workspace Owner</StyledSettingH2>
|
||||
<StyledSettingInputContainer>
|
||||
<Input
|
||||
width={327}
|
||||
disabled
|
||||
value={owner.name}
|
||||
placeholder="Workspace Owner"
|
||||
></Input>
|
||||
</StyledSettingInputContainer>
|
||||
<StyledDeleteButtonContainer>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickDelete}>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<WorkspaceDelete
|
||||
open={showDelete}
|
||||
onClose={handleCloseDelete}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button type="danger" shape="circle" onClick={handleClickLeave}>
|
||||
Leave Workspace
|
||||
</Button>
|
||||
<WorkspaceLeave
|
||||
open={showLeave}
|
||||
onClose={handleCloseLeave}
|
||||
workspaceName={workspaceName}
|
||||
workspaceId={workspace.id}
|
||||
nextWorkSpaceId={nextWorkSpaceId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledDeleteButtonContainer>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './general';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './leave';
|
||||
@@ -0,0 +1,64 @@
|
||||
import Modal from '@/ui/modal';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledButtonContent,
|
||||
} from './style';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import { Button } from '@/ui/button';
|
||||
import { leaveWorkspace } from '@affine/data-services';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspaceName: string;
|
||||
workspaceId: string;
|
||||
nextWorkSpaceId: string;
|
||||
}
|
||||
|
||||
export const WorkspaceLeave = ({
|
||||
open,
|
||||
onClose,
|
||||
nextWorkSpaceId,
|
||||
workspaceId,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const router = useRouter();
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const handleLeave = async () => {
|
||||
await leaveWorkspace({ id: workspaceId });
|
||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||
refreshWorkspacesMeta();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Leave Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
After you leave, you will not be able to access all the contents of
|
||||
this workspace.
|
||||
</StyledTextContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="danger"
|
||||
shape="circle"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceLeave;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@/styles';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
|
||||
export const StyledSettingInputContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledDeleteButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '154px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingAvatarContent = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '72px', width: '72px', marginRight: '24px' };
|
||||
});
|
||||
1
packages/app/src/components/workspace-setting/index.ts
Normal file
1
packages/app/src/components/workspace-setting/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './workspace-setting';
|
||||
230
packages/app/src/components/workspace-setting/style.ts
Normal file
230
packages/app/src/components/workspace-setting/style.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
|
||||
export const StyledSettingContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
padding: '0px',
|
||||
width: '961px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingSidebar = styled('div')(({ theme }) => {
|
||||
{
|
||||
return {
|
||||
width: '212px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingContent = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '48px',
|
||||
height: '620px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSetting = styled('div')(({ theme }) => {
|
||||
{
|
||||
return {
|
||||
width: '236px',
|
||||
height: '620px',
|
||||
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingSidebarHeader = styled('div')(() => {
|
||||
{
|
||||
return {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
marginTop: '37px',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const StyledSettingTabContainer = styled('ul')(() => {
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '25px',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
|
||||
({ theme, isActive }) => {
|
||||
{
|
||||
return {
|
||||
display: 'flex',
|
||||
marginBottom: '12px',
|
||||
padding: '0 24px',
|
||||
height: '32px',
|
||||
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '32px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledSettingTagIconContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '14.64px',
|
||||
width: '14.47px',
|
||||
fontSize: '14.47px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSettingH2 = styled('h2')<{ marginTop?: number }>(
|
||||
({ marginTop }) => {
|
||||
return {
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
marginTop: marginTop ? `${marginTop}px` : '0px',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledAvatarUploadBtn = styled(Button)(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
color: theme.colors.primaryColor,
|
||||
margin: '0 12px 0 24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberTitleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
marginTop: '60px',
|
||||
fontWeight: '500',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
|
||||
return { height: '40px', width: '40px' };
|
||||
});
|
||||
|
||||
export const StyledMemberNameContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '402px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberRoleContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '222px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListContainer = styled('ul')(() => {
|
||||
return {
|
||||
marginTop: '15px',
|
||||
height: '432px',
|
||||
overflowY: 'scroll',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberListItem = styled('li')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '72px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberInfo = styled('div')(() => {
|
||||
return {
|
||||
paddingLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberName = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '16px',
|
||||
color: theme.colors.textColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberEmail = styled('div')(({ theme }) => {
|
||||
return {
|
||||
fontWeight: '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMemberButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '14px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMoreVerticalButton = styled('button')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishExplanation = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '56px',
|
||||
paddingRight: '48px',
|
||||
fontWeight: '500',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishCopyContainer = styled('div')(() => {
|
||||
return {
|
||||
marginTop: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: '38px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCopyButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPublishContent = styled('div')(() => {
|
||||
return {
|
||||
height: '494px',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
import Modal, { ModalCloseButton } from '@/ui/modal';
|
||||
import {
|
||||
StyledCopyButtonContainer,
|
||||
StyledMemberAvatar,
|
||||
StyledMemberButtonContainer,
|
||||
StyledMemberEmail,
|
||||
StyledMemberInfo,
|
||||
StyledMemberListContainer,
|
||||
StyledMemberListItem,
|
||||
StyledMemberName,
|
||||
StyledMemberNameContainer,
|
||||
StyledMemberRoleContainer,
|
||||
StyledMemberTitleContainer,
|
||||
StyledMoreVerticalButton,
|
||||
StyledPublishContent,
|
||||
StyledPublishCopyContainer,
|
||||
StyledPublishExplanation,
|
||||
StyledSettingContainer,
|
||||
StyledSettingContent,
|
||||
StyledSettingH2,
|
||||
StyledSettingSidebar,
|
||||
StyledSettingSidebarHeader,
|
||||
StyledSettingTabContainer,
|
||||
StyledSettingTagIconContainer,
|
||||
WorkspaceSettingTagItem,
|
||||
} from './style';
|
||||
import {
|
||||
EditIcon,
|
||||
UsersIcon,
|
||||
PublishIcon,
|
||||
MoreVerticalIcon,
|
||||
EmailIcon,
|
||||
TrashIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, IconButton } from '@/ui/button';
|
||||
import Input from '@/ui/input';
|
||||
import { InviteMembers } from '../invite-members/index';
|
||||
import {
|
||||
getWorkspaceMembers,
|
||||
Workspace,
|
||||
Member,
|
||||
removeMember,
|
||||
updateWorkspace,
|
||||
} from '@affine/data-services';
|
||||
import { Avatar } from '@mui/material';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { toast } from '@/ui/toast';
|
||||
import { Empty } from '@/ui/empty';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { WorkspaceDetails } from '../workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||
import { GeneralPage } from './general';
|
||||
|
||||
enum ActiveTab {
|
||||
'general' = 'general',
|
||||
'members' = 'members',
|
||||
'publish' = 'publish',
|
||||
}
|
||||
|
||||
type SettingTabProps = {
|
||||
activeTab: ActiveTab;
|
||||
onTabChange?: (tab: ActiveTab) => void;
|
||||
};
|
||||
|
||||
type WorkspaceSettingProps = {
|
||||
isShow: boolean;
|
||||
onClose?: () => void;
|
||||
workspace: Workspace;
|
||||
owner: WorkspaceDetails[string]['owner'];
|
||||
};
|
||||
|
||||
const WorkspaceSettingTab = ({ activeTab, onTabChange }: SettingTabProps) => {
|
||||
return (
|
||||
<StyledSettingTabContainer>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.general}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.general);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<EditIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
General
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.members}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.members);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<UsersIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Members
|
||||
</WorkspaceSettingTagItem>
|
||||
<WorkspaceSettingTagItem
|
||||
isActive={activeTab === ActiveTab.publish}
|
||||
onClick={() => {
|
||||
onTabChange && onTabChange(ActiveTab.publish);
|
||||
}}
|
||||
>
|
||||
<StyledSettingTagIconContainer>
|
||||
<PublishIcon />
|
||||
</StyledSettingTagIconContainer>
|
||||
Publish
|
||||
</WorkspaceSettingTagItem>
|
||||
</StyledSettingTabContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceSetting = ({
|
||||
isShow,
|
||||
onClose,
|
||||
workspace,
|
||||
owner,
|
||||
}: WorkspaceSettingProps) => {
|
||||
const { user, workspaces } = useAppState();
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.general);
|
||||
const handleTabChange = (tab: ActiveTab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
const handleClickClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
const isOwner = user && owner.id === user.id;
|
||||
useEffect(() => {
|
||||
// reset tab when modal is closed
|
||||
if (!isShow) {
|
||||
setActiveTab(ActiveTab.general);
|
||||
}
|
||||
}, [isShow]);
|
||||
return (
|
||||
<Modal open={isShow}>
|
||||
<StyledSettingContainer>
|
||||
<ModalCloseButton onClick={handleClickClose} />
|
||||
{isOwner ? (
|
||||
<StyledSettingSidebar>
|
||||
<StyledSettingSidebarHeader>
|
||||
Workspace Settings
|
||||
</StyledSettingSidebarHeader>
|
||||
<WorkspaceSettingTab
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</StyledSettingSidebar>
|
||||
) : null}
|
||||
<StyledSettingContent>
|
||||
{activeTab === ActiveTab.general && (
|
||||
<GeneralPage
|
||||
workspace={workspace}
|
||||
owner={owner}
|
||||
workspaces={workspaces}
|
||||
/>
|
||||
)}
|
||||
{activeTab === ActiveTab.members && workspace && (
|
||||
<MembersPage workspace={workspace} />
|
||||
)}
|
||||
{activeTab === ActiveTab.publish && (
|
||||
<PublishPage workspace={workspace} />
|
||||
)}
|
||||
</StyledSettingContent>
|
||||
</StyledSettingContainer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const refreshMembers = useCallback(() => {
|
||||
getWorkspaceMembers({
|
||||
id: workspace.id,
|
||||
})
|
||||
.then(data => {
|
||||
setMembers(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}, [workspace.id]);
|
||||
useEffect(() => {
|
||||
refreshMembers();
|
||||
}, [refreshMembers]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledMemberTitleContainer>
|
||||
<StyledMemberNameContainer>
|
||||
Users({members.length})
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>Access level</StyledMemberRoleContainer>
|
||||
</StyledMemberTitleContainer>
|
||||
<StyledMemberListContainer>
|
||||
{members.length === 0 && (
|
||||
<Empty width={648} sx={{ marginTop: '60px' }} height={300}></Empty>
|
||||
)}
|
||||
{members.length ? (
|
||||
members.map(member => {
|
||||
return (
|
||||
<StyledMemberListItem key={member.id}>
|
||||
<StyledMemberNameContainer>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<Avatar src={member.user.avatar_url}></Avatar>
|
||||
) : (
|
||||
<StyledMemberAvatar alt="member avatar">
|
||||
<EmailIcon></EmailIcon>
|
||||
</StyledMemberAvatar>
|
||||
)}
|
||||
|
||||
<StyledMemberInfo>
|
||||
{member.user.type === 'Registered' ? (
|
||||
<StyledMemberName>{member.user.name}</StyledMemberName>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<StyledMemberEmail>{member.user.email}</StyledMemberEmail>
|
||||
</StyledMemberInfo>
|
||||
</StyledMemberNameContainer>
|
||||
<StyledMemberRoleContainer>
|
||||
{member.accepted
|
||||
? member.type !== 99
|
||||
? 'Member'
|
||||
: 'Workspace Owner'
|
||||
: 'Pending'}
|
||||
</StyledMemberRoleContainer>
|
||||
<StyledMoreVerticalButton>
|
||||
<Menu
|
||||
content={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
// confirm({
|
||||
// title: 'Delete Member?',
|
||||
// content: `will delete member`,
|
||||
// confirmText: 'Delete',
|
||||
// confirmType: 'danger',
|
||||
// }).then(confirm => {
|
||||
removeMember({
|
||||
permissionId: member.id,
|
||||
}).then(() => {
|
||||
// console.log('data: ', data);
|
||||
toast('Moved to Trash');
|
||||
refreshMembers();
|
||||
});
|
||||
// });
|
||||
}}
|
||||
icon={<TrashIcon />}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</StyledMoreVerticalButton>
|
||||
</StyledMemberListItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</StyledMemberListContainer>
|
||||
<StyledMemberButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsInviteModalShow(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Invite Members
|
||||
</Button>
|
||||
<InviteMembers
|
||||
onClose={() => {
|
||||
setIsInviteModalShow(false);
|
||||
}}
|
||||
onInviteSuccess={() => {
|
||||
refreshMembers();
|
||||
}}
|
||||
workspaceId={workspace.id}
|
||||
open={isInviteModalShow}
|
||||
></InviteMembers>
|
||||
</StyledMemberButtonContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PublishPage = ({ workspace }: { workspace: Workspace }) => {
|
||||
const shareUrl = window.location.host + '/workspace/' + workspace.id;
|
||||
const [publicStatus, setPublicStatus] = useState<boolean | null>(
|
||||
workspace.public
|
||||
);
|
||||
const togglePublic = (flag: boolean) => {
|
||||
updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: flag,
|
||||
}).then(data => {
|
||||
setPublicStatus(data?.public);
|
||||
toast('Updated Public Status Success');
|
||||
});
|
||||
};
|
||||
const copyUrl = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast('Copied url to clipboard');
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<StyledPublishContent>
|
||||
{publicStatus ? (
|
||||
<>
|
||||
<StyledPublishExplanation>
|
||||
The current workspace has been published to the web, everyone can
|
||||
view the contents of this workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
<StyledSettingH2 marginTop={48}>Share with link</StyledSettingH2>
|
||||
<StyledPublishCopyContainer>
|
||||
<Input width={500} value={shareUrl} disabled={true}></Input>
|
||||
<StyledCopyButtonContainer>
|
||||
<Button onClick={copyUrl} type="primary" shape="circle">
|
||||
Copy Link
|
||||
</Button>
|
||||
</StyledCopyButtonContainer>
|
||||
</StyledPublishCopyContainer>
|
||||
</>
|
||||
) : (
|
||||
<StyledPublishExplanation>
|
||||
After publishing to the web, everyone can view the content of this
|
||||
workspace through the link.
|
||||
</StyledPublishExplanation>
|
||||
)}
|
||||
</StyledPublishContent>
|
||||
{!publicStatus ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(true);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Publish to web
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
togglePublic(false);
|
||||
}}
|
||||
type="primary"
|
||||
shape="circle"
|
||||
>
|
||||
Stop publishing
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Divider } from '@/ui/divider';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import { SelectorPopperContainer } from './styles';
|
||||
import {
|
||||
PrivateWorkspaceItem,
|
||||
WorkspaceItem,
|
||||
CreateWorkspaceItem,
|
||||
ListItem,
|
||||
LoginItem,
|
||||
} from './WorkspaceItem';
|
||||
import { WorkspaceSetting } from '@/components/workspace-setting';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { getWorkspaceDetail, WorkspaceType } from '@affine/data-services';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
export type WorkspaceDetails = Record<
|
||||
string,
|
||||
{ memberCount: number; owner: { id: string; name: string } }
|
||||
>;
|
||||
|
||||
type SelectorPopperContentProps = {
|
||||
isShow: boolean;
|
||||
};
|
||||
|
||||
export const SelectorPopperContent = ({
|
||||
isShow,
|
||||
}: SelectorPopperContentProps) => {
|
||||
const { user, workspacesMeta, workspaces, refreshWorkspacesMeta } =
|
||||
useAppState();
|
||||
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [workSpaceDetails, setWorkSpaceDetails] = useState<WorkspaceDetails>(
|
||||
{}
|
||||
);
|
||||
const { triggerContactModal } = useModal();
|
||||
|
||||
const handleClickSettingWorkspace = (workspaceId: string) => {
|
||||
setSettingWorkspaceId(workspaceId);
|
||||
};
|
||||
const handleCloseWorkSpace = () => {
|
||||
setSettingWorkspaceId(null);
|
||||
};
|
||||
const settingWorkspace = settingWorkspaceId
|
||||
? workspacesMeta.find(workspace => workspace.id === settingWorkspaceId)
|
||||
: undefined;
|
||||
|
||||
const refreshDetails = useCallback(async () => {
|
||||
const workspaceDetailList = await Promise.all(
|
||||
workspacesMeta.map(async ({ id, type }) => {
|
||||
if (user) {
|
||||
if (type === WorkspaceType.Private) {
|
||||
return { id, member_count: 1, owner: user };
|
||||
} else {
|
||||
const data = await getWorkspaceDetail({ id });
|
||||
return { id, ...data } || { id, member_count: 0, owner: user };
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const workSpaceDetails: WorkspaceDetails = {};
|
||||
workspaceDetailList.forEach(details => {
|
||||
if (details) {
|
||||
const { id, member_count, owner } = details;
|
||||
if (!owner) return;
|
||||
workSpaceDetails[id] = {
|
||||
memberCount: member_count || 1,
|
||||
owner: {
|
||||
id: owner.id,
|
||||
name: owner.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
setWorkSpaceDetails(workSpaceDetails);
|
||||
}, [user, workspacesMeta]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isShow) {
|
||||
setSettingWorkspaceId(null);
|
||||
refreshWorkspacesMeta();
|
||||
refreshDetails();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isShow]);
|
||||
|
||||
return !user ? (
|
||||
<SelectorPopperContainer placement="bottom-start">
|
||||
<LoginItem />
|
||||
<StyledDivider />
|
||||
<ListItem
|
||||
icon={<InformationIcon />}
|
||||
name="About AFFiNE"
|
||||
onClick={() => triggerContactModal()}
|
||||
/>
|
||||
</SelectorPopperContainer>
|
||||
) : (
|
||||
<SelectorPopperContainer placement="bottom-start">
|
||||
<PrivateWorkspaceItem
|
||||
privateWorkspaceId={
|
||||
workspacesMeta.find(
|
||||
workspace => workspace.type === WorkspaceType.Private
|
||||
)?.id
|
||||
}
|
||||
/>
|
||||
<StyledDivider />
|
||||
<WorkspaceGroupTitle>Workspace</WorkspaceGroupTitle>
|
||||
<WorkspaceWrapper>
|
||||
{workspacesMeta.map(workspace => {
|
||||
return workspace.type !== WorkspaceType.Private ? (
|
||||
<WorkspaceItem
|
||||
type={workspace.type}
|
||||
key={workspace.id}
|
||||
id={workspace.id}
|
||||
icon={
|
||||
(workspaces[workspace.id]?.meta.avatar &&
|
||||
`/api/blob/${workspaces[workspace.id]?.meta.avatar}`) ||
|
||||
`loading...`
|
||||
}
|
||||
onClickSetting={handleClickSettingWorkspace}
|
||||
name={workspaces[workspace.id]?.meta.name || `loading...`}
|
||||
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</WorkspaceWrapper>
|
||||
<CreateWorkspaceItem />
|
||||
{settingWorkspace ? (
|
||||
<WorkspaceSetting
|
||||
isShow={Boolean(settingWorkspaceId)}
|
||||
onClose={handleCloseWorkSpace}
|
||||
workspace={settingWorkspace}
|
||||
owner={
|
||||
(settingWorkspaceId &&
|
||||
workSpaceDetails[settingWorkspaceId]?.owner) || {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<StyledDivider />
|
||||
<ListItem
|
||||
icon={<InformationIcon />}
|
||||
name="About AFFiNE"
|
||||
onClick={() => triggerContactModal()}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<LogOutIcon />}
|
||||
name="Sign out"
|
||||
onClick={() => {
|
||||
console.log('Sign out');
|
||||
// FIXME: remove token from local storage and reload the page
|
||||
localStorage.removeItem('affine_token');
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</SelectorPopperContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDivider = styled(Divider)({
|
||||
margin: '8px 12px',
|
||||
width: 'calc(100% - 24px)',
|
||||
});
|
||||
|
||||
const WorkspaceGroupTitle = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '30px',
|
||||
height: '30px',
|
||||
padding: '0 12px',
|
||||
};
|
||||
});
|
||||
|
||||
const WorkspaceWrapper = styled('div')(() => {
|
||||
return {
|
||||
maxHeight: '200px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { AddIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
WorkspaceItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from '../styles';
|
||||
import { WorkspaceCreate } from './workspace-create';
|
||||
|
||||
const name = 'Create new Workspace';
|
||||
|
||||
export const CreateWorkspaceItem = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<WorkspaceItemWrapper onClick={() => setOpen(true)}>
|
||||
<WorkspaceItemAvatar>
|
||||
<AddIcon />
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
</WorkspaceItemWrapper>
|
||||
<WorkspaceCreate open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CreateWorkspaceItem';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './workspace-create';
|
||||
@@ -0,0 +1,63 @@
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '40px 0 24px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButton = styled(Button)(() => {
|
||||
return {
|
||||
width: '260px',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createWorkspace, uploadBlob } from '@affine/data-services';
|
||||
import Modal from '@/ui/modal';
|
||||
import Input from '@/ui/input';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledInputContent,
|
||||
StyledButtonContent,
|
||||
StyledButton,
|
||||
} from './style';
|
||||
import { useState } from 'react';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
import router from 'next/router';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceCreateProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DefaultHeadImgColors = [
|
||||
['#C6F2F3', '#0C6066'],
|
||||
['#FFF5AB', '#896406'],
|
||||
['#FFCCA7', '#8F4500'],
|
||||
['#FFCECE', '#AF1212'],
|
||||
['#E3DEFF', '#511AAB'],
|
||||
];
|
||||
|
||||
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
||||
const [workspaceName, setWorkspaceId] = useState<string>('');
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const { refreshWorkspacesMeta } = useAppState();
|
||||
const handlerInputChange = (workspaceName: string) => {
|
||||
setWorkspaceId(workspaceName);
|
||||
};
|
||||
const createDefaultHeadImg = (workspaceName: string) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = 100;
|
||||
canvas.width = 100;
|
||||
const ctx = canvas.getContext('2d');
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (ctx) {
|
||||
const randomNumber = Math.floor(Math.random() * 5);
|
||||
const randomColor = DefaultHeadImgColors[randomNumber];
|
||||
ctx.fillStyle = randomColor[0];
|
||||
ctx.fillRect(0, 0, 100, 100);
|
||||
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
|
||||
ctx.fillStyle = randomColor[1];
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(workspaceName[0], 50, 50);
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
const blobId = uploadBlob({ blob });
|
||||
resolve(blobId);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}, 'image/png');
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleCreateWorkspace = async () => {
|
||||
setCreating(true);
|
||||
const blobId = await createDefaultHeadImg(workspaceName).catch(() => {
|
||||
setCreating(false);
|
||||
});
|
||||
if (blobId) {
|
||||
createWorkspace({ name: workspaceName, avatar: blobId })
|
||||
.then(async data => {
|
||||
await refreshWorkspacesMeta();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
router.push(`/workspace/${data.id}`);
|
||||
onClose();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, 'err');
|
||||
})
|
||||
.finally(() => {
|
||||
setCreating(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>Create new Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
Workspaces are shared environments where teams can collaborate. After
|
||||
creating a Workspace, you can invite others to join.
|
||||
</StyledTextContent>
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
onChange={handlerInputChange}
|
||||
placeholder="Set a Workspace name"
|
||||
value={workspaceName}
|
||||
></Input>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<StyledButton
|
||||
disabled={!workspaceName.length || creating}
|
||||
onClick={handleCreateWorkspace}
|
||||
loading={creating}
|
||||
>
|
||||
Create
|
||||
</StyledButton>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceCreate;
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
import { WorkspaceItemWrapper, WorkspaceItemContent } from './styles';
|
||||
|
||||
interface ListItemProps {
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ListItem = ({ name, icon, onClick }: ListItemProps) => {
|
||||
return (
|
||||
<WorkspaceItemWrapper onClick={onClick}>
|
||||
<StyledIconWrapper>{icon}</StyledIconWrapper>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
</WorkspaceItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.sm,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledIconWrapper = styled('div')({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
fontSize: '20px',
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { styled } from '@/styles';
|
||||
import { AffineIcon } from '../../icons/icons';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
LoginItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from './styles';
|
||||
|
||||
export const LoginItem = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
return (
|
||||
<LoginItemWrapper
|
||||
onClick={() => triggerLoginModal()}
|
||||
data-testid="open-login-modal"
|
||||
>
|
||||
<WorkspaceItemAvatar alt="AFFiNE" src={''}>
|
||||
<AffineIcon />
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title="AFFiNE">AFFiNE</Name>
|
||||
<Description
|
||||
title="Log in to sync with affine"
|
||||
className="login-description"
|
||||
>
|
||||
Log in to sync with affine
|
||||
</Description>
|
||||
</WorkspaceItemContent>
|
||||
</LoginItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const Description = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { styled } from '@/styles';
|
||||
import { useAppState } from '@/providers/app-state-provider/context';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
PrivateWorkspaceWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from './styles';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type PrivateWorkspaceItemProps = {
|
||||
privateWorkspaceId?: string;
|
||||
};
|
||||
|
||||
export const PrivateWorkspaceItem = ({
|
||||
privateWorkspaceId,
|
||||
}: PrivateWorkspaceItemProps) => {
|
||||
const { user } = useAppState();
|
||||
const router = useRouter();
|
||||
const handleClick = () => {
|
||||
if (privateWorkspaceId) {
|
||||
router.push(`/workspace/${privateWorkspaceId}`);
|
||||
}
|
||||
};
|
||||
if (user) {
|
||||
const Username = user.name;
|
||||
return (
|
||||
<PrivateWorkspaceWrapper onClick={handleClick}>
|
||||
<WorkspaceItemAvatar alt={Username} src={user.avatar_url}>
|
||||
{Username}
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={Username}>{Username}</Name>
|
||||
<Email title={user.email}>{user.email}</Email>
|
||||
</WorkspaceItemContent>
|
||||
</PrivateWorkspaceWrapper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
type SettingProps = {
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const FooterSetting = ({ onClick }: SettingProps) => {
|
||||
const handleClick: MouseEventHandler<HTMLButtonElement> = e => {
|
||||
e.stopPropagation();
|
||||
onClick && onClick();
|
||||
};
|
||||
return (
|
||||
<Wrapper
|
||||
className="footer-setting"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleClick(e);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled(IconButton)(() => {
|
||||
return {
|
||||
fontSize: '20px',
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { UsersIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
type FooterUsersProps = {
|
||||
memberCount: number;
|
||||
};
|
||||
|
||||
export const FooterUsers = ({ memberCount = 1 }: FooterUsersProps) => {
|
||||
return (
|
||||
<Wrapper className="footer-users">
|
||||
<>
|
||||
<UsersIcon />
|
||||
<Tip>{memberCount > 99 ? '99+' : memberCount}</Tip>
|
||||
</>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled(IconButton)({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px',
|
||||
});
|
||||
|
||||
const Tip = styled('span')({
|
||||
fontSize: '12px',
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
WorkspaceItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from '../styles';
|
||||
import { FooterSetting } from './FooterSetting';
|
||||
import { FooterUsers } from './FooterUsers';
|
||||
import { WorkspaceType } from '@affine/data-services';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
|
||||
interface WorkspaceItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
type: WorkspaceType;
|
||||
memberCount: number;
|
||||
onClickSetting?: (workspaceId: string) => void;
|
||||
}
|
||||
|
||||
export const WorkspaceItem = ({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
type,
|
||||
onClickSetting,
|
||||
memberCount,
|
||||
}: WorkspaceItemProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { currentWorkspaceId } = useAppState();
|
||||
|
||||
const handleClickSetting = async () => {
|
||||
onClickSetting && onClickSetting(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
onClick={() => {
|
||||
router.push(`/workspace/${id}`);
|
||||
}}
|
||||
canSet={
|
||||
type !== WorkspaceType.Private && currentWorkspaceId === String(id)
|
||||
}
|
||||
>
|
||||
<WorkspaceItemAvatar alt={name} src={icon}>
|
||||
{name.charAt(0)}
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
<Footer>
|
||||
<FooterUsers memberCount={memberCount} />
|
||||
<FooterSetting onClick={handleClickSetting} />
|
||||
</Footer>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.sm,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledWrapper = styled(WorkspaceItemWrapper)<{ canSet: boolean }>(
|
||||
({ canSet }) => {
|
||||
return {
|
||||
'& .footer-setting': {
|
||||
display: 'none',
|
||||
},
|
||||
':hover .footer-users': {
|
||||
display: canSet ? 'none' : '',
|
||||
},
|
||||
':hover .footer-setting': {
|
||||
display: canSet ? 'block' : 'none',
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const Footer = styled('div')({
|
||||
width: '42px',
|
||||
flex: '0 42px',
|
||||
fontSize: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '12px',
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WorkspaceItem';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './PrivateWorkspaceItem';
|
||||
export * from './WorkspaceItem';
|
||||
export * from './CreateWorkspaceItem';
|
||||
export * from './ListItem';
|
||||
export * from './LoginItem';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user