mirror of
https://github.com/makeplane/plane.git
synced 2025-12-29 00:24:56 +01:00
[PE-273] chore: mobile-editor (#1565)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { Extensions } from "@tiptap/core";
|
||||
import { AnyExtension } from "@tiptap/core";
|
||||
import { SlashCommands } from "@/extensions";
|
||||
// plane editor types
|
||||
|
||||
@@ -15,9 +15,8 @@ const renderUserCursor = (user: { color: string; name: string }): HTMLSpanElemen
|
||||
cursor.setAttribute("style", `border-color: ${user.color}`);
|
||||
|
||||
const label = document.createElement("span");
|
||||
|
||||
label.classList.value =
|
||||
"absolute rounded-[3px_3px_3px_0] text-[#0d0d0d] text-xs font-semibold leading-normal -top-[1.3rem] -left-[1px] py-0.5 px-1.5 select-none whitespace-nowrap";
|
||||
"custom-collaboration-cursor absolute rounded-[3px_3px_3px_0] text-[#0d0d0d] text-xs font-semibold leading-normal -top-[1.3rem] -left-[1px] py-0.5 px-1.5 select-none whitespace-nowrap";
|
||||
label.setAttribute("style", `background-color: ${user.color}`);
|
||||
label.insertBefore(document.createTextNode(user.name), null);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { Extensions } from "@tiptap/core";
|
||||
import { AnyExtension } from "@tiptap/core";
|
||||
// ui
|
||||
import { LayersIcon } from "@plane/ui";
|
||||
// extensions
|
||||
@@ -20,56 +20,53 @@ type Props = {
|
||||
userDetails: TUserDetails;
|
||||
};
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (props: Props) => {
|
||||
const { disabledExtensions, issueEmbedConfig, provider, userDetails } = props;
|
||||
type ExtensionConfig = {
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
getExtension: (props: Props) => AnyExtension;
|
||||
};
|
||||
|
||||
const isIssueEmbedDisabled = !!disabledExtensions?.includes("issue-embed");
|
||||
const isCollaborationCursorDisabled = !!disabledExtensions?.includes("collaboration-cursor");
|
||||
const createSlashCommandOptions: TSlashCommandAdditionalOption[] = [
|
||||
{
|
||||
commandKey: "issue-embed",
|
||||
key: "issue-embed",
|
||||
title: "Work item embed",
|
||||
description: "Embed work item from the project.",
|
||||
searchTerms: ["work item", "link", "embed"],
|
||||
icon: <LayersIcon className="size-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().insertContentAt(range, "<p>#workitem_</p>").run();
|
||||
},
|
||||
section: "general",
|
||||
pushAfter: "callout",
|
||||
},
|
||||
];
|
||||
|
||||
const extensions: Extensions = [];
|
||||
const additionalSlashCommandOptions: TSlashCommandAdditionalOption[] = [];
|
||||
|
||||
if (!isIssueEmbedDisabled) {
|
||||
const issueEmbedOption: TSlashCommandAdditionalOption = {
|
||||
commandKey: "issue-embed",
|
||||
key: "issue-embed",
|
||||
title: "Work item embed",
|
||||
description: "Embed work item from the project.",
|
||||
searchTerms: ["work item", "link", "embed"],
|
||||
icon: <LayersIcon className="size-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor.chain().focus().insertContentAt(range, "<p>#workitem_</p>").run();
|
||||
},
|
||||
section: "general",
|
||||
pushAfter: "callout",
|
||||
};
|
||||
additionalSlashCommandOptions.push(issueEmbedOption);
|
||||
}
|
||||
extensions.push(
|
||||
SlashCommands({
|
||||
additionalOptions: additionalSlashCommandOptions,
|
||||
disabledExtensions,
|
||||
})
|
||||
);
|
||||
|
||||
if (issueEmbedConfig && !isIssueEmbedDisabled) {
|
||||
extensions.push(
|
||||
const extensionRegistry: ExtensionConfig[] = [
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||
getExtension: () => SlashCommands({ additionalOptions: createSlashCommandOptions }),
|
||||
},
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("issue-embed"),
|
||||
getExtension: ({ issueEmbedConfig }) =>
|
||||
IssueEmbedSuggestions.configure({
|
||||
suggestion: {
|
||||
render: () => issueEmbedConfig.searchCallback && IssueListRenderer(issueEmbedConfig.searchCallback),
|
||||
render: () => issueEmbedConfig?.searchCallback && IssueListRenderer(issueEmbedConfig.searchCallback),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("collaboration-cursor"),
|
||||
getExtension: ({ provider, userDetails }) => CustomCollaborationCursor({ provider, userDetails }),
|
||||
},
|
||||
];
|
||||
|
||||
if (!isCollaborationCursorDisabled) {
|
||||
extensions.push(
|
||||
CustomCollaborationCursor({
|
||||
provider,
|
||||
userDetails,
|
||||
})
|
||||
);
|
||||
}
|
||||
export const DocumentEditorAdditionalExtensions = (props: Props) => {
|
||||
const { disabledExtensions = [] } = props;
|
||||
|
||||
return extensions;
|
||||
const documentExtensions = extensionRegistry
|
||||
.filter((config) => config.isEnabled(disabledExtensions))
|
||||
.map((config) => config.getExtension(props));
|
||||
|
||||
return documentExtensions;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TSlashCommandAdditionalOption } from "@/extensions";
|
||||
import { TExtensions } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
disabledExtensions?: TExtensions[];
|
||||
};
|
||||
|
||||
export const coreEditorAdditionalSlashCommandOptions = (props: Props): TSlashCommandAdditionalOption[] => {
|
||||
|
||||
24
packages/mobile-editor/.gitignore
vendored
Normal file
24
packages/mobile-editor/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
6
packages/mobile-editor/.prettierignore
Normal file
6
packages/mobile-editor/.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.next
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
5
packages/mobile-editor/.prettierrc
Normal file
5
packages/mobile-editor/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
28
packages/mobile-editor/eslint.config.js
Normal file
28
packages/mobile-editor/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
15
packages/mobile-editor/index.html
Normal file
15
packages/mobile-editor/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Plane Mobile Editor</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" data-theme="light"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
34
packages/mobile-editor/package.json
Normal file
34
packages/mobile-editor/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@plane/mobile-editor",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"vite:build": "tsc -b && vite build",
|
||||
"build": "turbo vite:build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/editor": "*",
|
||||
"lucide-react": "^0.454.0",
|
||||
"react": "^18.3.1",
|
||||
"react-cookie": "^7.2.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
"@plane/tailwind-config": "*",
|
||||
"@plane/types": "*",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"globals": "^15.9.0",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-node-polyfills": "^0.22.0"
|
||||
}
|
||||
}
|
||||
6
packages/mobile-editor/postcss.config.js
Normal file
6
packages/mobile-editor/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
214
packages/mobile-editor/src/App.css
Normal file
214
packages/mobile-editor/src/App.css
Normal file
@@ -0,0 +1,214 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.text-1\.5xl {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2\.5xl {
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light !important;
|
||||
|
||||
|
||||
--color-primary-100: 63, 118, 255;
|
||||
--color-primary-200: 57, 106, 230;
|
||||
--color-primary-300: 50, 94, 204;
|
||||
|
||||
--color-error-10: 255 252 252;
|
||||
--color-error-20: 255 247 247;
|
||||
--color-error-30: 255, 219, 220;
|
||||
--color-error-100: 244 170 170;
|
||||
--color-error-200: 220 62 62;
|
||||
--color-error-500: 140 51 58;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 237, 238, 243 !important; /* tertiary bg */
|
||||
|
||||
--color-background-primary: var(--color-primary-10);
|
||||
--color-background-error: var(--color-error-20);
|
||||
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-350: 130, 130, 130;
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-text-primary: var(--color-primary-100);
|
||||
--color-text-error: var(--color-error-200);
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
--color-border-primary: var(--color-primary-40);
|
||||
--color-border-error: var(--color-error-100);
|
||||
|
||||
--color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06),
|
||||
0px 1px 2px 0px rgba(23, 23, 23, 0.14);
|
||||
--color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
|
||||
--color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
|
||||
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
|
||||
--color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
|
||||
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
|
||||
--color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
|
||||
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
|
||||
--color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12),
|
||||
0px 1px 32px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
|
||||
--color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="light-contrast"] {
|
||||
color-scheme: light !important;
|
||||
|
||||
--color-background-100: 255, 255, 255; /* primary bg */
|
||||
--color-background-90: 247, 247, 247; /* secondary bg */
|
||||
--color-background-80: 237, 238, 243 !important; /* tertiary bg */
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-text-100: 23, 23, 23; /* primary text */
|
||||
--color-text-200: 58, 58, 58; /* secondary text */
|
||||
--color-text-300: 82, 82, 82; /* tertiary text */
|
||||
--color-text-350: 130, 130, 130;
|
||||
--color-text-400: 163, 163, 163; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 163, 163, 163; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
|
||||
}
|
||||
|
||||
[data-theme="light-contrast"] {
|
||||
--color-text-100: 11, 11, 11; /* primary text */
|
||||
--color-text-200: 38, 38, 38; /* secondary text */
|
||||
--color-text-300: 58, 58, 58; /* tertiary text */
|
||||
--color-text-350: 80, 80, 80;
|
||||
--color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
[data-theme="dark-contrast"] {
|
||||
color-scheme: dark !important;
|
||||
|
||||
--color-background-100: 25, 25, 25; /* primary bg */
|
||||
--color-background-90: 32, 32, 32; /* secondary bg */
|
||||
--color-background-80: 44, 44, 44; /* tertiary bg */
|
||||
|
||||
--color-background-primary: var(--color-background-90);
|
||||
--color-background-error: var(--color-background-90);
|
||||
|
||||
--color-text-primary: var(--color-primary-40);
|
||||
--color-text-error: var(--color-error-100);
|
||||
|
||||
--color-border-primary: var(--color-primary-200);
|
||||
--color-border-error: var(--color-error-500);
|
||||
|
||||
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
|
||||
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
|
||||
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
|
||||
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-text-100: 229, 229, 229; /* primary text */
|
||||
--color-text-200: 163, 163, 163; /* secondary text */
|
||||
--color-text-300: 115, 115, 115; /* tertiary text */
|
||||
--color-text-350: 130, 130, 130;
|
||||
--color-text-400: 82, 82, 82; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 82, 82, 82; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 34, 34, 34; /* subtle border= 1 */
|
||||
--color-border-200: 38, 38, 38; /* subtle border- 2 */
|
||||
--color-border-300: 46, 46, 46; /* strong border- 1 */
|
||||
--color-border-400: 58, 58, 58; /* strong border- 2 */
|
||||
|
||||
}
|
||||
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-text-100: 250, 250, 250; /* primary text */
|
||||
--color-text-200: 241, 241, 241; /* secondary text */
|
||||
--color-text-300: 212, 212, 212; /* tertiary text */
|
||||
--color-text-350: 190, 190, 190 --color-text-400: 115, 115, 115; /* placeholder text */
|
||||
|
||||
--color-scrollbar: 115, 115, 115; /* scrollbar thumb */
|
||||
|
||||
--color-border-100: 245, 245, 245; /* subtle border= 1 */
|
||||
--color-border-200: 229, 229, 229; /* subtle border- 2 */
|
||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
[data-theme="dark"],
|
||||
[data-theme="light-contrast"],
|
||||
[data-theme="dark-contrast"] {
|
||||
--color-primary-100: 63, 118, 255;
|
||||
--color-primary-200: 57, 106, 230;
|
||||
--color-primary-300: 50, 94, 204;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
font-variant-ligatures: none;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
/* scrollbar style */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
15
packages/mobile-editor/src/App.tsx
Normal file
15
packages/mobile-editor/src/App.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { EditorWrapper } from "@/components/editor/editor-wrapper";
|
||||
import useQueryParams from "@/hooks/use-query-params";
|
||||
import { MobileDocumentEditor } from "@/components/document-editor/document-editor";
|
||||
import { TEditorVariant } from "@/types/editor";
|
||||
|
||||
export const App = () => {
|
||||
const { variant } = useQueryParams(["variant"]);
|
||||
|
||||
// If the variant is not valid, do not render the editor.
|
||||
if (!variant || !Object.keys(TEditorVariant).includes(variant)) return null;
|
||||
|
||||
if (variant === TEditorVariant.document) return <MobileDocumentEditor />;
|
||||
|
||||
return <EditorWrapper variant={variant as TEditorVariant} />;
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
CollaborativeDocumentEditorWithRef,
|
||||
EditorRefApi,
|
||||
TDisplayConfig,
|
||||
TExtensions,
|
||||
TRealtimeConfig,
|
||||
TServerHandler,
|
||||
} from "@plane/editor";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { callNative, generateRandomColor, getEditorFileHandlers } from "@/helpers";
|
||||
import { TDocumentEditorParams } from "@/types/editor";
|
||||
import { useMentions, useMobileEditor, useToolbar, useEditorFlagging, useDisableZoom } from "@/hooks";
|
||||
import { IssueEmbedCard, IssueEmbedUpgradeCard, PageContentLoader } from "@/components";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
import { TWebhookConnectionQueryParams } from "@plane/types";
|
||||
|
||||
export const MobileDocumentEditor = () => {
|
||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||
const [initialParams, setInitialParams] = useState<TDocumentEditorParams | undefined>();
|
||||
|
||||
// hooks
|
||||
const { disabledExtensions, isIssueEmbedEnabled } = useEditorFlagging();
|
||||
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// It disables zooming in the editor.
|
||||
useDisableZoom();
|
||||
// It keeps the native toolbar in sync with the editor state.
|
||||
const { updateActiveStates } = useToolbar(editorRef);
|
||||
|
||||
const { handleEditorReady, onEditorFocus } = useMobileEditor(editorRef);
|
||||
|
||||
const { mentionSuggestionsRef, mentionHighlightsRef } = useMentions();
|
||||
|
||||
const fileHandler = useMemo(
|
||||
() =>
|
||||
getEditorFileHandlers({
|
||||
workspaceSlug: initialParams?.workspaceSlug ?? "",
|
||||
workspaceId: initialParams?.workspaceId ?? "",
|
||||
projectId: initialParams?.projectId ?? "",
|
||||
baseApi: initialParams?.baseApi ?? "",
|
||||
}),
|
||||
[initialParams]
|
||||
);
|
||||
|
||||
const displayConfig: TDisplayConfig = {
|
||||
fontSize: "large-font",
|
||||
fontStyle: "sans-serif",
|
||||
};
|
||||
|
||||
const handleServerConnect = useCallback(() => setHasConnectionFailed(false), []);
|
||||
|
||||
const handleServerError = useCallback(() => setHasConnectionFailed(true), []);
|
||||
|
||||
const serverHandler: TServerHandler = useMemo(
|
||||
() => ({
|
||||
onConnect: handleServerConnect,
|
||||
onServerError: handleServerError,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const realtimeConfig: TRealtimeConfig | undefined = useMemo(() => {
|
||||
if (!initialParams) return undefined;
|
||||
// Construct the WebSocket Collaboration URL
|
||||
try {
|
||||
const LIVE_SERVER_BASE_URL = initialParams?.liveServerUrl.trim();
|
||||
const WS_LIVE_URL = new URL(LIVE_SERVER_BASE_URL);
|
||||
const isSecureEnvironment = initialParams?.liveServerUrl.startsWith("https");
|
||||
WS_LIVE_URL.protocol = isSecureEnvironment ? "wss" : "ws";
|
||||
WS_LIVE_URL.pathname = `${initialParams?.liveServerBasePath}/collaboration`;
|
||||
|
||||
// Construct realtime config
|
||||
return {
|
||||
url: WS_LIVE_URL.toString(),
|
||||
queryParams: {
|
||||
workspaceSlug: initialParams.workspaceSlug.toString(),
|
||||
documentType: initialParams.documentType.toString() as TWebhookConnectionQueryParams["documentType"],
|
||||
projectId: initialParams.projectId ?? "",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating realtime config", error);
|
||||
return undefined;
|
||||
}
|
||||
}, [initialParams]);
|
||||
|
||||
// Disabled extensions for the editor.
|
||||
const resolvedDisabledExtensions: TExtensions[] = useMemo(
|
||||
() => [...(disabledExtensions ?? []), "slash-commands"],
|
||||
[disabledExtensions]
|
||||
);
|
||||
|
||||
const mentionHandler = useMemo(
|
||||
() => ({
|
||||
suggestions: () => Promise.resolve(mentionSuggestionsRef.current),
|
||||
highlights: () => Promise.resolve(mentionHighlightsRef.current),
|
||||
}),
|
||||
[mentionSuggestionsRef.current, mentionHighlightsRef.current]
|
||||
);
|
||||
|
||||
const userConfig = useMemo(
|
||||
() => ({
|
||||
id: initialParams?.userId ?? "",
|
||||
cookie: initialParams?.cookie,
|
||||
name: initialParams?.userDisplayName ?? "",
|
||||
color: generateRandomColor(initialParams?.userId ?? ""),
|
||||
}),
|
||||
[initialParams]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
callNative(CallbackHandlerStrings.getInitialDocumentEditorParams).then((params: TDocumentEditorParams) =>
|
||||
setInitialParams(params)
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (hasConnectionFailed) return null;
|
||||
|
||||
if (!realtimeConfig || !initialParams || !disabledExtensions) return <PageContentLoader />;
|
||||
|
||||
return (
|
||||
<div onClick={onEditorFocus} className="min-h-screen">
|
||||
<CollaborativeDocumentEditorWithRef
|
||||
editable={true}
|
||||
placeholder={"Write something..."}
|
||||
onTransaction={updateActiveStates}
|
||||
id={initialParams?.pageId}
|
||||
fileHandler={fileHandler}
|
||||
handleEditorReady={handleEditorReady}
|
||||
ref={editorRef}
|
||||
containerClassName="min-h-screen p-0"
|
||||
displayConfig={displayConfig}
|
||||
editorClassName="pl-6 min-h-screen pb-32 pt-6"
|
||||
mentionHandler={mentionHandler as any}
|
||||
realtimeConfig={realtimeConfig}
|
||||
serverHandler={serverHandler}
|
||||
user={userConfig}
|
||||
disabledExtensions={resolvedDisabledExtensions}
|
||||
embedHandler={{
|
||||
issue: {
|
||||
widgetCallback: ({ issueId, projectId, workspaceSlug }) => {
|
||||
if (!isIssueEmbedEnabled) return <IssueEmbedUpgradeCard />;
|
||||
return <IssueEmbedCard issueId={issueId} projectId={projectId} workspaceSlug={workspaceSlug} />;
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./document-editor";
|
||||
export * from "./page-content-loader";
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
export const PageContentLoader = () => (
|
||||
<div className="size-full">
|
||||
<Loader className="relative space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="py-2">
|
||||
<Loader.Item width="100%" height="36px" />
|
||||
</div>
|
||||
<Loader.Item width="80%" height="22px" />
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
<Loader.Item width="30%" height="22px" />
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Loader.Item width="60%" height="36px" />
|
||||
</div>
|
||||
<Loader.Item width="70%" height="22px" />
|
||||
<Loader.Item width="30%" height="22px" />
|
||||
<div className="relative flex items-center gap-2">
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
<Loader.Item width="30%" height="22px" />
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<Loader.Item width="50%" height="30px" />
|
||||
</div>
|
||||
<Loader.Item width="100%" height="22px" />
|
||||
<div className="py-2">
|
||||
<Loader.Item width="30%" height="30px" />
|
||||
</div>
|
||||
<Loader.Item width="30%" height="22px" />
|
||||
<div className="relative flex items-center gap-2">
|
||||
<div className="py-2">
|
||||
<Loader.Item width="30px" height="30px" />
|
||||
</div>
|
||||
<Loader.Item width="30%" height="22px" />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { EditorRefApi, LiteTextEditorWithRef, RichTextEditorWithRef, TExtensions } from "@plane/editor";
|
||||
import { useDisableZoom, useToolbar, useMentions, useMobileEditor } from "@/hooks";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor-file-asset.helper";
|
||||
import { TEditorParams, TEditorVariant } from "@/types/editor";
|
||||
import { callNative } from "@/helpers";
|
||||
import { TrailingNode } from "@/extensions/trailing-node";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
export const EditorWrapper = ({ variant }: { variant: TEditorVariant }) => {
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
const [initialParams, setInitialParams] = useState<TEditorParams | undefined>();
|
||||
// It is a custom hook that disables zooming in the editor.
|
||||
useDisableZoom();
|
||||
// It keeps the native toolbar in sync with the editor state.
|
||||
const { updateActiveStates } = useToolbar(editorRef);
|
||||
|
||||
const { handleEditorReady, onEditorFocus } = useMobileEditor(editorRef);
|
||||
|
||||
const { mentionSuggestionsRef, mentionHighlightsRef } = useMentions();
|
||||
|
||||
const fileHandler = useMemo(
|
||||
() =>
|
||||
getEditorFileHandlers({
|
||||
workspaceSlug: initialParams?.workspaceSlug ?? "",
|
||||
workspaceId: initialParams?.workspaceId ?? "",
|
||||
projectId: initialParams?.projectId ?? "",
|
||||
baseApi: initialParams?.baseApi ?? "",
|
||||
}),
|
||||
[initialParams?.workspaceSlug, initialParams?.workspaceId, initialParams?.projectId, initialParams?.baseApi]
|
||||
);
|
||||
|
||||
// This is called by the native code to reset the initial params of the editor.
|
||||
const resetInitialParams = useCallback((params: TEditorParams) => {
|
||||
setInitialParams(params);
|
||||
}, []);
|
||||
|
||||
// This is called when the editor is ready to get the initial params from the native code.
|
||||
useEffect(() => {
|
||||
callNative(CallbackHandlerStrings.getInitialEditorParams).then((params: TEditorParams) => setInitialParams(params));
|
||||
}, []);
|
||||
|
||||
// Disabled extensions for the editor.
|
||||
const disabledExtensions: TExtensions[] = useMemo(() => ["enter-key", "slash-commands"], []);
|
||||
|
||||
// Additional extensions for the editor.
|
||||
const externalExtensions = useMemo(() => [TrailingNode], []);
|
||||
|
||||
const mentionHandler = useMemo(
|
||||
() => ({
|
||||
suggestions: () => Promise.resolve(mentionSuggestionsRef.current),
|
||||
highlights: () => Promise.resolve(mentionHighlightsRef.current),
|
||||
}),
|
||||
[mentionSuggestionsRef.current, mentionHighlightsRef.current]
|
||||
);
|
||||
|
||||
window.resetInitialParams = resetInitialParams;
|
||||
|
||||
if (!initialParams) return null;
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hidden h-screen" onClick={onEditorFocus}>
|
||||
{variant === TEditorVariant.lite && (
|
||||
<LiteTextEditorWithRef
|
||||
ref={editorRef}
|
||||
autofocus
|
||||
disabledExtensions={disabledExtensions}
|
||||
extensions={externalExtensions}
|
||||
placeholder={initialParams?.placeholder}
|
||||
onTransaction={updateActiveStates}
|
||||
handleEditorReady={handleEditorReady}
|
||||
editorClassName="min-h-screen pb-32"
|
||||
containerClassName="p-0 border-none"
|
||||
mentionHandler={mentionHandler as any}
|
||||
fileHandler={fileHandler}
|
||||
initialValue={initialParams?.content ?? "<p></p>"}
|
||||
id="lite-editor"
|
||||
/>
|
||||
)}
|
||||
{variant === TEditorVariant.rich && (
|
||||
<RichTextEditorWithRef
|
||||
ref={editorRef}
|
||||
extensions={externalExtensions}
|
||||
placeholder={initialParams?.placeholder}
|
||||
editorClassName="min-h-screen pb-32"
|
||||
containerClassName="p-0 border-none"
|
||||
bubbleMenuEnabled={false}
|
||||
disabledExtensions={disabledExtensions}
|
||||
mentionHandler={mentionHandler as any}
|
||||
onTransaction={updateActiveStates}
|
||||
handleEditorReady={handleEditorReady}
|
||||
fileHandler={fileHandler}
|
||||
initialValue={initialParams?.content ?? "<p></p>"}
|
||||
id="rich-editor"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
packages/mobile-editor/src/components/editor/index.ts
Normal file
1
packages/mobile-editor/src/components/editor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./editor-wrapper";
|
||||
3
packages/mobile-editor/src/components/index.ts
Normal file
3
packages/mobile-editor/src/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./document-editor";
|
||||
export * from "./issue-embed";
|
||||
export * from "./document-editor";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./issue-embed";
|
||||
export * from "./issue-embed-upgrade";
|
||||
export * from "./issue-identifier";
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Button } from "@plane/ui";
|
||||
import { Crown } from "lucide-react";
|
||||
|
||||
export const IssueEmbedUpgradeCard = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-min cursor-pointer space-y-2.5 rounded-lg bg-[#F3F4F7] border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs"
|
||||
}
|
||||
>
|
||||
<div className="relative h-[71%]">
|
||||
<div className="h-full backdrop-filter backdrop-blur-[30px] bg-opacity-30 flex items-center w-full justify-between gap-5 pl-4 pr-5 py-3 max-md:max-w-full max-md:flex-wrap relative">
|
||||
<div className="flex-col items-center">
|
||||
<div className="rounded p-2 bg-white w-min mb-3">
|
||||
<Crown size={16} color="#FFBA18" />
|
||||
</div>
|
||||
<div className="text-custom-text text-base">
|
||||
Embed and access issues in pages seamlessly, upgrade to plane pro now.
|
||||
</div>
|
||||
</div>
|
||||
<Button className="py-2" variant="primary" onClick={() => {}}>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// types
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_PROPERTIES } from "@/constants/issue";
|
||||
// components
|
||||
import { IssueIdentifier } from "@/components/issue-embed/issue-identifier";
|
||||
import { TIssue } from "@/types/issue";
|
||||
import { callNative } from "@/helpers";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
projectId?: string;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
export const IssueEmbedCard: React.FC<Props> = (props) => {
|
||||
const { issueId, projectId, workspaceSlug } = props;
|
||||
|
||||
// states
|
||||
const [issueDetails, setIssueDetails] = useState<TIssue | undefined>(undefined);
|
||||
const [error, setError] = useState<any | null>(null);
|
||||
|
||||
// issue display properties
|
||||
const displayProperties: IIssueDisplayProperties = {};
|
||||
|
||||
ISSUE_DISPLAY_PROPERTIES.forEach((property) => {
|
||||
displayProperties[property.key] = true;
|
||||
});
|
||||
|
||||
// get the issue details from the native code.
|
||||
useEffect(() => {
|
||||
if (!issueDetails) {
|
||||
callNative(
|
||||
CallbackHandlerStrings.getIssueDetails,
|
||||
JSON.stringify({
|
||||
issueId,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
})
|
||||
).then((issue: string) => setIssueDetails(JSON.parse(issue)));
|
||||
}
|
||||
}, [issueDetails, issueId, projectId, workspaceSlug]);
|
||||
|
||||
if (!issueDetails && !error)
|
||||
return (
|
||||
<div className="rounded-md my-4">
|
||||
<Loader>
|
||||
<Loader.Item height="30px" />
|
||||
<div className="mt-3 space-y-2">
|
||||
<Loader.Item height="20px" width="70%" />
|
||||
<Loader.Item height="20px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-md border-2 border-orange-500 bg-orange-500/10 text-orange-500 py-3 my-2 text-base">
|
||||
<AlertTriangle className="text-orange-500 size-8" />
|
||||
This Issue embed is not found in any project. It can no longer be updated or accessed from here.
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="issue-embed cursor-pointer space-y-2 rounded-lg border border-custom-border-300 shadow-custom-shadow-2xs p-3 px-4 my-2">
|
||||
<IssueIdentifier
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueIdentifier={issueDetails?.sequenceId?.toString() ?? ""}
|
||||
/>
|
||||
<h4 className="!text-lg !font-medium !mt-2 line-clamp-2 break-words">{issueDetails?.name}</h4>
|
||||
{/*issueDetails && (
|
||||
<IssueProperties
|
||||
className="flex flex-wrap items-center gap-2 whitespace-nowrap text-custom-text-300 pt-1.5"
|
||||
issue={issueDetails}
|
||||
displayProperties={displayProperties}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Loader } from "@plane/ui";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { callNative } from "@/helpers/flutter-callback.helper";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
type Props = {
|
||||
issueIdentifier: string;
|
||||
projectId?: string;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
export const IssueIdentifier: React.FC<Props> = (props) => {
|
||||
const { projectId, workspaceSlug, issueIdentifier } = props;
|
||||
const [projectIdentifier, setProjectIdentifier] = useState<String | undefined>(undefined);
|
||||
|
||||
// get the project identifier from the native code.
|
||||
useEffect(() => {
|
||||
if (!projectIdentifier) {
|
||||
callNative(
|
||||
CallbackHandlerStrings.getProjectIdentifier,
|
||||
JSON.stringify({
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
})
|
||||
).then((identifier: string) => setProjectIdentifier(identifier));
|
||||
}
|
||||
}, [projectId, workspaceSlug]);
|
||||
|
||||
if (!projectIdentifier)
|
||||
return (
|
||||
<Loader className="flex flex-shrink-0 w-20 h-5">
|
||||
<Loader.Item height="100%" width="100%" />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={"text-sm font-medium text-custom-text-300"}>{`${projectIdentifier}-${issueIdentifier}`}</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export class CallbackHandlerStrings {
|
||||
// Utility callbacks
|
||||
static readonly getActiveToolbarState = "getActiveToolbarState";
|
||||
static readonly onEditorFocused = "onEditorFocused";
|
||||
static readonly onEditorReady = "onEditorReady";
|
||||
static readonly getInitialEditorParams = "getInitialEditorParams";
|
||||
static readonly getInitialDocumentEditorParams = "getInitialDocumentEditorParams";
|
||||
// Mention callbacks
|
||||
static readonly getMembers = "getMembers";
|
||||
static readonly getUserId = "getUserId";
|
||||
// Feature flag callbacks
|
||||
static readonly getFeatureFlags = "getFeatureFlags";
|
||||
// Asset callbacks
|
||||
static readonly getResolvedImageUrl = "getResolvedImageUrl";
|
||||
static readonly deleteImage = "deleteImage";
|
||||
static readonly restoreImage = "restoreImage";
|
||||
// Issue embed callbacks
|
||||
static readonly getProjectIdentifier = "getProjectIdentifier";
|
||||
static readonly getIssueDetails = "getIssueDetails";
|
||||
}
|
||||
134
packages/mobile-editor/src/constants/editor.ts
Normal file
134
packages/mobile-editor/src/constants/editor.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Bold,
|
||||
CaseSensitive,
|
||||
Code2,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Heading5,
|
||||
Heading6,
|
||||
Image,
|
||||
Italic,
|
||||
List,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
LucideIcon,
|
||||
Quote,
|
||||
Strikethrough,
|
||||
Table,
|
||||
Underline,
|
||||
} from "lucide-react";
|
||||
// editor
|
||||
import { TEditorCommands, TEditorFontStyle } from "@plane/editor";
|
||||
// ui
|
||||
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
|
||||
|
||||
type TEditorTypes = "lite" | "document";
|
||||
|
||||
export type ToolbarMenuItem = {
|
||||
key: TEditorCommands;
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
shortcut?: string[];
|
||||
editors: TEditorTypes[];
|
||||
};
|
||||
|
||||
export const TYPOGRAPHY_ITEMS: ToolbarMenuItem[] = [
|
||||
{ key: "text", name: "Text", icon: CaseSensitive, editors: ["document"] },
|
||||
{ key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] },
|
||||
{ key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] },
|
||||
{ key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] },
|
||||
{ key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] },
|
||||
{ key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] },
|
||||
{ key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] },
|
||||
];
|
||||
|
||||
const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [
|
||||
{ key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] },
|
||||
{ key: "italic", name: "Italic", icon: Italic, shortcut: ["Cmd", "I"], editors: ["lite", "document"] },
|
||||
{ key: "underline", name: "Underline", icon: Underline, shortcut: ["Cmd", "U"], editors: ["lite", "document"] },
|
||||
{
|
||||
key: "strike",
|
||||
name: "Strikethrough",
|
||||
icon: Strikethrough,
|
||||
shortcut: ["Cmd", "Shift", "S"],
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
];
|
||||
|
||||
const LIST_ITEMS: ToolbarMenuItem[] = [
|
||||
{
|
||||
key: "bulleted-list",
|
||||
name: "Bulleted list",
|
||||
icon: List,
|
||||
shortcut: ["Cmd", "Shift", "7"],
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
{
|
||||
key: "numbered-list",
|
||||
name: "Numbered list",
|
||||
icon: ListOrdered,
|
||||
shortcut: ["Cmd", "Shift", "8"],
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
{
|
||||
key: "to-do-list",
|
||||
name: "To-do list",
|
||||
icon: ListTodo,
|
||||
shortcut: ["Cmd", "Shift", "9"],
|
||||
editors: ["lite", "document"],
|
||||
},
|
||||
];
|
||||
|
||||
const USER_ACTION_ITEMS: ToolbarMenuItem[] = [
|
||||
{ key: "quote", name: "Quote", icon: Quote, editors: ["lite", "document"] },
|
||||
{ key: "code", name: "Code", icon: Code2, editors: ["lite", "document"] },
|
||||
];
|
||||
|
||||
const COMPLEX_ITEMS: ToolbarMenuItem[] = [
|
||||
{ key: "table", name: "Table", icon: Table, editors: ["document"] },
|
||||
{ key: "image", name: "Image", icon: Image, editors: ["lite", "document"] },
|
||||
];
|
||||
|
||||
export const TOOLBAR_ITEMS: {
|
||||
[editorType in TEditorTypes]: {
|
||||
[key: string]: ToolbarMenuItem[];
|
||||
};
|
||||
} = {
|
||||
lite: {
|
||||
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("lite")),
|
||||
list: LIST_ITEMS.filter((item) => item.editors.includes("lite")),
|
||||
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("lite")),
|
||||
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("lite")),
|
||||
},
|
||||
document: {
|
||||
basic: BASIC_MARK_ITEMS.filter((item) => item.editors.includes("document")),
|
||||
list: LIST_ITEMS.filter((item) => item.editors.includes("document")),
|
||||
userAction: USER_ACTION_ITEMS.filter((item) => item.editors.includes("document")),
|
||||
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")),
|
||||
},
|
||||
};
|
||||
|
||||
export const EDITOR_FONT_STYLES: {
|
||||
key: TEditorFontStyle;
|
||||
label: string;
|
||||
icon: any;
|
||||
}[] = [
|
||||
{
|
||||
key: "sans-serif",
|
||||
label: "Sans serif",
|
||||
icon: SansSerifIcon,
|
||||
},
|
||||
{
|
||||
key: "serif",
|
||||
label: "Serif",
|
||||
icon: SerifIcon,
|
||||
},
|
||||
{
|
||||
key: "monospace",
|
||||
label: "Mono",
|
||||
icon: MonospaceIcon,
|
||||
},
|
||||
];
|
||||
|
||||
38
packages/mobile-editor/src/constants/issue.ts
Normal file
38
packages/mobile-editor/src/constants/issue.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface IIssueDisplayProperties {
|
||||
assignee?: boolean;
|
||||
start_date?: boolean;
|
||||
due_date?: boolean;
|
||||
labels?: boolean;
|
||||
key?: boolean;
|
||||
priority?: boolean;
|
||||
state?: boolean;
|
||||
sub_issue_count?: boolean;
|
||||
link?: boolean;
|
||||
attachment_count?: boolean;
|
||||
estimate?: boolean;
|
||||
created_on?: boolean;
|
||||
updated_on?: boolean;
|
||||
modules?: boolean;
|
||||
cycle?: boolean;
|
||||
issue_type?: boolean;
|
||||
}
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
key: keyof IIssueDisplayProperties;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ key: "key", title: "ID" },
|
||||
{ key: "issue_type", title: "Issue Type" },
|
||||
{ key: "assignee", title: "Assignee" },
|
||||
{ key: "start_date", title: "Start date" },
|
||||
{ key: "due_date", title: "Due date" },
|
||||
{ key: "labels", title: "Labels" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "state", title: "State" },
|
||||
{ key: "sub_issue_count", title: "Sub issue count" },
|
||||
{ key: "attachment_count", title: "Attachment count" },
|
||||
{ key: "link", title: "Link" },
|
||||
{ key: "estimate", title: "Estimate" },
|
||||
{ key: "modules", title: "Modules" },
|
||||
{ key: "cycle", title: "Cycle" },
|
||||
];
|
||||
65
packages/mobile-editor/src/extensions/trailing-node.ts
Normal file
65
packages/mobile-editor/src/extensions/trailing-node.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
// @ts-expect-error - TODO: Remove this when the types are fixed
|
||||
function nodeEqualsType({ types, node }) {
|
||||
return (Array.isArray(types) && types.includes(node.type)) || node.type === types;
|
||||
}
|
||||
|
||||
export interface TrailingNodeOptions {
|
||||
node: string;
|
||||
notAfter: string[];
|
||||
}
|
||||
|
||||
export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
||||
name: "trailingNode",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
node: "paragraph",
|
||||
notAfter: ["paragraph"],
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const plugin = new PluginKey(this.name);
|
||||
const disabledNodes = Object.entries(this.editor.schema.nodes)
|
||||
.map(([, value]) => value)
|
||||
.filter((node) => this.options.notAfter.includes(node.name));
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: plugin,
|
||||
appendTransaction: (_, __, state) => {
|
||||
const { doc, tr, schema } = state;
|
||||
const shouldInsertNodeAtEnd = plugin.getState(state);
|
||||
const endPosition = doc.content.size;
|
||||
const type = schema.nodes[this.options.node];
|
||||
|
||||
if (!shouldInsertNodeAtEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return tr.insert(endPosition, type.create());
|
||||
},
|
||||
state: {
|
||||
init: (_, state) => {
|
||||
const lastNode = state.tr.doc.lastChild;
|
||||
|
||||
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
||||
},
|
||||
apply: (tr, value) => {
|
||||
if (!tr.docChanged) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const lastNode = tr.doc.lastChild;
|
||||
|
||||
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
3
packages/mobile-editor/src/helpers/common.helper.ts
Normal file
3
packages/mobile-editor/src/helpers/common.helper.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || "";
|
||||
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "";
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
|
||||
146
packages/mobile-editor/src/helpers/editor-file-asset.helper.ts
Normal file
146
packages/mobile-editor/src/helpers/editor-file-asset.helper.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
import { checkURLValidity, callNative } from "@/helpers";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
type TEditorSrcArgs = {
|
||||
assetId: string;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
baseApi: string;
|
||||
};
|
||||
|
||||
type TURLArgs = {
|
||||
src: string;
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
baseApi: string;
|
||||
};
|
||||
|
||||
type TEditorFileHandlerArgs = {
|
||||
projectId?: string;
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
baseApi: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description combines the file path with the base URL
|
||||
* @param {string} path
|
||||
* @returns {string} final URL with the base URL
|
||||
*/
|
||||
export const getFileURL = async (path: string, baseApi: string) => {
|
||||
if (!path) return undefined;
|
||||
const isValidURL = path.startsWith("http");
|
||||
if (isValidURL) return path;
|
||||
|
||||
const url = await callNative(CallbackHandlerStrings.getResolvedImageUrl, `${baseApi}${path}`);
|
||||
return isValidURL ? path : url;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description generates the file source using assetId
|
||||
* @param {TEditorSrcArgs} args
|
||||
*/
|
||||
export const getEditorAssetSrc = async (args: TEditorSrcArgs) => {
|
||||
const { assetId, projectId, workspaceSlug } = args;
|
||||
let url: string | undefined = "";
|
||||
if (projectId) {
|
||||
url = await getFileURL(
|
||||
`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${assetId}/`,
|
||||
args.baseApi
|
||||
);
|
||||
} else {
|
||||
url = await getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/${assetId}/`, args.baseApi);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description generate the restore file URL
|
||||
* @param {TURLArgs} args
|
||||
* @returns {string} restore URL
|
||||
*/
|
||||
export const getRestoreURL = (args: TURLArgs) => {
|
||||
const { baseApi, src, workspaceId, workspaceSlug } = args;
|
||||
let url: string | undefined;
|
||||
if (checkURLValidity(src)) {
|
||||
const assetKey = src.split("/").pop();
|
||||
return `${baseApi}/api/workspaces/file-assets/${workspaceId}/${assetKey}`;
|
||||
}
|
||||
url = `${args.baseApi}/api/assets/v2/workspaces/${workspaceSlug}/restore/${src}/`;
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description generate the delete file URL
|
||||
* @param {TURLArgs} args
|
||||
*/
|
||||
export const getDeleteURL = (args: TURLArgs) => {
|
||||
const { baseApi, src, projectId, workspaceId, workspaceSlug } = args;
|
||||
let url: string | undefined;
|
||||
if (checkURLValidity(src)) {
|
||||
const assetKey = src.split("/").pop();
|
||||
return `${baseApi}/api/workspaces/file-assets/${workspaceId}/${assetKey}`;
|
||||
}
|
||||
if (projectId) {
|
||||
url = `${args.baseApi}/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${src}/`;
|
||||
} else {
|
||||
url = `${args.baseApi}/api/assets/v2/workspaces/${workspaceSlug}/${src}/`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the editor.
|
||||
* @param {TEditorFileHandlerArgs} args
|
||||
*/
|
||||
export const getEditorFileHandlers = (args: TEditorFileHandlerArgs): TFileHandler => {
|
||||
const { projectId, workspaceSlug, workspaceId, baseApi } = args;
|
||||
return {
|
||||
assetsUploadStatus: {},
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
(await getEditorAssetSrc({
|
||||
assetId: path,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
baseApi,
|
||||
})) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
upload: async (_, file: File) => Promise.resolve(""),
|
||||
delete: async (src: string) => {
|
||||
const url = getDeleteURL({
|
||||
src,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
workspaceId,
|
||||
baseApi,
|
||||
});
|
||||
await callNative(CallbackHandlerStrings.deleteImage, url);
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
const url = getRestoreURL({
|
||||
src,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
workspaceId,
|
||||
baseApi,
|
||||
});
|
||||
await callNative(CallbackHandlerStrings.restoreImage, url);
|
||||
},
|
||||
cancel: () => {},
|
||||
validation: {
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
@@ -0,0 +1,2 @@
|
||||
export const callNative = async (method: string, args?: string) =>
|
||||
await window.flutter_inappwebview?.callHandler(method, args);
|
||||
5
packages/mobile-editor/src/helpers/index.ts
Normal file
5
packages/mobile-editor/src/helpers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./mentions.helper";
|
||||
export * from "./flutter-callback.helper";
|
||||
export * from "./editor-file-asset.helper";
|
||||
export * from "./string.helper";
|
||||
export * from "./common.helper";
|
||||
13
packages/mobile-editor/src/helpers/mentions.helper.ts
Normal file
13
packages/mobile-editor/src/helpers/mentions.helper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const transformMentionSuggestions = (projectMembers: any): any[] =>
|
||||
projectMembers.map((member: any) => ({
|
||||
entity_name: "user_mention",
|
||||
entity_identifier: `${member?.id}`,
|
||||
id: `${member?.id}`,
|
||||
type: "User",
|
||||
title: `${member?.displayName}`,
|
||||
subtitle: member?.email ?? "",
|
||||
avatar: member?.avatarUrl,
|
||||
redirect_uri: "",
|
||||
}));
|
||||
|
||||
export const transformMentionHighlights = (userId: any) => [userId];
|
||||
31
packages/mobile-editor/src/helpers/string.helper.ts
Normal file
31
packages/mobile-editor/src/helpers/string.helper.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const generateRandomColor = (string: string): string => {
|
||||
if (!string) return "rgb(var(--color-primary-100))";
|
||||
|
||||
string = `${string}`;
|
||||
|
||||
const uniqueId = string.length.toString() + string; // Unique identifier based on string length
|
||||
const combinedString = uniqueId + string;
|
||||
|
||||
const hash = Array.from(combinedString).reduce((acc, char) => {
|
||||
const charCode = char.charCodeAt(0);
|
||||
return (acc << 5) - acc + charCode;
|
||||
}, 0);
|
||||
|
||||
const hue = hash % 360;
|
||||
const saturation = 70; // Higher saturation for pastel colors
|
||||
const lightness = 60; // Mid-range lightness for pastel colors
|
||||
|
||||
const randomColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||
|
||||
return randomColor;
|
||||
};
|
||||
|
||||
export const checkURLValidity = (url: string): boolean => {
|
||||
if (!url) return false;
|
||||
|
||||
// regex to support complex query parameters and fragments
|
||||
const urlPattern =
|
||||
/^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i;
|
||||
|
||||
return urlPattern.test(url);
|
||||
};
|
||||
6
packages/mobile-editor/src/hooks/index.ts
Normal file
6
packages/mobile-editor/src/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./use-mentions";
|
||||
export * from "./use-mobile-editor";
|
||||
export * from "./use-toolbar";
|
||||
export * from "./use-disable-zoom";
|
||||
export * from "./use-editor-flagging";
|
||||
|
||||
27
packages/mobile-editor/src/hooks/use-disable-zoom.ts
Normal file
27
packages/mobile-editor/src/hooks/use-disable-zoom.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useDisableZoom = () => {
|
||||
useEffect(() => {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === "+" || e.key === "-" || e.key === "=")) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("wheel", handleWheel, { passive: false });
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("wheel", handleWheel);
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
36
packages/mobile-editor/src/hooks/use-editor-flagging.ts
Normal file
36
packages/mobile-editor/src/hooks/use-editor-flagging.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
import { callNative } from "@/helpers/flutter-callback.helper";
|
||||
import { TFeatureFlagsResponse } from "@/types/feature-flag";
|
||||
import { TExtensions } from "@plane/editor";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* @description extensions disabled in various editors
|
||||
*/
|
||||
export const useEditorFlagging = (): {
|
||||
disabledExtensions?: TExtensions[];
|
||||
isIssueEmbedEnabled: boolean;
|
||||
} => {
|
||||
const [featureFlags, setFeatureFlags] = useState<TFeatureFlagsResponse | null>(null);
|
||||
const [disabledExtensions, setDocumentEditor] = useState<TExtensions[] | undefined>(undefined);
|
||||
|
||||
// get the feature flags from the native code
|
||||
useEffect(() => {
|
||||
callNative(CallbackHandlerStrings.getFeatureFlags).then((flags: string) => {
|
||||
console.log(flags);
|
||||
setFeatureFlags(JSON.parse(flags));
|
||||
setDocumentEditor([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isIssueEmbedEnabled = featureFlags?.pageIssueEmbeds ?? false;
|
||||
const isCollaborationCursorEnabled = featureFlags?.collaborationCursor ?? false;
|
||||
|
||||
// extensions disabled in the document editor
|
||||
if (!isIssueEmbedEnabled) disabledExtensions?.push("issue-embed");
|
||||
if (!isCollaborationCursorEnabled) disabledExtensions?.push("collaboration-cursor");
|
||||
return {
|
||||
disabledExtensions,
|
||||
isIssueEmbedEnabled,
|
||||
};
|
||||
};
|
||||
48
packages/mobile-editor/src/hooks/use-mentions.ts
Normal file
48
packages/mobile-editor/src/hooks/use-mentions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { transformMentionSuggestions } from "@/helpers/mentions.helper";
|
||||
import { callNative } from "@/helpers/flutter-callback.helper";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
export const useMentions = () => {
|
||||
// Mention suggestions and highlights are stored in refs, so they can be updated without re-initializing the editor.
|
||||
const mentionSuggestionsRef = useRef<any[]>([]);
|
||||
const mentionHighlightsRef = useRef<string[]>([]);
|
||||
|
||||
// Get the members from the native code.
|
||||
const getMembers = useCallback(async (): Promise<any[]> => {
|
||||
await callNative(CallbackHandlerStrings.getMembers).then((members: any) => {
|
||||
const mentionSuggestions = transformMentionSuggestions(members);
|
||||
mentionSuggestionsRef.current = mentionSuggestions;
|
||||
return mentionSuggestions;
|
||||
});
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
// Set the members in the mention
|
||||
const setMembers = useCallback((members: any) => {
|
||||
const mentionSuggestions = transformMentionSuggestions(members);
|
||||
mentionSuggestionsRef.current = mentionSuggestions;
|
||||
}, []);
|
||||
|
||||
// Get the userId from the native
|
||||
const getUserId = useCallback(
|
||||
() =>
|
||||
callNative(CallbackHandlerStrings.getUserId).then((userId: string) => (mentionHighlightsRef.current = [userId])),
|
||||
[]
|
||||
);
|
||||
|
||||
window.getMembers = getMembers;
|
||||
window.setMembers = setMembers;
|
||||
window.getUserId = getUserId;
|
||||
|
||||
useEffect(() => {
|
||||
getMembers();
|
||||
getUserId();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
mentionSuggestionsRef,
|
||||
mentionHighlightsRef,
|
||||
};
|
||||
};
|
||||
59
packages/mobile-editor/src/hooks/use-mobile-editor.ts
Normal file
59
packages/mobile-editor/src/hooks/use-mobile-editor.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { EditorRefApi, TEditorCommands } from "@plane/editor";
|
||||
import { callNative } from "@/helpers/flutter-callback.helper";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
export const useMobileEditor = (editorRef: React.MutableRefObject<EditorRefApi | null>) => {
|
||||
/**
|
||||
* @returns the current content of the editor in HTML format.
|
||||
*/
|
||||
const sendHtmlContent = useCallback(() => editorRef.current?.getDocument().html, [editorRef.current]);
|
||||
|
||||
// Notifies the native code that the editor is focused.
|
||||
const onEditorFocus = useCallback(() => callNative(CallbackHandlerStrings.onEditorFocused), []);
|
||||
|
||||
// Executes the action based on the action key.
|
||||
const executeAction = useCallback(
|
||||
(actionKey: TEditorCommands) => {
|
||||
// @ts-expect-error type mismatch here
|
||||
editorRef.current?.executeMenuItemCommand({
|
||||
itemKey: actionKey,
|
||||
});
|
||||
editorRef.current?.scrollToNodeViaDOMCoordinates("instant");
|
||||
},
|
||||
[editorRef.current]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Unfocus the editor.
|
||||
* @usecase This is required to remove the focus from the editor when the user taps outside the editor.
|
||||
*/
|
||||
const unfocus = useCallback(() => editorRef.current?.blur(), [editorRef.current]);
|
||||
|
||||
// Scrolls to the focused node in the editor.
|
||||
const scrollToFocus = useCallback(
|
||||
() => editorRef.current?.scrollToNodeViaDOMCoordinates("instant"),
|
||||
[editorRef.current]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description when the editor is ready, call the native code to notify that the editor is ready.
|
||||
* @param isReady - boolean
|
||||
*/
|
||||
const handleEditorReady = useCallback((isReady: boolean) => {
|
||||
if (isReady) {
|
||||
callNative(CallbackHandlerStrings.onEditorReady);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Expose the functions to the window object.
|
||||
window.unfocus = unfocus;
|
||||
window.scrollToFocus = scrollToFocus;
|
||||
window.executeAction = executeAction;
|
||||
window.sendHtmlContent = sendHtmlContent;
|
||||
|
||||
return {
|
||||
handleEditorReady,
|
||||
onEditorFocus,
|
||||
};
|
||||
};
|
||||
23
packages/mobile-editor/src/hooks/use-query-params.tsx
Normal file
23
packages/mobile-editor/src/hooks/use-query-params.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
type QueryParams<T extends string> = {
|
||||
[key in T]: string;
|
||||
};
|
||||
|
||||
export function useQueryParams<T extends string>(
|
||||
paramNames: Array<T>,
|
||||
): QueryParams<T> {
|
||||
return useMemo(() => {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
const params = {} as QueryParams<T>;
|
||||
paramNames.forEach((name) => {
|
||||
params[name] = urlParams.get(name) ?? "";
|
||||
});
|
||||
|
||||
return params;
|
||||
}, [paramNames]);
|
||||
}
|
||||
|
||||
export default useQueryParams;
|
||||
29
packages/mobile-editor/src/hooks/use-toolbar.ts
Normal file
29
packages/mobile-editor/src/hooks/use-toolbar.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
import { useCallback } from "react";
|
||||
import { TOOLBAR_ITEMS } from "@/constants/editor";
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { callNative } from "@/helpers/flutter-callback.helper";
|
||||
import { CallbackHandlerStrings } from "@/constants/callback-handler-strings";
|
||||
|
||||
export const useToolbar = (editorRef: React.MutableRefObject<EditorRefApi | null>) => {
|
||||
// Notifies the native code to with active toolbar state.
|
||||
const updateActiveStates = useCallback(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const newActiveStates: Record<string, boolean> = {};
|
||||
Object.values(TOOLBAR_ITEMS.document)
|
||||
.flat()
|
||||
.forEach((item) => {
|
||||
newActiveStates[item.key] =
|
||||
// @ts-expect-error type mismatch here
|
||||
editorRef.current?.isMenuItemActive({
|
||||
itemKey: item.key,
|
||||
}) ?? false;
|
||||
});
|
||||
callNative(CallbackHandlerStrings.getActiveToolbarState, JSON.stringify(newActiveStates));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
updateActiveStates,
|
||||
};
|
||||
};
|
||||
5
packages/mobile-editor/src/main.tsx
Normal file
5
packages/mobile-editor/src/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./App.css";
|
||||
import { App } from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
26
packages/mobile-editor/src/types/editor.ts
Normal file
26
packages/mobile-editor/src/types/editor.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
type TBaseEditorParams = {
|
||||
workspaceSlug: string;
|
||||
workspaceId: string;
|
||||
placeholder?: string;
|
||||
baseApi: string;
|
||||
variant: TEditorVariant;
|
||||
};
|
||||
export type TEditorParams = TBaseEditorParams & {
|
||||
content: string;
|
||||
projectId?: string;
|
||||
};
|
||||
export type TDocumentEditorParams = TBaseEditorParams & {
|
||||
pageId: string;
|
||||
documentType: string;
|
||||
projectId?: string;
|
||||
userId: string;
|
||||
userDisplayName: string;
|
||||
cookie: string;
|
||||
liveServerUrl: string;
|
||||
liveServerBasePath: string;
|
||||
};
|
||||
export enum TEditorVariant {
|
||||
lite = "lite",
|
||||
rich = "rich",
|
||||
document = "document",
|
||||
}
|
||||
9
packages/mobile-editor/src/types/feature-flag.ts
Normal file
9
packages/mobile-editor/src/types/feature-flag.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum E_FEATURE_FLAGS {
|
||||
COLLABORATION_CURSOR = "collaborationCursor",
|
||||
EDITOR_AI_OPS = "editorAIOps",
|
||||
PAGE_ISSUE_EMBEDS = "pageIssueEmbeds",
|
||||
}
|
||||
|
||||
export type TFeatureFlagsResponse = {
|
||||
[featureFlag in E_FEATURE_FLAGS]: boolean;
|
||||
};
|
||||
20
packages/mobile-editor/src/types/global_functions.ts
Normal file
20
packages/mobile-editor/src/types/global_functions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TEditorCommands } from "@plane/editor";
|
||||
import { TEditorParams } from "@/types/editor";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
flutter_inappwebview: {
|
||||
callHandler(method: string, args?: string): Promise<any>;
|
||||
};
|
||||
// use-mobile-editor.ts
|
||||
resetInitialParams: (params: TEditorParams) => void;
|
||||
sendHtmlContent: () => string | undefined;
|
||||
executeAction: (actionKey: TEditorCommands) => void;
|
||||
unfocus: () => void;
|
||||
scrollToFocus: () => void;
|
||||
// use-mentions.ts
|
||||
getMembers: () => Promise<any[]>;
|
||||
setMembers: (members: any) => void;
|
||||
getUserId: () => void;
|
||||
}
|
||||
}
|
||||
36
packages/mobile-editor/src/types/issue.ts
Normal file
36
packages/mobile-editor/src/types/issue.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
|
||||
export type TIssue = {
|
||||
id: string;
|
||||
sequenceId: number;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
|
||||
state_id: string | null;
|
||||
priority: TIssuePriorities | null;
|
||||
label_ids: string[];
|
||||
assignee_ids: string[];
|
||||
estimate_point: string | null;
|
||||
|
||||
sub_issues_count: number;
|
||||
attachment_count: number;
|
||||
link_count: number;
|
||||
|
||||
project_id: string | null;
|
||||
parent_id: string | null;
|
||||
cycle_id: string | null;
|
||||
module_ids: string[] | null;
|
||||
type_id: string | null;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
start_date: string | null;
|
||||
target_date: string | null;
|
||||
completed_at: string | null;
|
||||
archived_at: string | null;
|
||||
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
|
||||
is_draft: boolean;
|
||||
};
|
||||
299
packages/mobile-editor/tailwind.config.js
Normal file
299
packages/mobile-editor/tailwind.config.js
Normal file
@@ -0,0 +1,299 @@
|
||||
const convertToRGB = (variableName) => `rgba(var(${variableName}))`;
|
||||
const convertToRGBA = (variableName, alpha) => `rgba(var(${variableName}), ${alpha})`;
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: {
|
||||
relative: true,
|
||||
files: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./core/**/*.{js,ts,jsx,tsx}",
|
||||
"./ce/**/*.{js,ts,jsx,tsx}",
|
||||
"./ee/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.tsx",
|
||||
"./constants/**/*.{js,ts,jsx,tsx}",
|
||||
"./layouts/**/*.tsx",
|
||||
"./pages/**/*.tsx",
|
||||
"./app/**/*.tsx",
|
||||
"./ui/**/*.tsx",
|
||||
"./src/**/*.tsx",
|
||||
"./**/*.tsx",
|
||||
"../packages/mobile-editor/**/*.{js,ts,jsx,tsx}",
|
||||
"../packages/ui/**/*.{js,ts,jsx,tsx}",
|
||||
"../packages/editor/**/src/**/*.{js,ts,jsx,tsx}",
|
||||
"!../packages/ui/**/*.stories{js,ts,jsx,tsx}",
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||
"custom-shadow-md": "var(--color-shadow-md)",
|
||||
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||
"custom-shadow-4xl": "var(--color-shadow-4xl)",
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
primary: {
|
||||
100: convertToRGB("--color-primary-100"),
|
||||
200: convertToRGB("--color-primary-200"),
|
||||
300: convertToRGB("--color-primary-300"),
|
||||
DEFAULT: convertToRGB("--color-primary-100"),
|
||||
},
|
||||
background: {
|
||||
|
||||
100: convertToRGB("--color-background-100"),
|
||||
200: convertToRGB("--color-background-200"),
|
||||
300: convertToRGB("--color-background-300"),
|
||||
400: convertToRGB("--color-background-400"),
|
||||
overlay: convertToRGBA("--color-background-80", 0.95),
|
||||
primary: convertToRGB(" --color-background-primary"),
|
||||
error: convertToRGB(" --color-background-error"),
|
||||
DEFAULT: convertToRGB("--color-background-100"),
|
||||
},
|
||||
text: {
|
||||
100: convertToRGB("--color-text-100"),
|
||||
200: convertToRGB("--color-text-200"),
|
||||
300: convertToRGB("--color-text-300"),
|
||||
350: convertToRGB("--color-text-350"),
|
||||
400: convertToRGB("--color-text-400"),
|
||||
primary: convertToRGB("--color-text-primary"),
|
||||
error: convertToRGB("--color-text-error"),
|
||||
DEFAULT: convertToRGB("--color-text-100"),
|
||||
},
|
||||
border: {
|
||||
0: "rgb(255, 255, 255)",
|
||||
100: convertToRGB("--color-border-100"),
|
||||
200: convertToRGB("--color-border-200"),
|
||||
300: convertToRGB("--color-border-300"),
|
||||
400: convertToRGB("--color-border-400"),
|
||||
1000: "rgb(0, 0, 0)",
|
||||
primary: convertToRGB("--color-border-primary"),
|
||||
error: convertToRGB("--color-border-error"),
|
||||
DEFAULT: convertToRGB("--color-border-200"),
|
||||
},
|
||||
error: {
|
||||
10: convertToRGB("--color-error-10"),
|
||||
20: convertToRGB("--color-error-20"),
|
||||
30: convertToRGB("--color-error-30"),
|
||||
100: convertToRGB("--color-error-100"),
|
||||
200: convertToRGB("--color-error-200"),
|
||||
500: convertToRGB("--color-error-500"),
|
||||
},
|
||||
|
||||
backdrop: "rgba(0, 0, 0, 0.25)",
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
leftToaster: {
|
||||
"0%": { left: "-20rem" },
|
||||
"100%": { left: "0" },
|
||||
},
|
||||
rightToaster: {
|
||||
"0%": { right: "-20rem" },
|
||||
"100%": { right: "0" },
|
||||
},
|
||||
"bar-loader": {
|
||||
from: { left: "-100%" },
|
||||
to: { left: "100%" },
|
||||
},
|
||||
},
|
||||
typography: () => ({
|
||||
brand: {
|
||||
css: {
|
||||
"--tw-prose-body": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-p": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-headings": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-lead": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-links": convertToRGB("--color-primary-100"),
|
||||
"--tw-prose-bold": "inherit",
|
||||
"--tw-prose-counters": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-bullets": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-hr": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quotes": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-quote-borders": convertToRGB("--color-border-200"),
|
||||
"--tw-prose-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-code": convertToRGB("--color-text-100"),
|
||||
"--tw-prose-pre-bg": convertToRGB("--color-background-100"),
|
||||
"--tw-prose-th-borders": convertToRGB("--color-border-200"),
|
||||
"--tw-prose-td-borders": convertToRGB("--color-border-200"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
screens: {
|
||||
"3xl": "1792px",
|
||||
},
|
||||
// scale down font sizes to 90% of default
|
||||
fontSize: {
|
||||
xs: "0.675rem",
|
||||
sm: "0.7875rem",
|
||||
base: "0.9rem",
|
||||
lg: "1.0125rem",
|
||||
xl: "1.125rem",
|
||||
"2xl": "1.35rem",
|
||||
"3xl": "1.6875rem",
|
||||
"4xl": "2.25rem",
|
||||
"5xl": "2.7rem",
|
||||
"6xl": "3.375rem",
|
||||
"7xl": "4.05rem",
|
||||
"8xl": "5.4rem",
|
||||
"9xl": "7.2rem",
|
||||
},
|
||||
// scale down spacing to 90% of default
|
||||
padding: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
"page-x": "1.35rem",
|
||||
"page-y": "1.35rem",
|
||||
},
|
||||
margin: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
space: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
gap: {
|
||||
0: "0",
|
||||
0.5: "0.1125rem",
|
||||
1: "0.225rem",
|
||||
1.5: "0.3375rem",
|
||||
2: "0.45rem",
|
||||
2.5: "0.5625rem",
|
||||
3: "0.675rem",
|
||||
3.5: "0.7875rem",
|
||||
4: "0.9rem",
|
||||
5: "1.125rem",
|
||||
6: "1.35rem",
|
||||
7: "1.575rem",
|
||||
8: "1.8rem",
|
||||
9: "2.025rem",
|
||||
10: "2.25rem",
|
||||
11: "2.475rem",
|
||||
12: "2.7rem",
|
||||
16: "3.6rem",
|
||||
20: "4.5rem",
|
||||
24: "5.4rem",
|
||||
32: "7.2rem",
|
||||
40: "9rem",
|
||||
48: "10.8rem",
|
||||
56: "12.6rem",
|
||||
64: "14.4rem",
|
||||
72: "16.2rem",
|
||||
80: "18rem",
|
||||
96: "21.6rem",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
custom: ["Inter", "sans-serif"],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// require("tailwindcss-animate"),
|
||||
// require("@tailwindcss/typography"),
|
||||
function ({ addUtilities }) {
|
||||
const newUtilities = {
|
||||
// Mobile screens
|
||||
".px-page-x": {
|
||||
paddingLeft: "0.675rem",
|
||||
paddingRight: "0.675rem",
|
||||
},
|
||||
// Medium screens (768px and up)
|
||||
"@media (min-width: 768px)": {
|
||||
".px-page-x": {
|
||||
paddingLeft: "1.35rem",
|
||||
paddingRight: "1.35rem",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
addUtilities(newUtilities, ["responsive"]);
|
||||
},
|
||||
],
|
||||
};
|
||||
33
packages/mobile-editor/tsconfig.app.json
Normal file
33
packages/mobile-editor/tsconfig.app.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"module": "preserve",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"types"
|
||||
]
|
||||
}
|
||||
1
packages/mobile-editor/tsconfig.app.tsbuildinfo
Normal file
1
packages/mobile-editor/tsconfig.app.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/components/index.ts","./src/components/document-editor/document-editor.tsx","./src/components/document-editor/index.ts","./src/components/document-editor/page-content-loader.tsx","./src/components/editor/editor-wrapper.tsx","./src/components/editor/index.ts","./src/components/issue-embed/index.ts","./src/components/issue-embed/issue-embed-upgrade.tsx","./src/components/issue-embed/issue-embed.tsx","./src/components/issue-embed/issue-identifier.tsx","./src/constants/callback-handler-strings.ts","./src/constants/editor.ts","./src/constants/issue.ts","./src/extensions/trailing-node.ts","./src/helpers/common.helper.ts","./src/helpers/editor-file-asset.helper.ts","./src/helpers/flutter-callback.helper.ts","./src/helpers/index.ts","./src/helpers/mentions.helper.ts","./src/helpers/string.helper.ts","./src/hooks/index.ts","./src/hooks/use-disable-zoom.ts","./src/hooks/use-editor-flagging.ts","./src/hooks/use-mentions.ts","./src/hooks/use-mobile-editor.ts","./src/hooks/use-query-params.tsx","./src/hooks/use-toolbar.ts","./src/types/editor.ts","./src/types/feature-flag.ts","./src/types/global_functions.ts","./src/types/issue.ts"],"version":"5.7.3"}
|
||||
8
packages/mobile-editor/tsconfig.json
Normal file
8
packages/mobile-editor/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
packages/mobile-editor/vite.config.js
Normal file
27
packages/mobile-editor/vite.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "tailwindcss";
|
||||
import path from "path";
|
||||
|
||||
export default ({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
return defineConfig({
|
||||
define: {
|
||||
"process.env": env,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "out",
|
||||
},
|
||||
plugins: [react()],
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -34,6 +34,10 @@
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [".next/**", "dist/**"]
|
||||
},
|
||||
"vite:build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [".next/**", "dist/**"]
|
||||
},
|
||||
"develop": {
|
||||
"cache": false,
|
||||
"persistent": true,
|
||||
|
||||
Reference in New Issue
Block a user