forked from github/plane
Merge branch 'fix/self_hosted_instance' of github.com:makeplane/plane into develop-deploy
This commit is contained in:
commit
9e33804d12
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,3 +79,4 @@ pnpm-workspace.yaml
|
|||||||
tmp/
|
tmp/
|
||||||
## packages
|
## packages
|
||||||
dist
|
dist
|
||||||
|
.temp/
|
||||||
|
@ -5,6 +5,7 @@ CORS_ALLOWED_ORIGINS=""
|
|||||||
|
|
||||||
# Error logs
|
# Error logs
|
||||||
SENTRY_DSN=""
|
SENTRY_DSN=""
|
||||||
|
SENTRY_ENVIRONMENT="development"
|
||||||
|
|
||||||
# Database Settings
|
# Database Settings
|
||||||
PGUSER="plane"
|
PGUSER="plane"
|
||||||
|
@ -3,11 +3,18 @@ set -e
|
|||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Set default value for ENABLE_REGISTRATION
|
||||||
|
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1}
|
||||||
|
|
||||||
|
# Check if ENABLE_REGISTRATION is not set to '0'
|
||||||
|
if [ "$ENABLE_REGISTRATION" != "0" ]; then
|
||||||
# Register instance
|
# Register instance
|
||||||
python manage.py register_instance
|
python manage.py register_instance
|
||||||
# Load the configuration variable
|
# Load the configuration variable
|
||||||
python manage.py configure_instance
|
python manage.py configure_instance
|
||||||
|
fi
|
||||||
|
|
||||||
# Create the default bucket
|
# Create the default bucket
|
||||||
python bin/bucket_script.py
|
python manage.py create_bucket
|
||||||
|
|
||||||
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:8000 --max-requests 1200 --max-requests-jitter 1000 --access-logfile -
|
||||||
|
@ -320,11 +320,11 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
except RequestException as e:
|
except RequestException as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@ -230,13 +230,18 @@ class InstanceConfigurationEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
key = request.data.get("key", False)
|
configurations = InstanceConfiguration.objects.filter(key__in=request.data.keys())
|
||||||
if not key:
|
|
||||||
return Response(
|
bulk_configurations = []
|
||||||
{"error": "Key is required"}, status=status.HTTP_400_BAD_REQUEST
|
for configuration in configurations:
|
||||||
|
configuration.value = request.data.get(configuration.key, configuration.value)
|
||||||
|
bulk_configurations.append(configuration)
|
||||||
|
|
||||||
|
InstanceConfiguration.objects.bulk_update(
|
||||||
|
bulk_configurations,
|
||||||
|
["value"],
|
||||||
|
batch_size=100
|
||||||
)
|
)
|
||||||
configuration = InstanceConfiguration.objects.get(key=key)
|
|
||||||
configuration.value = request.data.get("value")
|
serializer = InstanceConfigurationSerializer(configurations, many=True)
|
||||||
configuration.save()
|
|
||||||
serializer = InstanceConfigurationSerializer(configuration)
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -27,9 +27,6 @@ DEBUG = False
|
|||||||
# Allowed Hosts
|
# Allowed Hosts
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
# Redirect if / is not present
|
|
||||||
APPEND_SLASH = True
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
@ -301,7 +298,7 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
|||||||
],
|
],
|
||||||
traces_sample_rate=1,
|
traces_sample_rate=1,
|
||||||
send_default_pii=True,
|
send_default_pii=True,
|
||||||
environment=os.environ.get("ENVIRONMENT", "development"),
|
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
|
||||||
profiles_sample_rate=1.0,
|
profiles_sample_rate=1.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ x-app-env : &app-env
|
|||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""}
|
||||||
- DOCKERIZED=${DOCKERIZED:-1} # deprecated
|
- DOCKERIZED=${DOCKERIZED:-1} # deprecated
|
||||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
|
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
|
||||||
|
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||||
# Gunicorn Workers
|
# Gunicorn Workers
|
||||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
||||||
#DB SETTINGS
|
#DB SETTINGS
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
BRANCH=${BRANCH:-master}
|
BRANCH=master
|
||||||
SCRIPT_DIR=$PWD
|
SCRIPT_DIR=$PWD
|
||||||
PLANE_INSTALL_DIR=$PWD/plane-app
|
PLANE_INSTALL_DIR=$PWD/plane-app
|
||||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
|
||||||
|
|
||||||
function install(){
|
function install(){
|
||||||
echo
|
echo
|
||||||
@ -28,6 +27,19 @@ function download(){
|
|||||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$BRANCH" != "master" ];
|
||||||
|
then
|
||||||
|
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
|
||||||
|
|
||||||
|
sed -e 's@plane-frontend:@plane-frontend-private:@g' \
|
||||||
|
-e 's@plane-space:@plane-space-private:@g' \
|
||||||
|
-e 's@plane-backend:@plane-backend-private:@g' \
|
||||||
|
-e 's@plane-proxy:@plane-proxy-private:@g' \
|
||||||
|
-e 's@${APP_RELEASE:-latest}@'"$BRANCH"'@g' \
|
||||||
|
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||||
|
|
||||||
|
rm $PLANE_INSTALL_DIR/temp.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Latest version is now available for you to use"
|
echo "Latest version is now available for you to use"
|
||||||
@ -108,4 +120,10 @@ function askForAction(){
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [ "$BRANCH" != "master" ];
|
||||||
|
then
|
||||||
|
PLANE_INSTALL_DIR=$PWD/plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||||
|
fi
|
||||||
|
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||||
|
|
||||||
askForAction
|
askForAction
|
||||||
|
@ -14,7 +14,7 @@ SENTRY_DSN=""
|
|||||||
GITHUB_CLIENT_SECRET=""
|
GITHUB_CLIENT_SECRET=""
|
||||||
DOCKERIZED=1 # deprecated
|
DOCKERIZED=1 # deprecated
|
||||||
CORS_ALLOWED_ORIGINS=""
|
CORS_ALLOWED_ORIGINS=""
|
||||||
|
SENTRY_ENVIRONMENT="production"
|
||||||
|
|
||||||
#DB SETTINGS
|
#DB SETTINGS
|
||||||
PGHOST=plane-db
|
PGHOST=plane-db
|
||||||
|
@ -30,9 +30,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/popover2": "^2.0.10",
|
"@blueprintjs/popover2": "^2.0.10",
|
||||||
"@tiptap/core": "^2.1.7",
|
"@tiptap/core": "^2.1.7",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
||||||
"@tiptap/extension-color": "^2.1.11",
|
"@tiptap/extension-color": "^2.1.11",
|
||||||
"@tiptap/extension-image": "^2.1.7",
|
"@tiptap/extension-image": "^2.1.7",
|
||||||
"@tiptap/extension-link": "^2.1.7",
|
"@tiptap/extension-link": "^2.1.7",
|
||||||
|
"@tiptap/extension-list-item": "^2.1.12",
|
||||||
"@tiptap/extension-mention": "^2.1.12",
|
"@tiptap/extension-mention": "^2.1.12",
|
||||||
"@tiptap/extension-table": "^2.1.6",
|
"@tiptap/extension-table": "^2.1.6",
|
||||||
"@tiptap/extension-table-cell": "^2.1.6",
|
"@tiptap/extension-table-cell": "^2.1.6",
|
||||||
@ -42,9 +44,8 @@
|
|||||||
"@tiptap/extension-task-list": "^2.1.7",
|
"@tiptap/extension-task-list": "^2.1.7",
|
||||||
"@tiptap/extension-text-style": "^2.1.11",
|
"@tiptap/extension-text-style": "^2.1.11",
|
||||||
"@tiptap/extension-underline": "^2.1.7",
|
"@tiptap/extension-underline": "^2.1.7",
|
||||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
|
||||||
"jsx-dom-cjs": "^8.0.3",
|
|
||||||
"@tiptap/pm": "^2.1.7",
|
"@tiptap/pm": "^2.1.7",
|
||||||
|
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||||
"@tiptap/react": "^2.1.7",
|
"@tiptap/react": "^2.1.7",
|
||||||
"@tiptap/starter-kit": "^2.1.10",
|
"@tiptap/starter-kit": "^2.1.10",
|
||||||
"@tiptap/suggestion": "^2.0.4",
|
"@tiptap/suggestion": "^2.0.4",
|
||||||
@ -56,7 +57,11 @@
|
|||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
"eventsource-parser": "^0.1.0",
|
"eventsource-parser": "^0.1.0",
|
||||||
|
"highlight.js": "^11.8.0",
|
||||||
|
"jsx-dom-cjs": "^8.0.3",
|
||||||
|
"lowlight": "^3.0.0",
|
||||||
"lucide-react": "^0.244.0",
|
"lucide-react": "^0.244.0",
|
||||||
|
"prosemirror-async-query": "^0.0.4",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-moveable": "^0.54.2",
|
"react-moveable": "^0.54.2",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// styles
|
// styles
|
||||||
// import "./styles/tailwind.css";
|
// import "./styles/tailwind.css";
|
||||||
// import "./styles/editor.css";
|
// import "./styles/editor.css";
|
||||||
|
import "./styles/github-dark.css";
|
||||||
|
|
||||||
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
||||||
|
|
||||||
|
@ -50,10 +50,11 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
|
|||||||
else editor.chain().focus().toggleUnderline().run();
|
else editor.chain().focus().toggleUnderline().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleCode = (editor: Editor, range?: Range) => {
|
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
||||||
if (range) editor.chain().focus().deleteRange(range).toggleCode().run();
|
if (range) editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||||
else editor.chain().focus().toggleCode().run();
|
else editor.chain().focus().toggleCodeBlock().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||||
if (range)
|
if (range)
|
||||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||||
|
85
packages/editor/core/src/styles/github-dark.css
Normal file
85
packages/editor/core/src/styles/github-dark.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
code.hljs {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
.hljs {
|
||||||
|
color: #c9d1d9;
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-meta .hljs-keyword,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable.language_ {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-title.class_,
|
||||||
|
.hljs-title.class_.inherited__,
|
||||||
|
.hljs-title.function_ {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-operator,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
.hljs-meta .hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-string {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
.hljs-code,
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-formula {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #7ee787;
|
||||||
|
}
|
||||||
|
.hljs-subst {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
.hljs-section {
|
||||||
|
color: #1f6feb;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hljs-bullet {
|
||||||
|
color: #f2cc60;
|
||||||
|
}
|
||||||
|
.hljs-emphasis {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.hljs-strong {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.hljs-addition {
|
||||||
|
color: #aff5b4;
|
||||||
|
background-color: #033a16;
|
||||||
|
}
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #ffdcd7;
|
||||||
|
background-color: #67060c;
|
||||||
|
}
|
29
packages/editor/core/src/ui/extensions/code/index.tsx
Normal file
29
packages/editor/core/src/ui/extensions/code/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||||
|
|
||||||
|
import { common, createLowlight } from "lowlight";
|
||||||
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
|
|
||||||
|
const lowlight = createLowlight(common);
|
||||||
|
lowlight.register("ts", ts);
|
||||||
|
|
||||||
|
export const CustomCodeBlock = CodeBlockLowlight.extend({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Tab: ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { selection, doc } = state;
|
||||||
|
const { $from, empty } = selection;
|
||||||
|
|
||||||
|
if (!empty || $from.parent.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.commands.insertContent(" ");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
lowlight,
|
||||||
|
defaultLanguage: "plaintext",
|
||||||
|
exitOnTripleEnter: false,
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-keymap";
|
@ -0,0 +1,30 @@
|
|||||||
|
import { getNodeType } from '@tiptap/core'
|
||||||
|
import { NodeType } from '@tiptap/pm/model'
|
||||||
|
import { EditorState } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
|
||||||
|
const { $from } = state.selection
|
||||||
|
const nodeType = getNodeType(typeOrName, state.schema)
|
||||||
|
|
||||||
|
let currentNode = null
|
||||||
|
let currentDepth = $from.depth
|
||||||
|
let currentPos = $from.pos
|
||||||
|
let targetDepth: number | null = null
|
||||||
|
|
||||||
|
while (currentDepth > 0 && targetDepth === null) {
|
||||||
|
currentNode = $from.node(currentDepth)
|
||||||
|
|
||||||
|
if (currentNode.type === nodeType) {
|
||||||
|
targetDepth = currentDepth
|
||||||
|
} else {
|
||||||
|
currentDepth -= 1
|
||||||
|
currentPos -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDepth === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { getNodeAtPosition } from "@tiptap/core";
|
||||||
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
import { findListItemPos } from "./find-list-item-pos";
|
||||||
|
|
||||||
|
export const getNextListDepth = (typeOrName: string, state: EditorState) => {
|
||||||
|
const listItemPos = findListItemPos(typeOrName, state);
|
||||||
|
|
||||||
|
if (!listItemPos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, depth] = getNodeAtPosition(
|
||||||
|
state,
|
||||||
|
typeOrName,
|
||||||
|
listItemPos.$pos.pos + 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
return depth;
|
||||||
|
};
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Editor, isAtStartOfNode, isNodeActive } from "@tiptap/core";
|
||||||
|
import { Node } from "@tiptap/pm/model";
|
||||||
|
|
||||||
|
import { findListItemPos } from "./find-list-item-pos";
|
||||||
|
import { hasListBefore } from "./has-list-before";
|
||||||
|
|
||||||
|
export const handleBackspace = (
|
||||||
|
editor: Editor,
|
||||||
|
name: string,
|
||||||
|
parentListTypes: string[],
|
||||||
|
) => {
|
||||||
|
// this is required to still handle the undo handling
|
||||||
|
if (editor.commands.undoInputRule()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the cursor is not at the start of a node
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isAtStartOfNode(editor.state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the current item is NOT inside a list item &
|
||||||
|
// the previous item is a list (orderedList or bulletList)
|
||||||
|
// move the cursor into the list and delete the current item
|
||||||
|
if (
|
||||||
|
!isNodeActive(editor.state, name) &&
|
||||||
|
hasListBefore(editor.state, name, parentListTypes)
|
||||||
|
) {
|
||||||
|
const { $anchor } = editor.state.selection;
|
||||||
|
|
||||||
|
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
|
||||||
|
|
||||||
|
const listDescendants: Array<{ node: Node; pos: number }> = [];
|
||||||
|
|
||||||
|
$listPos.node().descendants((node, pos) => {
|
||||||
|
if (node.type.name === name) {
|
||||||
|
listDescendants.push({ node, pos });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastItem = listDescendants.at(-1);
|
||||||
|
|
||||||
|
if (!lastItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $lastItemPos = editor.state.doc.resolve(
|
||||||
|
$listPos.start() + lastItem.pos + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return editor
|
||||||
|
.chain()
|
||||||
|
.cut(
|
||||||
|
{ from: $anchor.start() - 1, to: $anchor.end() + 1 },
|
||||||
|
$lastItemPos.end(),
|
||||||
|
)
|
||||||
|
.joinForward()
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the cursor is not inside the current node type
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isNodeActive(editor.state, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItemPos = findListItemPos(name, editor.state);
|
||||||
|
|
||||||
|
if (!listItemPos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if current node is a list item and cursor it at start of a list node,
|
||||||
|
// simply lift the list item i.e. remove it as a list item (task/bullet/ordered)
|
||||||
|
// irrespective of above node being a list or not
|
||||||
|
return editor.chain().liftListItem(name).run();
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Editor, isAtEndOfNode, isNodeActive } from "@tiptap/core";
|
||||||
|
|
||||||
|
import { nextListIsDeeper } from "./next-list-is-deeper";
|
||||||
|
import { nextListIsHigher } from "./next-list-is-higher";
|
||||||
|
|
||||||
|
export const handleDelete = (editor: Editor, name: string) => {
|
||||||
|
// if the cursor is not inside the current node type
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isNodeActive(editor.state, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the cursor is not at the end of a node
|
||||||
|
// do nothing and proceed
|
||||||
|
if (!isAtEndOfNode(editor.state, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the next node is a list with a deeper depth
|
||||||
|
if (nextListIsDeeper(name, editor.state)) {
|
||||||
|
return editor
|
||||||
|
.chain()
|
||||||
|
.focus(editor.state.selection.from + 4)
|
||||||
|
.lift(name)
|
||||||
|
.joinBackward()
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextListIsHigher(name, editor.state)) {
|
||||||
|
return editor.chain().joinForward().joinBackward().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.commands.joinItemForward();
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { EditorState } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
|
||||||
|
const { $anchor } = editorState.selection
|
||||||
|
|
||||||
|
const previousNodePos = Math.max(0, $anchor.pos - 2)
|
||||||
|
|
||||||
|
const previousNode = editorState.doc.resolve(previousNodePos).node()
|
||||||
|
|
||||||
|
if (!previousNode || !parentListTypes.includes(previousNode.type.name)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { EditorState } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
|
||||||
|
const { $anchor } = state.selection
|
||||||
|
|
||||||
|
const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2)
|
||||||
|
|
||||||
|
if ($targetPos.index() === $targetPos.parent.childCount - 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($targetPos.nodeAfter?.type.name !== typeOrName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { EditorState } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
|
||||||
|
const { $anchor } = state.selection
|
||||||
|
|
||||||
|
const $targetPos = state.doc.resolve($anchor.pos - 2)
|
||||||
|
|
||||||
|
if ($targetPos.index() === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($targetPos.nodeBefore?.type.name !== typeOrName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
export * from "./find-list-item-pos";
|
||||||
|
export * from "./get-next-list-depth";
|
||||||
|
export * from "./handle-backspace";
|
||||||
|
export * from "./handle-delete";
|
||||||
|
export * from "./has-list-before";
|
||||||
|
export * from "./has-list-item-after";
|
||||||
|
export * from "./has-list-item-before";
|
||||||
|
export * from "./next-list-is-deeper";
|
||||||
|
export * from "./next-list-is-higher";
|
@ -0,0 +1,19 @@
|
|||||||
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
import { findListItemPos } from "./find-list-item-pos";
|
||||||
|
import { getNextListDepth } from "./get-next-list-depth";
|
||||||
|
|
||||||
|
export const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
|
||||||
|
const listDepth = getNextListDepth(typeOrName, state);
|
||||||
|
const listItemPos = findListItemPos(typeOrName, state);
|
||||||
|
|
||||||
|
if (!listItemPos || !listDepth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listDepth > listItemPos.depth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { EditorState } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
import { findListItemPos } from "./find-list-item-pos";
|
||||||
|
import { getNextListDepth } from "./get-next-list-depth";
|
||||||
|
|
||||||
|
export const nextListIsHigher = (typeOrName: string, state: EditorState) => {
|
||||||
|
const listDepth = getNextListDepth(typeOrName, state);
|
||||||
|
const listItemPos = findListItemPos(typeOrName, state);
|
||||||
|
|
||||||
|
if (!listItemPos || !listDepth) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listDepth < listItemPos.depth) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
@ -0,0 +1,94 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
|
||||||
|
import { handleBackspace, handleDelete } from "./list-helpers";
|
||||||
|
|
||||||
|
export type ListKeymapOptions = {
|
||||||
|
listTypes: Array<{
|
||||||
|
itemName: string;
|
||||||
|
wrapperNames: string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListKeymap = Extension.create<ListKeymapOptions>({
|
||||||
|
name: "listKeymap",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
listTypes: [
|
||||||
|
{
|
||||||
|
itemName: "listItem",
|
||||||
|
wrapperNames: ["bulletList", "orderedList"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemName: "taskItem",
|
||||||
|
wrapperNames: ["taskList"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Delete: ({ editor }) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
this.options.listTypes.forEach(({ itemName }) => {
|
||||||
|
if (editor.state.schema.nodes[itemName] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleDelete(editor, itemName)) {
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
},
|
||||||
|
"Mod-Delete": ({ editor }) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
this.options.listTypes.forEach(({ itemName }) => {
|
||||||
|
if (editor.state.schema.nodes[itemName] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleDelete(editor, itemName)) {
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
},
|
||||||
|
Backspace: ({ editor }) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
|
||||||
|
if (editor.state.schema.nodes[itemName] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleBackspace(editor, itemName, wrapperNames)) {
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
},
|
||||||
|
"Mod-Backspace": ({ editor }) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
|
||||||
|
if (editor.state.schema.nodes[itemName] === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleBackspace(editor, itemName, wrapperNames)) {
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return handled;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
116
packages/editor/core/src/ui/extensions/horizontal-rule.tsx
Normal file
116
packages/editor/core/src/ui/extensions/horizontal-rule.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { TextSelection } from "prosemirror-state";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InputRule,
|
||||||
|
mergeAttributes,
|
||||||
|
Node,
|
||||||
|
nodeInputRule,
|
||||||
|
wrappingInputRule,
|
||||||
|
} from "@tiptap/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension based on:
|
||||||
|
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface HorizontalRuleOptions {
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
horizontalRule: {
|
||||||
|
/**
|
||||||
|
* Add a horizontal rule
|
||||||
|
*/
|
||||||
|
setHorizontalRule: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Node.create<HorizontalRuleOptions>({
|
||||||
|
name: "horizontalRule",
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
group: "block",
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
color: {
|
||||||
|
default: "#dddddd",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: `div[data-type="${this.name}"]`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
"div",
|
||||||
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
|
"data-type": this.name,
|
||||||
|
}),
|
||||||
|
["div", {}],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setHorizontalRule:
|
||||||
|
() =>
|
||||||
|
({ chain }) => {
|
||||||
|
return (
|
||||||
|
chain()
|
||||||
|
.insertContent({ type: this.name })
|
||||||
|
// set cursor after horizontal rule
|
||||||
|
.command(({ tr, dispatch }) => {
|
||||||
|
if (dispatch) {
|
||||||
|
const { $to } = tr.selection;
|
||||||
|
const posAfter = $to.end();
|
||||||
|
|
||||||
|
if ($to.nodeAfter) {
|
||||||
|
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||||
|
} else {
|
||||||
|
// add node after horizontal rule if it’s the end of the document
|
||||||
|
const node =
|
||||||
|
$to.parent.type.contentMatch.defaultType?.create();
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
tr.insert(posAfter, node);
|
||||||
|
tr.setSelection(TextSelection.create(tr.doc, posAfter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
new InputRule({
|
||||||
|
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
@ -6,12 +6,12 @@ import { Color } from "@tiptap/extension-color";
|
|||||||
import TaskItem from "@tiptap/extension-task-item";
|
import TaskItem from "@tiptap/extension-task-item";
|
||||||
import TaskList from "@tiptap/extension-task-list";
|
import TaskList from "@tiptap/extension-task-list";
|
||||||
import { Markdown } from "tiptap-markdown";
|
import { Markdown } from "tiptap-markdown";
|
||||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
|
||||||
|
|
||||||
import TableHeader from "./table/table-header/table-header";
|
import TableHeader from "./table/table-header/table-header";
|
||||||
import Table from "./table/table";
|
import Table from "./table/table";
|
||||||
import TableCell from "./table/table-cell/table-cell";
|
import TableCell from "./table/table-cell/table-cell";
|
||||||
import TableRow from "./table/table-row/table-row";
|
import TableRow from "./table/table-row/table-row";
|
||||||
|
import HorizontalRule from "./horizontal-rule";
|
||||||
|
|
||||||
import ImageExtension from "./image";
|
import ImageExtension from "./image";
|
||||||
|
|
||||||
@ -20,6 +20,10 @@ import { isValidHttpUrl } from "../../lib/utils";
|
|||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
||||||
import { Mentions } from "../mentions";
|
import { Mentions } from "../mentions";
|
||||||
|
|
||||||
|
import { CustomKeymap } from "./keymap";
|
||||||
|
import { CustomCodeBlock } from "./code";
|
||||||
|
import { ListKeymap } from "./custom-list-keymap";
|
||||||
|
|
||||||
export const CoreEditorExtensions = (
|
export const CoreEditorExtensions = (
|
||||||
mentionConfig: {
|
mentionConfig: {
|
||||||
mentionSuggestions: IMentionSuggestion[];
|
mentionSuggestions: IMentionSuggestion[];
|
||||||
@ -49,22 +53,16 @@ export const CoreEditorExtensions = (
|
|||||||
class: "border-l-4 border-custom-border-300",
|
class: "border-l-4 border-custom-border-300",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: false,
|
||||||
HTMLAttributes: {
|
|
||||||
class:
|
|
||||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
|
||||||
spellcheck: "false",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
codeBlock: false,
|
codeBlock: false,
|
||||||
horizontalRule: false,
|
horizontalRule: false,
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
color: "rgba(var(--color-text-100))",
|
color: "rgba(var(--color-text-100))",
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
gapcursor: false,
|
|
||||||
}),
|
}),
|
||||||
Gapcursor,
|
CustomKeymap,
|
||||||
|
ListKeymap,
|
||||||
TiptapLink.configure({
|
TiptapLink.configure({
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url) => isValidHttpUrl(url),
|
||||||
@ -86,6 +84,7 @@ export const CoreEditorExtensions = (
|
|||||||
class: "not-prose pl-2",
|
class: "not-prose pl-2",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
CustomCodeBlock,
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "flex items-start my-4",
|
class: "flex items-start my-4",
|
||||||
@ -95,7 +94,9 @@ export const CoreEditorExtensions = (
|
|||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
|
transformPastedText: true,
|
||||||
}),
|
}),
|
||||||
|
HorizontalRule,
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
|
54
packages/editor/core/src/ui/extensions/keymap.tsx
Normal file
54
packages/editor/core/src/ui/extensions/keymap.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
customkeymap: {
|
||||||
|
/**
|
||||||
|
* Select text between node boundaries
|
||||||
|
*/
|
||||||
|
selectTextWithinNodeBoundaries: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomKeymap = Extension.create({
|
||||||
|
name: "CustomKeymap",
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
selectTextWithinNodeBoundaries:
|
||||||
|
() =>
|
||||||
|
({ editor, commands }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { tr } = state;
|
||||||
|
const startNodePos = tr.selection.$from.start();
|
||||||
|
const endNodePos = tr.selection.$to.end();
|
||||||
|
return commands.setTextSelection({
|
||||||
|
from: startNodePos,
|
||||||
|
to: endNodePos,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
"Mod-a": ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { tr } = state;
|
||||||
|
const startSelectionPos = tr.selection.from;
|
||||||
|
const endSelectionPos = tr.selection.to;
|
||||||
|
const startNodePos = tr.selection.$from.start();
|
||||||
|
const endNodePos = tr.selection.$to.end();
|
||||||
|
const isCurrentTextSelectionNotExtendedToNodeBoundaries =
|
||||||
|
startSelectionPos > startNodePos || endSelectionPos < endNodePos;
|
||||||
|
if (isCurrentTextSelectionNotExtendedToNodeBoundaries) {
|
||||||
|
editor.chain().selectTextWithinNodeBoundaries().run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
@ -1,11 +1,10 @@
|
|||||||
const icons = {
|
const icons = {
|
||||||
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
|
colorPicker: `<svg xmlns="http://www.w3.org/2000/svg" length="24" viewBox="0 0 24 24" style="transform: ;msFilter:;"><path fill="rgb(var(--color-text-300))" d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414 16.586 13H5.414L11 7.414z"></path></svg>`,
|
||||||
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
|
deleteColumn: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z"/></svg>`,
|
||||||
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
|
deleteRow: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" length="24"><path fill="#e53e3e" d="M0 0H24V24H0z"/><path d="M20 5c.552 0 1 .448 1 1v6c0 .552-.448 1-1 1 .628.835 1 1.874 1 3 0 2.761-2.239 5-5 5s-5-2.239-5-5c0-1.126.372-2.165 1-3H4c-.552 0-1-.448-1-1V6c0-.552.448-1 1-1h16zm-7 10v2h6v-2h-6zm6-8H5v4h14V7z"/></svg>`,
|
||||||
insertLeftTableIcon: `<svg
|
insertLeftTableIcon: `<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={24}
|
length={24}
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
@ -16,8 +15,7 @@ const icons = {
|
|||||||
`,
|
`,
|
||||||
insertRightTableIcon: `<svg
|
insertRightTableIcon: `<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={24}
|
length={24}
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
@ -28,8 +26,7 @@ const icons = {
|
|||||||
`,
|
`,
|
||||||
insertTopTableIcon: `<svg
|
insertTopTableIcon: `<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={24}
|
length={24}
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
@ -40,8 +37,7 @@ const icons = {
|
|||||||
`,
|
`,
|
||||||
insertBottomTableIcon: `<svg
|
insertBottomTableIcon: `<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={24}
|
length={24}
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||||
import {
|
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
MutableRefObject,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { DeleteImage } from "../../types/delete-image";
|
import { DeleteImage } from "../../types/delete-image";
|
||||||
import { CoreEditorProps } from "../props";
|
import { CoreEditorProps } from "../props";
|
||||||
import { CoreEditorExtensions } from "../extensions";
|
import { CoreEditorExtensions } from "../extensions";
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
toggleBlockquote,
|
toggleBlockquote,
|
||||||
toggleBold,
|
toggleBold,
|
||||||
toggleBulletList,
|
toggleBulletList,
|
||||||
toggleCode,
|
toggleCodeBlock,
|
||||||
toggleHeadingOne,
|
toggleHeadingOne,
|
||||||
toggleHeadingThree,
|
toggleHeadingThree,
|
||||||
toggleHeadingTwo,
|
toggleHeadingTwo,
|
||||||
@ -89,13 +89,6 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
|||||||
icon: StrikethroughIcon,
|
icon: StrikethroughIcon,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
|
||||||
name: "code",
|
|
||||||
isActive: () => editor?.isActive("code"),
|
|
||||||
command: () => toggleCode(editor),
|
|
||||||
icon: CodeIcon,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
||||||
name: "bullet-list",
|
name: "bullet-list",
|
||||||
isActive: () => editor?.isActive("bulletList"),
|
isActive: () => editor?.isActive("bulletList"),
|
||||||
@ -110,6 +103,13 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
|||||||
icon: CheckSquare,
|
icon: CheckSquare,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||||
|
name: "code",
|
||||||
|
isActive: () => editor?.isActive("code"),
|
||||||
|
command: () => toggleCodeBlock(editor),
|
||||||
|
icon: CodeIcon,
|
||||||
|
});
|
||||||
|
|
||||||
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||||
name: "ordered-list",
|
name: "ordered-list",
|
||||||
isActive: () => editor?.isActive("orderedList"),
|
isActive: () => editor?.isActive("orderedList"),
|
||||||
|
@ -46,14 +46,14 @@ type EditorBubbleMenuProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||||
const basicMarkItems: BubbleMenuItem[] = [
|
const basicTextFormattingItems: BubbleMenuItem[] = [
|
||||||
BoldItem(props.editor),
|
BoldItem(props.editor),
|
||||||
ItalicItem(props.editor),
|
ItalicItem(props.editor),
|
||||||
UnderLineItem(props.editor),
|
UnderLineItem(props.editor),
|
||||||
StrikeThroughItem(props.editor),
|
StrikeThroughItem(props.editor),
|
||||||
];
|
];
|
||||||
|
|
||||||
const listItems: BubbleMenuItem[] = [
|
const listFormattingItems: BubbleMenuItem[] = [
|
||||||
BulletListItem(props.editor),
|
BulletListItem(props.editor),
|
||||||
NumberedListItem(props.editor),
|
NumberedListItem(props.editor),
|
||||||
];
|
];
|
||||||
@ -103,7 +103,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
|||||||
<div className="flex items-stretch justify-between gap-2 w-full border-[0.5px] border-custom-border-200 bg-custom-background-90 rounded p-1">
|
<div className="flex items-stretch justify-between gap-2 w-full border-[0.5px] border-custom-border-200 bg-custom-background-90 rounded p-1">
|
||||||
<div className="flex items-stretch">
|
<div className="flex items-stretch">
|
||||||
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
|
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
|
||||||
{basicMarkItems.map((item, index) => (
|
{basicTextFormattingItems.map((item, index) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={index}
|
key={index}
|
||||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||||
@ -130,7 +130,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
|
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
|
||||||
{listItems.map((item, index) => (
|
{listFormattingItems.map((item, index) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={index}
|
key={index}
|
||||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||||
|
@ -30,14 +30,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.1.11",
|
|
||||||
"@tiptap/extension-horizontal-rule": "^2.1.11",
|
"@tiptap/extension-horizontal-rule": "^2.1.11",
|
||||||
"@tiptap/extension-placeholder": "^2.1.11",
|
"@tiptap/extension-placeholder": "^2.1.11",
|
||||||
"@tiptap/suggestion": "^2.1.7",
|
"@tiptap/suggestion": "^2.1.7",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"highlight.js": "^11.8.0",
|
|
||||||
"lowlight": "^3.0.0",
|
|
||||||
"lucide-react": "^0.244.0"
|
"lucide-react": "^0.244.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import "./styles/github-dark.css";
|
|
||||||
|
|
||||||
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
||||||
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
|
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
|
||||||
export type { IMentionSuggestion, IMentionHighlight } from "./ui";
|
export type { IMentionSuggestion, IMentionHighlight } from "./ui";
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
export function createDragHandleElement(): HTMLElement {
|
||||||
|
let dragHandleElement = document.createElement("div");
|
||||||
|
dragHandleElement.draggable = true;
|
||||||
|
dragHandleElement.dataset.dragHandle = "";
|
||||||
|
dragHandleElement.classList.add("drag-handle");
|
||||||
|
|
||||||
|
const dragHandleContainer = document.createElement("div");
|
||||||
|
dragHandleContainer.classList.add("drag-handle-container");
|
||||||
|
dragHandleElement.appendChild(dragHandleContainer);
|
||||||
|
|
||||||
|
const dotsContainer = document.createElement("div");
|
||||||
|
dotsContainer.classList.add("drag-handle-dots");
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const spanElement = document.createElement("span");
|
||||||
|
spanElement.classList.add("drag-handle-dot");
|
||||||
|
dotsContainer.appendChild(spanElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragHandleContainer.appendChild(dotsContainer);
|
||||||
|
|
||||||
|
return dragHandleElement;
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
|
|
||||||
.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
|
235
packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx
Normal file
235
packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { Extension } from "@tiptap/core";
|
||||||
|
|
||||||
|
import { PluginKey, NodeSelection, Plugin } from "@tiptap/pm/state";
|
||||||
|
// @ts-ignore
|
||||||
|
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||||
|
import { createDragHandleElement } from "../../lib/utils/DragHandleElement";
|
||||||
|
|
||||||
|
export interface DragHandleOptions {
|
||||||
|
dragHandleWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function absoluteRect(node: Element) {
|
||||||
|
const data = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: data.top,
|
||||||
|
left: data.left,
|
||||||
|
width: data.width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||||||
|
return document
|
||||||
|
.elementsFromPoint(coords.x, coords.y)
|
||||||
|
.find((elem: Element) => {
|
||||||
|
return (
|
||||||
|
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||||
|
elem.matches(
|
||||||
|
[
|
||||||
|
"li",
|
||||||
|
"p:not(:first-child)",
|
||||||
|
"pre",
|
||||||
|
"blockquote",
|
||||||
|
"h1, h2, h3",
|
||||||
|
"[data-type=horizontalRule]",
|
||||||
|
".tableWrapper",
|
||||||
|
].join(", "),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodePosAtDOM(node: Element, view: EditorView) {
|
||||||
|
const boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (node.nodeName === "IMG") {
|
||||||
|
return view.posAtCoords({
|
||||||
|
left: boundingRect.left + 1,
|
||||||
|
top: boundingRect.top + 1,
|
||||||
|
})?.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeName === "PRE") {
|
||||||
|
return (
|
||||||
|
view.posAtCoords({
|
||||||
|
left: boundingRect.left + 1,
|
||||||
|
top: boundingRect.top + 1,
|
||||||
|
})?.pos! - 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.posAtCoords({
|
||||||
|
left: boundingRect.left + 1,
|
||||||
|
top: boundingRect.top + 1,
|
||||||
|
})?.inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DragHandle(options: DragHandleOptions) {
|
||||||
|
function handleDragStart(event: DragEvent, view: EditorView) {
|
||||||
|
view.focus();
|
||||||
|
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + options.dragHandleWidth + 50,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element)) return;
|
||||||
|
|
||||||
|
const nodePos = nodePosAtDOM(node, view);
|
||||||
|
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||||
|
|
||||||
|
view.dispatch(
|
||||||
|
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const slice = view.state.selection.content();
|
||||||
|
const { dom, text } = __serializeForClipboard(view, slice);
|
||||||
|
|
||||||
|
event.dataTransfer.clearData();
|
||||||
|
event.dataTransfer.setData("text/html", dom.innerHTML);
|
||||||
|
event.dataTransfer.setData("text/plain", text);
|
||||||
|
event.dataTransfer.effectAllowed = "copyMove";
|
||||||
|
|
||||||
|
event.dataTransfer.setDragImage(node, 0, 0);
|
||||||
|
|
||||||
|
view.dragging = { slice, move: event.ctrlKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent, view: EditorView) {
|
||||||
|
view.focus();
|
||||||
|
|
||||||
|
view.dom.classList.remove("dragging");
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + 50 + options.dragHandleWidth,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element)) return;
|
||||||
|
|
||||||
|
const nodePos = nodePosAtDOM(node, view);
|
||||||
|
|
||||||
|
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||||
|
|
||||||
|
view.dispatch(
|
||||||
|
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragHandleElement: HTMLElement | null = null;
|
||||||
|
|
||||||
|
function hideDragHandle() {
|
||||||
|
if (dragHandleElement) {
|
||||||
|
dragHandleElement.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDragHandle() {
|
||||||
|
if (dragHandleElement) {
|
||||||
|
dragHandleElement.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("dragHandle"),
|
||||||
|
view: (view) => {
|
||||||
|
dragHandleElement = createDragHandleElement();
|
||||||
|
dragHandleElement.addEventListener("dragstart", (e) => {
|
||||||
|
handleDragStart(e, view);
|
||||||
|
});
|
||||||
|
dragHandleElement.addEventListener("click", (e) => {
|
||||||
|
handleClick(e, view);
|
||||||
|
});
|
||||||
|
|
||||||
|
dragHandleElement.addEventListener("dragstart", (e) => {
|
||||||
|
handleDragStart(e, view);
|
||||||
|
});
|
||||||
|
dragHandleElement.addEventListener("click", (e) => {
|
||||||
|
handleClick(e, view);
|
||||||
|
});
|
||||||
|
|
||||||
|
hideDragHandle();
|
||||||
|
|
||||||
|
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy: () => {
|
||||||
|
dragHandleElement?.remove?.();
|
||||||
|
dragHandleElement = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousemove: (view, event) => {
|
||||||
|
if (!view.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + options.dragHandleWidth,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element)) {
|
||||||
|
hideDragHandle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compStyle = window.getComputedStyle(node);
|
||||||
|
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
||||||
|
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
||||||
|
|
||||||
|
const rect = absoluteRect(node);
|
||||||
|
|
||||||
|
rect.top += (lineHeight - 24) / 2;
|
||||||
|
rect.top += paddingTop;
|
||||||
|
// Li markers
|
||||||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||||
|
rect.left -= options.dragHandleWidth;
|
||||||
|
}
|
||||||
|
rect.width = options.dragHandleWidth;
|
||||||
|
|
||||||
|
if (!dragHandleElement) return;
|
||||||
|
|
||||||
|
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
||||||
|
dragHandleElement.style.top = `${rect.top + 3}px`;
|
||||||
|
showDragHandle();
|
||||||
|
},
|
||||||
|
keydown: () => {
|
||||||
|
hideDragHandle();
|
||||||
|
},
|
||||||
|
wheel: () => {
|
||||||
|
hideDragHandle();
|
||||||
|
},
|
||||||
|
// dragging className is used for CSS
|
||||||
|
dragstart: (view) => {
|
||||||
|
view.dom.classList.add("dragging");
|
||||||
|
},
|
||||||
|
drop: (view) => {
|
||||||
|
view.dom.classList.remove("dragging");
|
||||||
|
},
|
||||||
|
dragend: (view) => {
|
||||||
|
view.dom.classList.remove("dragging");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragAndDrop = Extension.create({
|
||||||
|
name: "dragAndDrop",
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
DragHandle({
|
||||||
|
dragHandleWidth: 24,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DragAndDrop;
|
@ -1,50 +1,18 @@
|
|||||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
||||||
import { common, createLowlight } from "lowlight";
|
|
||||||
import { InputRule } from "@tiptap/core";
|
|
||||||
|
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
|
||||||
|
|
||||||
import SlashCommand from "./slash-command";
|
import SlashCommand from "./slash-command";
|
||||||
import { UploadImage } from "../";
|
import { UploadImage } from "../";
|
||||||
|
import DragAndDrop from "./drag-drop";
|
||||||
const lowlight = createLowlight(common);
|
|
||||||
lowlight.register("ts", ts);
|
|
||||||
|
|
||||||
export const RichTextEditorExtensions = (
|
export const RichTextEditorExtensions = (
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (
|
setIsSubmitting?: (
|
||||||
isSubmitting: "submitting" | "submitted" | "saved",
|
isSubmitting: "submitting" | "submitted" | "saved",
|
||||||
) => void,
|
) => void,
|
||||||
|
dragDropEnabled?: boolean,
|
||||||
) => [
|
) => [
|
||||||
HorizontalRule.extend({
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
new InputRule({
|
|
||||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
|
||||||
handler: ({ state, range, commands }) => {
|
|
||||||
commands.splitBlock();
|
|
||||||
|
|
||||||
const attributes = {};
|
|
||||||
const { tr } = state;
|
|
||||||
const start = range.from;
|
|
||||||
const end = range.to;
|
|
||||||
// @ts-ignore
|
|
||||||
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
}).configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: "mb-6 border-t border-custom-border-300",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
SlashCommand(uploadFile, setIsSubmitting),
|
SlashCommand(uploadFile, setIsSubmitting),
|
||||||
CodeBlockLowlight.configure({
|
dragDropEnabled === true && DragAndDrop,
|
||||||
lowlight,
|
|
||||||
}),
|
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: ({ node }) => {
|
placeholder: ({ node }) => {
|
||||||
if (node.type.name === "heading") {
|
if (node.type.name === "heading") {
|
||||||
@ -53,7 +21,9 @@ export const RichTextEditorExtensions = (
|
|||||||
if (node.type.name === "image" || node.type.name === "table") {
|
if (node.type.name === "image" || node.type.name === "table") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (node.type.name === "codeBlock") {
|
||||||
|
return "Type in your code here...";
|
||||||
|
}
|
||||||
return "Press '/' for commands...";
|
return "Press '/' for commands...";
|
||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
|
@ -25,6 +25,7 @@ export type IMentionHighlight = string;
|
|||||||
|
|
||||||
interface IRichTextEditor {
|
interface IRichTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
|
dragDropEnabled?: boolean;
|
||||||
uploadFile: UploadImage;
|
uploadFile: UploadImage;
|
||||||
deleteFile: DeleteImage;
|
deleteFile: DeleteImage;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
@ -54,6 +55,7 @@ interface EditorHandle {
|
|||||||
|
|
||||||
const RichTextEditor = ({
|
const RichTextEditor = ({
|
||||||
onChange,
|
onChange,
|
||||||
|
dragDropEnabled,
|
||||||
debouncedUpdatesEnabled,
|
debouncedUpdatesEnabled,
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
@ -79,7 +81,11 @@ const RichTextEditor = ({
|
|||||||
cancelUploadImage,
|
cancelUploadImage,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting),
|
extensions: RichTextEditorExtensions(
|
||||||
|
uploadFile,
|
||||||
|
setIsSubmitting,
|
||||||
|
dragDropEnabled,
|
||||||
|
),
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions,
|
mentionSuggestions,
|
||||||
});
|
});
|
||||||
|
@ -38,21 +38,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
const { selection } = state;
|
const { selection } = state;
|
||||||
|
|
||||||
const { empty } = selection;
|
const { empty } = selection;
|
||||||
const hasEditorFocus = view.hasFocus();
|
|
||||||
|
|
||||||
// if (typeof window !== "undefined") {
|
|
||||||
// const selection: any = window?.getSelection();
|
|
||||||
// if (selection.rangeCount !== 0) {
|
|
||||||
// const range = selection.getRangeAt(0);
|
|
||||||
// if (findTableAncestor(range.startContainer)) {
|
|
||||||
// console.log("table");
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hasEditorFocus ||
|
|
||||||
empty ||
|
empty ||
|
||||||
!editor.isEditable ||
|
!editor.isEditable ||
|
||||||
editor.isActive("image") ||
|
editor.isActive("image") ||
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
BulletListItem,
|
BulletListItem,
|
||||||
cn,
|
cn,
|
||||||
CodeItem,
|
|
||||||
HeadingOneItem,
|
HeadingOneItem,
|
||||||
HeadingThreeItem,
|
HeadingThreeItem,
|
||||||
HeadingTwoItem,
|
HeadingTwoItem,
|
||||||
NumberedListItem,
|
NumberedListItem,
|
||||||
QuoteItem,
|
QuoteItem,
|
||||||
|
CodeItem,
|
||||||
TodoListItem,
|
TodoListItem,
|
||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
|
@ -53,11 +53,12 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
|||||||
background-color: rgb(var(--color-background-100));
|
background-color: rgb(var(--color-background-100));
|
||||||
margin: 0;
|
margin: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 1.2rem;
|
width: 0.8rem;
|
||||||
height: 1.2rem;
|
height: 0.8rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 2px solid rgb(var(--color-text-100));
|
border: 1.5px solid rgb(var(--color-text-100));
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.2rem;
|
||||||
|
margin-top: 0.15rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
|
||||||
@ -71,8 +72,8 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
|
|||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
width: 0.65em;
|
width: 0.5em;
|
||||||
height: 0.65em;
|
height: 0.5em;
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
transition: 120ms transform ease-in-out;
|
transition: 120ms transform ease-in-out;
|
||||||
box-shadow: inset 1em 1em;
|
box-shadow: inset 1em 1em;
|
||||||
@ -229,3 +230,93 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|||||||
.ProseMirror table * .is-empty::before {
|
.ProseMirror table * .is-empty::before {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror pre {
|
||||||
|
background: rgba(var(--color-background-80));
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: rgba(var(--color-text-100));
|
||||||
|
font-family: "JetBrainsMono", monospace;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror pre code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(img):not(pre) {
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
background-color: rgb(var(--color-background-90));
|
||||||
|
border: 1px solid #5abbf7;
|
||||||
|
padding: 4px 2px 4px 2px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
position: fixed;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-in 0.2s;
|
||||||
|
height: 18px;
|
||||||
|
width: 15px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: grab;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: rgb(var(--color-background-90));
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:hover {
|
||||||
|
background-color: rgb(var(--color-background-80));
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.drag-handle {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-container {
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
cursor: grab;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-dots {
|
||||||
|
height: 100%;
|
||||||
|
width: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-dot {
|
||||||
|
height: 2.75px;
|
||||||
|
width: 3px;
|
||||||
|
background-color: rgba(var(--color-text-200));
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[data-type="horizontalRule"] {
|
||||||
|
line-height: 0;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
border-bottom: 1px solid rgb(var(--color-text-100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,12 +30,12 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
cycle: cycleStore,
|
cycle: cycleStore,
|
||||||
cycleIssueFilter: cycleIssueFilterStore,
|
cycleIssueFilter: cycleIssueFilterStore,
|
||||||
project: projectStore,
|
project: { currentProjectDetails },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
labels={projectLabels ?? undefined}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -30,13 +30,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
module: moduleStore,
|
module: moduleStore,
|
||||||
moduleFilter: moduleFilterStore,
|
moduleFilter: moduleFilterStore,
|
||||||
project: projectStore,
|
project: { currentProjectDetails },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
commandPalette: commandPaletteStore,
|
commandPalette: commandPaletteStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
labels={projectLabels ?? undefined}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -21,14 +21,13 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: { currentProjectDetails },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
archivedIssueFilters: archivedIssueFiltersStore,
|
archivedIssueFilters: archivedIssueFiltersStore,
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
// for archived issues list layout is the only option
|
// for archived issues list layout is the only option
|
||||||
const activeLayout = "list";
|
const activeLayout = "list";
|
||||||
|
|
||||||
@ -119,7 +118,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
|||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
labels={projectLabels ?? undefined}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -25,7 +25,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
project: projectStore,
|
project: { currentProjectDetails },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
inbox: inboxStore,
|
inbox: inboxStore,
|
||||||
@ -92,7 +93,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
},
|
},
|
||||||
[issueFilterStore, projectId, workspaceSlug]
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
);
|
);
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined;
|
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined;
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
|||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
labels={projectLabels ?? undefined}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -22,14 +22,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
const {
|
const {
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
projectViewFilters: projectViewFiltersStore,
|
projectViewFilters: projectViewFiltersStore,
|
||||||
project: projectStore,
|
project: { currentProjectDetails },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
projectViews: projectViewsStore,
|
projectViews: projectViewsStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||||
|
|
||||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||||
@ -163,7 +162,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
labels={projectLabels ?? undefined}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -1,44 +1,125 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// ui
|
// ui
|
||||||
import { Input } from "@plane/ui";
|
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IInstance } from "types/instance";
|
import { IInstance } from "types/instance";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
export interface IInstanceGeneralForm {
|
export interface IInstanceGeneralForm {
|
||||||
data: IInstance;
|
instance: IInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneralFormValues {
|
export interface GeneralFormValues {
|
||||||
instance_name: string;
|
instance_name: string;
|
||||||
namespace: string | null;
|
|
||||||
is_telemetry_enabled: boolean;
|
is_telemetry_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
export const InstanceGeneralForm: FC<IInstanceGeneralForm> = (props) => {
|
||||||
const { data } = props;
|
const { instance } = props;
|
||||||
|
// store
|
||||||
const {} = useForm<GeneralFormValues>({
|
const { instance: instanceStore } = useMobxStore();
|
||||||
|
// toast
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
// form data
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<GeneralFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
instance_name: data.instance_name,
|
instance_name: instance.instance_name,
|
||||||
namespace: data.namespace,
|
is_telemetry_enabled: instance.is_telemetry_enabled,
|
||||||
is_telemetry_enabled: data.is_telemetry_enabled,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: GeneralFormValues) => {
|
||||||
|
const payload: Partial<GeneralFormValues> = { ...formData };
|
||||||
|
|
||||||
|
await instanceStore
|
||||||
|
.updateInstanceInfo(payload)
|
||||||
|
.then(() =>
|
||||||
|
setToastAlert({
|
||||||
|
title: "Success",
|
||||||
|
type: "success",
|
||||||
|
message: "Settings updated successfully",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5">
|
<div className="flex flex-col gap-8 m-8">
|
||||||
<div className="my-2 ">
|
<div className="grid grid-col grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-8 w-full">
|
||||||
<label>Instance Name</label>
|
<div className="flex flex-col gap-1">
|
||||||
<Input name="instance_name" />
|
<h4 className="text-sm">Name of instance</h4>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="instance_name"
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<Input
|
||||||
|
id="instance_name"
|
||||||
|
name="instance_name"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.instance_name)}
|
||||||
|
placeholder="Instance Name"
|
||||||
|
className="rounded-md font-medium w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2">
|
|
||||||
<label>Instance ID</label>
|
<div className="flex flex-col gap-1">
|
||||||
<Input name="instance_id" value={data.instance_id} disabled={true} />
|
<h4 className="text-sm">Admin Email</h4>
|
||||||
|
<Input
|
||||||
|
id="primary_email"
|
||||||
|
name="primary_email"
|
||||||
|
type="email"
|
||||||
|
value={instance.primary_email}
|
||||||
|
placeholder="Admin Email"
|
||||||
|
className="w-full cursor-not-allowed !text-custom-text-400"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2">
|
|
||||||
<label>Namespace</label>
|
<div className="flex flex-col gap-1">
|
||||||
<Input name="namespace" />
|
<h4 className="text-sm">Instance Id</h4>
|
||||||
|
<Input
|
||||||
|
id="instance_id"
|
||||||
|
name="instance_id"
|
||||||
|
type="text"
|
||||||
|
value={instance.instance_id}
|
||||||
|
className="rounded-md font-medium w-full cursor-not-allowed !text-custom-text-400"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-8 pt-4">
|
||||||
|
<div>
|
||||||
|
<div className="text-custom-text-100 font-medium text-sm">Share anonymous usage instance</div>
|
||||||
|
<div className="text-custom-text-300 font-normal text-xs">
|
||||||
|
Help us understand how you use Plane so we can build better for you.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="is_telemetry_enabled"
|
||||||
|
render={({ field: { value, onChange } }) => <ToggleSwitch value={value} onChange={onChange} size="sm" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center py-1">
|
||||||
|
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./help-section";
|
export * from "./help-section";
|
||||||
export * from "./sidebar-menu";
|
export * from "./sidebar-menu";
|
||||||
|
export * from "./sidebar-dropdown";
|
||||||
export * from "./general-form";
|
export * from "./general-form";
|
||||||
|
148
web/components/instance/sidebar-dropdown.tsx
Normal file
148
web/components/instance/sidebar-dropdown.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import { LogOut, Settings, Shield, UserCircle2 } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// services
|
||||||
|
import { AuthService } from "services/auth.service";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
|
||||||
|
// Static Data
|
||||||
|
const profileLinks = (workspaceSlug: string, userId: string) => [
|
||||||
|
{
|
||||||
|
name: "View profile",
|
||||||
|
icon: UserCircle2,
|
||||||
|
link: `/${workspaceSlug}/profile/${userId}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
icon: Settings,
|
||||||
|
link: `/${workspaceSlug}/me/profile`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const authService = new AuthService();
|
||||||
|
|
||||||
|
export const InstanceSidebarDropdown = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
// store
|
||||||
|
const {
|
||||||
|
theme: { sidebarCollapsed },
|
||||||
|
workspace: { workspaceSlug },
|
||||||
|
user: { currentUser, currentUserSettings },
|
||||||
|
} = useMobxStore();
|
||||||
|
// hooks
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
// redirect url for normal mode
|
||||||
|
const redirectWorkspaceSlug =
|
||||||
|
workspaceSlug ||
|
||||||
|
currentUserSettings?.workspace?.last_workspace_slug ||
|
||||||
|
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
await authService
|
||||||
|
.signOut()
|
||||||
|
.then(() => {
|
||||||
|
router.push("/");
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Failed to sign out. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-x-3 gap-y-2 px-4 py-4">
|
||||||
|
<div className="w-full h-full truncate">
|
||||||
|
<div
|
||||||
|
className={`flex flex-grow items-center gap-x-2 rounded p-1 truncate ${
|
||||||
|
sidebarCollapsed ? "justify-center" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`flex-shrink-0 `}>
|
||||||
|
<Shield className="h-6 w-6 text-custom-text-100" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<h4 className="text-custom-text-100 font-medium text-base truncate">Instance Admin Settings</h4>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<Menu as="div" className="relative flex-shrink-0">
|
||||||
|
<Menu.Button className="grid place-items-center outline-none">
|
||||||
|
<Avatar
|
||||||
|
name={currentUser?.display_name}
|
||||||
|
src={currentUser?.avatar}
|
||||||
|
size={24}
|
||||||
|
shape="square"
|
||||||
|
className="!text-base"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
className="absolute left-0 z-20 mt-1.5 flex flex-col w-52 origin-top-left rounded-md
|
||||||
|
border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 divide-y divide-custom-sidebar-border-200 shadow-lg text-xs outline-none"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2.5 pb-2">
|
||||||
|
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
|
||||||
|
{profileLinks(workspaceSlug?.toString() ?? "", currentUser?.id ?? "").map((link, index) => (
|
||||||
|
<Menu.Item key={index} as="button" type="button">
|
||||||
|
<Link href={link.link}>
|
||||||
|
<a className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80">
|
||||||
|
<link.icon className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
<Menu.Item
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
||||||
|
onClick={handleSignOut}
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
Sign out
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2 pb-0">
|
||||||
|
<Menu.Item as="button" type="button" className="w-full">
|
||||||
|
<Link href={redirectWorkspaceSlug}>
|
||||||
|
<a className="flex w-full items-center justify-center rounded px-2 py-1 text-sm font-medium text-custom-primary-100 hover:text-custom-primary-200 bg-custom-primary-10 hover:bg-custom-primary-20">
|
||||||
|
Normal Mode
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -37,7 +37,7 @@ export const InstanceAdminSidebarMenu = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-full overflow-y-auto w-full cursor-pointer space-y-2 p-4">
|
||||||
{INSTANCE_ADMIN_LINKS.map((item, index) => {
|
{INSTANCE_ADMIN_LINKS.map((item, index) => {
|
||||||
const isActive = item.name === "Settings" ? router.asPath.includes(item.href) : router.asPath === item.href;
|
const isActive = item.name === "Settings" ? router.asPath.includes(item.href) : router.asPath === item.href;
|
||||||
|
|
||||||
|
@ -151,6 +151,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
value={value}
|
value={value}
|
||||||
setShouldShowAlert={setShowAlert}
|
setShouldShowAlert={setShowAlert}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
|
dragDropEnabled={true}
|
||||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||||
noBorder={!isAllowed}
|
noBorder={!isAllowed}
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
|
@ -15,13 +15,13 @@ import { X } from "lucide-react";
|
|||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types";
|
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: IIssueFilterOptions;
|
appliedFilters: IIssueFilterOptions;
|
||||||
handleClearAllFilters: () => void;
|
handleClearAllFilters: () => void;
|
||||||
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
|
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
|
||||||
labels?: IIssueLabels[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
members?: IUserLite[] | undefined;
|
members?: IUserLite[] | undefined;
|
||||||
projects?: IProject[] | undefined;
|
projects?: IProject[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
|
@ -3,11 +3,11 @@ import { observer } from "mobx-react-lite";
|
|||||||
// icons
|
// icons
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleRemove: (val: string) => void;
|
handleRemove: (val: string) => void;
|
||||||
labels: IIssueLabels[] | undefined;
|
labels: IIssueLabel[] | undefined;
|
||||||
values: string[];
|
values: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
archivedIssueFilters: archivedIssueFiltersStore,
|
archivedIssueFilters: archivedIssueFiltersStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
@ -77,7 +77,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
labels={projectLabels ?? []}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -12,7 +12,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
cycleIssueFilter: cycleIssueFilterStore,
|
cycleIssueFilter: cycleIssueFilterStore,
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
@ -72,7 +72,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
labels={projectLabels ?? []}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -13,7 +13,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
moduleFilter: moduleFilterStore,
|
moduleFilter: moduleFilterStore,
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
@ -73,7 +73,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
labels={projectLabels ?? []}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -14,7 +14,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
@ -77,7 +77,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
labels={projectLabels ?? []}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -18,7 +18,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
projectViews: projectViewsStore,
|
projectViews: projectViewsStore,
|
||||||
@ -99,7 +99,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
appliedFilters={appliedFilters}
|
appliedFilters={appliedFilters}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
labels={projectLabels ?? []}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
states={projectStateStore.states?.[projectId?.toString() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
FilterTargetDate,
|
FilterTargetDate,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types";
|
import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { ILayoutDisplayFiltersOptions } from "constants/issue";
|
import { ILayoutDisplayFiltersOptions } from "constants/issue";
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ type Props = {
|
|||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||||
labels?: IIssueLabels[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
members?: IUserLite[] | undefined;
|
members?: IUserLite[] | undefined;
|
||||||
projects?: IProject[] | undefined;
|
projects?: IProject[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
|
@ -5,7 +5,7 @@ import { FilterHeader, FilterOption } from "components/issues";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
|
||||||
const LabelIcons = ({ color }: { color: string }) => (
|
const LabelIcons = ({ color }: { color: string }) => (
|
||||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
|
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||||
@ -14,7 +14,7 @@ const LabelIcons = ({ color }: { color: string }) => (
|
|||||||
type Props = {
|
type Props = {
|
||||||
appliedFilters: string[] | null;
|
appliedFilters: string[] | null;
|
||||||
handleUpdate: (val: string) => void;
|
handleUpdate: (val: string) => void;
|
||||||
labels: IIssueLabels[] | undefined;
|
labels: IIssueLabel[] | undefined;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
@ -99,7 +100,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
// const estimates =
|
// const estimates =
|
||||||
@ -137,7 +137,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
@ -164,7 +164,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
|
@ -21,7 +21,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
const { workspaceSlug, moduleId } = router.query;
|
const { workspaceSlug, moduleId } = router.query;
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: { workspaceProjects },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
@ -97,9 +98,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
|
||||||
// const estimates =
|
// const estimates =
|
||||||
// currentProjectDetails?.estimate !== null
|
// currentProjectDetails?.estimate !== null
|
||||||
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||||
@ -135,9 +134,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={workspaceProjects}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
@ -162,9 +161,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={workspaceProjects}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
|
@ -21,7 +21,8 @@ export const KanBanLayout: React.FC = observer(() => {
|
|||||||
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
const { workspaceSlug } = router.query as { workspaceSlug: string };
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: { workspaceProjects },
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
issue: issueStore,
|
issue: issueStore,
|
||||||
@ -29,7 +30,6 @@ export const KanBanLayout: React.FC = observer(() => {
|
|||||||
issueKanBanView: issueKanBanViewStore,
|
issueKanBanView: issueKanBanViewStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
|
||||||
|
|
||||||
const issues = issueStore?.getIssues;
|
const issues = issueStore?.getIssues;
|
||||||
|
|
||||||
@ -92,13 +92,11 @@ export const KanBanLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects?.[workspaceSlug] || null : null;
|
// const estimates =
|
||||||
const estimates =
|
// currentProjectDetails?.estimate !== null
|
||||||
currentProjectDetails?.estimate !== null
|
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
||||||
? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
|
// : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -129,9 +127,9 @@ export const KanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={workspaceProjects}
|
||||||
enableQuickIssueCreate
|
enableQuickIssueCreate
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
@ -156,9 +154,9 @@ export const KanBanLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={workspaceProjects}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
|
@ -56,7 +56,7 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
// const labels = projectStore?.projectLabels || null;
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = projectStateStore?.projectStates || null;
|
const projects = projectStateStore?.projectStates || null;
|
||||||
const estimates = null;
|
const estimates = null;
|
||||||
|
@ -5,7 +5,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
|||||||
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
|
import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { getValueFromObject } from "constants/issue";
|
import { getValueFromObject } from "constants/issue";
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||||||
states: IState[] | null;
|
states: IState[] | null;
|
||||||
stateGroups: any;
|
stateGroups: any;
|
||||||
priorities: any;
|
priorities: any;
|
||||||
labels: IIssueLabels[] | null;
|
labels: IIssueLabel[] | null;
|
||||||
members: IUserLite[] | null;
|
members: IUserLite[] | null;
|
||||||
projects: IProject[] | null;
|
projects: IProject[] | null;
|
||||||
issues: any;
|
issues: any;
|
||||||
@ -181,7 +181,7 @@ export interface IKanBanSwimLanes {
|
|||||||
states: IState[] | null;
|
states: IState[] | null;
|
||||||
stateGroups: any;
|
stateGroups: any;
|
||||||
priorities: any;
|
priorities: any;
|
||||||
labels: IIssueLabels[] | null;
|
labels: IIssueLabel[] | null;
|
||||||
members: IUserLite[] | null;
|
members: IUserLite[] | null;
|
||||||
projects: IProject[] | null;
|
projects: IProject[] | null;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { ListGroupByHeaderRoot } from "./headers/group-by-root";
|
import { ListGroupByHeaderRoot } from "./headers/group-by-root";
|
||||||
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues";
|
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
|
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { getValueFromObject } from "constants/issue";
|
import { getValueFromObject } from "constants/issue";
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ export interface IList {
|
|||||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
states: IState[] | null;
|
states: IState[] | null;
|
||||||
labels: IIssueLabels[] | null;
|
labels: IIssueLabel[] | null;
|
||||||
members: IUserLite[] | null;
|
members: IUserLite[] | null;
|
||||||
projects: IProject[] | null;
|
projects: IProject[] | null;
|
||||||
stateGroups: any;
|
stateGroups: any;
|
||||||
|
@ -19,6 +19,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
archivedIssues: archivedIssueStore,
|
archivedIssues: archivedIssueStore,
|
||||||
@ -42,7 +43,6 @@ export const ArchivedIssueListLayout: FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
const estimates =
|
const estimates =
|
||||||
@ -64,7 +64,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||||
|
@ -21,6 +21,7 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
@ -59,7 +60,6 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
const estimates =
|
const estimates =
|
||||||
@ -85,7 +85,7 @@ export const CycleListLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||||
|
@ -21,6 +21,7 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
@ -59,7 +60,6 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
const estimates =
|
const estimates =
|
||||||
@ -85,7 +85,7 @@ export const ModuleListLayout: React.FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||||
|
@ -20,6 +20,7 @@ export const ListLayout: FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
issue: issueStore,
|
issue: issueStore,
|
||||||
@ -49,7 +50,6 @@ export const ListLayout: FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
const estimates =
|
const estimates =
|
||||||
@ -80,7 +80,7 @@ export const ListLayout: FC = observer(() => {
|
|||||||
states={states}
|
states={states}
|
||||||
stateGroups={stateGroups}
|
stateGroups={stateGroups}
|
||||||
priorities={priorities}
|
priorities={priorities}
|
||||||
labels={labels}
|
labels={projectLabels}
|
||||||
members={projectMembers?.map((m) => m.member) ?? null}
|
members={projectMembers?.map((m) => m.member) ?? null}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
enableQuickIssueCreate
|
enableQuickIssueCreate
|
||||||
|
@ -30,7 +30,7 @@ export const ProjectViewListLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const states = projectStateStore?.projectStates || null;
|
const states = projectStateStore?.projectStates || null;
|
||||||
const priorities = ISSUE_PRIORITIES || null;
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
const labels = projectStore?.projectLabels || null;
|
// const labels = projectStore?.projectLabels || null;
|
||||||
const stateGroups = ISSUE_STATE_GROUPS || null;
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
const projects = projectStateStore?.projectStates || null;
|
const projects = projectStateStore?.projectStates || null;
|
||||||
const estimates = null;
|
const estimates = null;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// components
|
// components
|
||||||
@ -44,7 +42,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
noLabelBorder = false,
|
noLabelBorder = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
|
const {
|
||||||
|
workspace: workspaceStore,
|
||||||
|
projectLabel: { fetchProjectLabels, projectLabels },
|
||||||
|
}: RootStore = useMobxStore();
|
||||||
const workspaceSlug = workspaceStore?.workspaceSlug;
|
const workspaceSlug = workspaceStore?.workspaceSlug;
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -53,12 +54,9 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<Boolean>(false);
|
const [isLoading, setIsLoading] = useState<Boolean>(false);
|
||||||
|
|
||||||
const projectLabels = projectId && projectStore?.labels?.[projectId];
|
const fetchLabels = () => {
|
||||||
|
|
||||||
const fetchProjectLabels = () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (workspaceSlug && projectId)
|
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
|
||||||
projectStore.fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = (projectLabels ? projectLabels : []).map((label) => ({
|
const options = (projectLabels ? projectLabels : []).map((label) => ({
|
||||||
@ -169,7 +167,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
? "cursor-pointer"
|
? "cursor-pointer"
|
||||||
: "cursor-pointer hover:bg-custom-background-80"
|
: "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
onClick={() => !projectLabels && fetchProjectLabels()}
|
onClick={() => !projectLabels && fetchLabels()}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||||
|
@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
// components
|
// components
|
||||||
import { SpreadsheetColumn } from "components/issues";
|
import { SpreadsheetColumn } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
|
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
@ -13,7 +13,7 @@ type Props = {
|
|||||||
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||||
issues: IIssue[] | undefined;
|
issues: IIssue[] | undefined;
|
||||||
members?: IUserLite[] | undefined;
|
members?: IUserLite[] | undefined;
|
||||||
labels?: IIssueLabels[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,12 +5,12 @@ import { IssuePropertyLabels } from "../../properties";
|
|||||||
// hooks
|
// hooks
|
||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLabels } from "types";
|
import { IIssue, IIssueLabel } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
onChange: (formData: Partial<IIssue>) => void;
|
onChange: (formData: Partial<IIssue>) => void;
|
||||||
labels: IIssueLabels[] | undefined;
|
labels: IIssueLabel[] | undefined;
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
@ -19,7 +18,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
@ -61,7 +60,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues as IIssueUnGroupedStructure}
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
labels={projectLabels || undefined}
|
||||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||||
handleIssueAction={() => {}}
|
handleIssueAction={() => {}}
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
@ -19,7 +19,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
@ -61,7 +61,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues as IIssueUnGroupedStructure}
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
labels={projectLabels ?? undefined}
|
||||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||||
handleIssueAction={() => {}}
|
handleIssueAction={() => {}}
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
@ -19,7 +19,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
issue: issueStore,
|
issue: issueStore,
|
||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
user: userStore,
|
user: userStore,
|
||||||
@ -63,7 +63,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues as IIssueUnGroupedStructure}
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
labels={projectLabels || undefined}
|
||||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||||
handleIssueAction={() => {}}
|
handleIssueAction={() => {}}
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
@ -19,7 +18,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
issueFilter: issueFilterStore,
|
issueFilter: issueFilterStore,
|
||||||
projectViewIssues: projectViewIssueStore,
|
projectViewIssues: projectViewIssueStore,
|
||||||
issueDetail: issueDetailStore,
|
issueDetail: issueDetailStore,
|
||||||
project: projectStore,
|
projectLabel: { projectLabels },
|
||||||
projectMember: { projectMembers },
|
projectMember: { projectMembers },
|
||||||
projectState: projectStateStore,
|
projectState: projectStateStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
@ -61,7 +60,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues as IIssueUnGroupedStructure}
|
issues={issues as IIssueUnGroupedStructure}
|
||||||
members={projectMembers?.map((m) => m.member)}
|
members={projectMembers?.map((m) => m.member)}
|
||||||
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined}
|
labels={projectLabels || undefined}
|
||||||
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
|
||||||
handleIssueAction={() => {}}
|
handleIssueAction={() => {}}
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueLabels, IState, IUserLite, TIssueOrderByOptions } from "types";
|
import { IIssue, IIssueDisplayFilterOptions, IIssueLabel, IState, IUserLite, TIssueOrderByOptions } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
|
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ type Props = {
|
|||||||
issues: IIssue[] | undefined;
|
issues: IIssue[] | undefined;
|
||||||
property: string;
|
property: string;
|
||||||
members?: IUserLite[] | undefined;
|
members?: IUserLite[] | undefined;
|
||||||
labels?: IIssueLabels[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreat
|
|||||||
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
|
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
@ -14,7 +14,7 @@ type Props = {
|
|||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
issues: IIssue[] | undefined;
|
issues: IIssue[] | undefined;
|
||||||
members?: IUserLite[] | undefined;
|
members?: IUserLite[] | undefined;
|
||||||
labels?: IIssueLabels[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
|
||||||
|
@ -140,6 +140,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
</div>
|
</div>
|
||||||
<span>{errors.name ? errors.name.message : null}</span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
|
dragDropEnabled={true}
|
||||||
cancelUploadImage={fileService.cancelUpload}
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
|
@ -29,7 +29,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
project: { labels, fetchProjectLabels },
|
projectLabel: { labels, fetchProjectLabels },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
@ -15,7 +15,7 @@ import { IssueLabelSelect } from "../select";
|
|||||||
// icons
|
// icons
|
||||||
import { Plus, X } from "lucide-react";
|
import { Plus, X } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueLabels } from "types";
|
import { IIssue, IIssueLabel } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -28,7 +28,7 @@ type Props = {
|
|||||||
uneditable: boolean;
|
uneditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabel> = {
|
||||||
name: "",
|
name: "",
|
||||||
color: "#ff0000",
|
color: "#ff0000",
|
||||||
};
|
};
|
||||||
@ -57,20 +57,20 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
|||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<Partial<IIssueLabels>>({
|
} = useForm<Partial<IIssueLabel>>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabel[]>(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId
|
||||||
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
|
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNewLabel = async (formData: Partial<IIssueLabels>) => {
|
const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
await issueLabelService
|
await issueLabelService
|
||||||
|
@ -12,7 +12,7 @@ import { Button, Input } from "@plane/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import type { IIssueLabels, IState } from "types";
|
import type { IIssueLabel, IState } from "types";
|
||||||
// constants
|
// constants
|
||||||
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
|
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -22,7 +22,7 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
onSuccess?: (response: IIssueLabels) => void;
|
onSuccess?: (response: IIssueLabel) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IState> = {
|
const defaultValues: Partial<IState> = {
|
||||||
@ -47,7 +47,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
|
|||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<IIssueLabels>({
|
} = useForm<IIssueLabel>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const onSubmit = async (formData: IIssueLabels) => {
|
const onSubmit = async (formData: IIssueLabel) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
await projectLabelStore
|
await projectLabelStore
|
||||||
|
@ -11,7 +11,7 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { Button, Input } from "@plane/ui";
|
import { Button, Input } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
|
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -20,11 +20,11 @@ type Props = {
|
|||||||
labelForm: boolean;
|
labelForm: boolean;
|
||||||
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
labelToUpdate: IIssueLabels | null;
|
labelToUpdate?: IIssueLabel;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabel> = {
|
||||||
name: "",
|
name: "",
|
||||||
color: "rgb(var(--color-text-200))",
|
color: "rgb(var(--color-text-200))",
|
||||||
};
|
};
|
||||||
@ -50,7 +50,7 @@ export const CreateUpdateLabelInline = observer(
|
|||||||
watch,
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
setFocus,
|
setFocus,
|
||||||
} = useForm<IIssueLabels>({
|
} = useForm<IIssueLabel>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ export const CreateUpdateLabelInline = observer(
|
|||||||
if (onClose) onClose();
|
if (onClose) onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => {
|
const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
await projectLabelStore
|
await projectLabelStore
|
||||||
@ -79,7 +79,7 @@ export const CreateUpdateLabelInline = observer(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => {
|
const handleLabelUpdate: SubmitHandler<IIssueLabel> = async (formData) => {
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
await projectLabelStore
|
await projectLabelStore
|
||||||
@ -128,9 +128,7 @@ export const CreateUpdateLabelInline = observer(
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)();
|
handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)();
|
||||||
}}
|
}}
|
||||||
className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${
|
className={`flex scroll-m-8 items-center gap-2 bg-custom-background-100 w-full ${labelForm ? "" : "hidden"}`}
|
||||||
labelForm ? "" : "hidden"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
|
<Popover className="relative z-10 flex h-full w-full items-center justify-center">
|
||||||
@ -198,10 +196,10 @@ export const CreateUpdateLabelInline = observer(
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="neutral-primary" onClick={() => handleClose()}>
|
<Button variant="neutral-primary" onClick={() => handleClose()} size="sm">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" type="submit" loading={isSubmitting}>
|
<Button variant="primary" type="submit" size="sm" loading={isSubmitting}>
|
||||||
{isUpdating ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"}
|
{isUpdating ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -12,12 +12,12 @@ import useToast from "hooks/use-toast";
|
|||||||
// ui
|
// ui
|
||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import type { IIssueLabels } from "types";
|
import type { IIssueLabel } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
data: IIssueLabels | null;
|
data: IIssueLabel | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteLabelModal: React.FC<Props> = observer((props) => {
|
export const DeleteLabelModal: React.FC<Props> = observer((props) => {
|
||||||
|
@ -4,5 +4,5 @@ export * from "./delete-label-modal";
|
|||||||
export * from "./label-select";
|
export * from "./label-select";
|
||||||
export * from "./labels-list-modal";
|
export * from "./labels-list-modal";
|
||||||
export * from "./project-setting-label-group";
|
export * from "./project-setting-label-group";
|
||||||
export * from "./project-setting-label-list-item";
|
export * from "./project-setting-label-item";
|
||||||
export * from "./project-setting-label-list";
|
export * from "./project-setting-label-list";
|
||||||
|
24
web/components/labels/label-block/drag-handle.tsx
Normal file
24
web/components/labels/label-block/drag-handle.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
|
||||||
|
import { MoreVertical } from "lucide-react";
|
||||||
|
|
||||||
|
interface IDragHandle {
|
||||||
|
isDragging: boolean;
|
||||||
|
dragHandleProps: DraggableProvidedDragHandleProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DragHandle = (props: IDragHandle) => {
|
||||||
|
const { isDragging, dragHandleProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`rounded text-custom-sidebar-text-200 flex flex-shrink-0 mr-1 group-hover:opacity-100 ${
|
||||||
|
isDragging ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
{...dragHandleProps}
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-3.5 w-3.5 stroke-custom-text-400" />
|
||||||
|
<MoreVertical className="h-3.5 w-3.5 stroke-custom-text-400 -ml-5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
80
web/components/labels/label-block/label-item-block.tsx
Normal file
80
web/components/labels/label-block/label-item-block.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { LucideIcon, X } from "lucide-react";
|
||||||
|
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
|
||||||
|
//ui
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
//types
|
||||||
|
import { IIssueLabel } from "types";
|
||||||
|
//hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
//components
|
||||||
|
import { DragHandle } from "./drag-handle";
|
||||||
|
import { LabelName } from "./label-name";
|
||||||
|
|
||||||
|
//types
|
||||||
|
export interface ICustomMenuItem {
|
||||||
|
CustomIcon: LucideIcon;
|
||||||
|
onClick: (label: IIssueLabel) => void;
|
||||||
|
isVisible: boolean;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILabelItemBlock {
|
||||||
|
label: IIssueLabel;
|
||||||
|
isDragging: boolean;
|
||||||
|
customMenuItems: ICustomMenuItem[];
|
||||||
|
dragHandleProps: DraggableProvidedDragHandleProps;
|
||||||
|
handleLabelDelete: (label: IIssueLabel) => void;
|
||||||
|
isLabelGroup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LabelItemBlock = (props: ILabelItemBlock) => {
|
||||||
|
const { label, isDragging, customMenuItems, dragHandleProps, handleLabelDelete, isLabelGroup } = props;
|
||||||
|
|
||||||
|
//state
|
||||||
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
|
||||||
|
//refs
|
||||||
|
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center group">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DragHandle isDragging={isDragging} dragHandleProps={dragHandleProps} />
|
||||||
|
<LabelName color={label.color} name={label.name} isGroup={isLabelGroup ?? false} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={actionSectionRef}
|
||||||
|
className={`absolute right-3 flex items-start gap-3.5 px-4 ${
|
||||||
|
isMenuActive || isLabelGroup
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
|
||||||
|
} ${isLabelGroup && "-top-0.5"}`}
|
||||||
|
>
|
||||||
|
<CustomMenu ellipsis buttonClassName="h-4 w-4 leading-4 text-custom-sidebar-text-400">
|
||||||
|
{customMenuItems.map(
|
||||||
|
({ isVisible, onClick, CustomIcon, text }) =>
|
||||||
|
isVisible && (
|
||||||
|
<CustomMenu.MenuItem onClick={() => onClick(label)}>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<CustomIcon className="h-4 w-4" />
|
||||||
|
<span>{text}</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
{!isLabelGroup && (
|
||||||
|
<div className="py-0.5">
|
||||||
|
<button className="flex h-4 w-4 items-center justify-start gap-2" onClick={() => handleLabelDelete(label)}>
|
||||||
|
<X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
27
web/components/labels/label-block/label-name.tsx
Normal file
27
web/components/labels/label-block/label-name.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from "lucide-react";
|
||||||
|
|
||||||
|
interface ILabelName {
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
isGroup: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LabelName = (props: ILabelName) => {
|
||||||
|
const { name, color, isGroup } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isGroup ? (
|
||||||
|
<Component className="h-3.5 w-3.5" color={color} />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color && color !== "" ? color : "#000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h6 className="text-sm">{name}</h6>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -6,12 +6,12 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (data: string[]) => void;
|
onChange: (data: string[]) => void;
|
||||||
labels: IIssueLabels[] | undefined;
|
labels: IIssueLabel[] | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
|
@ -11,12 +11,12 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import { LayerStackIcon } from "@plane/ui";
|
import { LayerStackIcon } from "@plane/ui";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
parent: IIssueLabels | undefined;
|
parent: IIssueLabel | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelsListModal: React.FC<Props> = observer((props) => {
|
export const LabelsListModal: React.FC<Props> = observer((props) => {
|
||||||
@ -27,7 +27,9 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
// store
|
// store
|
||||||
const { projectLabel: projectLabelStore, project: projectStore } = useMobxStore();
|
const {
|
||||||
|
projectLabel: { projectLabels, fetchProjectLabels, updateLabel },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -35,28 +37,24 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
|
|||||||
// api call to fetch project details
|
// api call to fetch project details
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
|
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
|
||||||
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null;
|
const filteredLabels: IIssueLabel[] =
|
||||||
|
|
||||||
const filteredLabels: IIssueLabels[] =
|
|
||||||
query === ""
|
query === ""
|
||||||
? issueLabels ?? []
|
? projectLabels ?? []
|
||||||
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
: projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
setQuery("");
|
setQuery("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const addChildLabel = async (label: IIssueLabels) => {
|
const addChildLabel = async (label: IIssueLabel) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
await projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
|
await updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
|
||||||
parent: parent?.id!,
|
parent: parent?.id!,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -108,7 +106,7 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
<ul className="text-sm text-gray-700">
|
<ul className="text-sm text-gray-700">
|
||||||
{filteredLabels.map((label) => {
|
{filteredLabels.map((label) => {
|
||||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
const children = projectLabels?.filter((l) => l.parent === label.id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(label.parent === "" || label.parent === null) && // issue does not have any other parent
|
(label.parent === "" || label.parent === null) && // issue does not have any other parent
|
||||||
@ -128,7 +126,6 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addChildLabel(label);
|
addChildLabel(label);
|
||||||
handleClose();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -1,77 +1,115 @@
|
|||||||
import React from "react";
|
import React, { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "@plane/ui";
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDown, Component, Pencil, Plus, Trash2, X } from "lucide-react";
|
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
import {
|
||||||
|
Draggable,
|
||||||
|
DraggableProvided,
|
||||||
|
DraggableProvidedDragHandleProps,
|
||||||
|
DraggableStateSnapshot,
|
||||||
|
Droppable,
|
||||||
|
} from "@hello-pangea/dnd";
|
||||||
|
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
|
||||||
|
import { CreateUpdateLabelInline } from "./create-update-label-inline";
|
||||||
|
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
||||||
|
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: IIssueLabels;
|
label: IIssueLabel;
|
||||||
labelChildren: IIssueLabels[];
|
labelChildren: IIssueLabel[];
|
||||||
handleLabelDelete: () => void;
|
handleLabelDelete: (label: IIssueLabel) => void;
|
||||||
editLabel: (label: IIssueLabels) => void;
|
dragHandleProps: DraggableProvidedDragHandleProps;
|
||||||
addLabelToGroup: (parentLabel: IIssueLabels) => void;
|
draggableSnapshot: DraggableStateSnapshot;
|
||||||
|
isUpdating: boolean;
|
||||||
|
setIsUpdating: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isDropDisabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
||||||
const { label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete } = props;
|
const {
|
||||||
|
label,
|
||||||
|
labelChildren,
|
||||||
|
handleLabelDelete,
|
||||||
|
draggableSnapshot: groupDragSnapshot,
|
||||||
|
dragHandleProps,
|
||||||
|
isUpdating,
|
||||||
|
setIsUpdating,
|
||||||
|
isDropDisabled,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// router
|
const [isEditLabelForm, setEditLabelForm] = useState(false);
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
// store
|
const renderDraggable = useDraggableInPortal();
|
||||||
const { projectLabel: projectLabelStore } = useMobxStore();
|
|
||||||
|
|
||||||
const removeFromGroup = (label: IIssueLabels) => {
|
const customMenuItems: ICustomMenuItem[] = [
|
||||||
if (!workspaceSlug || !projectId) return;
|
{
|
||||||
|
CustomIcon: Pencil,
|
||||||
projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
|
onClick: () => {
|
||||||
parent: null,
|
setEditLabelForm(true);
|
||||||
});
|
setIsUpdating(true);
|
||||||
};
|
},
|
||||||
|
isVisible: true,
|
||||||
|
text: "Edit label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CustomIcon: Trash2,
|
||||||
|
onClick: handleLabelDelete,
|
||||||
|
isVisible: true,
|
||||||
|
text: "Delete label",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
as="div"
|
as="div"
|
||||||
className="rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3.5 py-3 text-custom-text-100"
|
className={`rounded border-[0.5px] border-custom-border-200 text-custom-text-100 ${
|
||||||
|
groupDragSnapshot.combineTargetFor ? "bg-custom-background-80" : "bg-custom-background-100"
|
||||||
|
}`}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex cursor-pointer items-center justify-between gap-2">
|
<Droppable
|
||||||
<div className="flex items-center gap-2">
|
key={`label.group.droppable.${label.id}`}
|
||||||
<Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" />
|
droppableId={`label.group.droppable.${label.id}`}
|
||||||
<h6>{label.name}</h6>
|
isCombineEnabled={!groupDragSnapshot.isDragging && !isUpdating}
|
||||||
</div>
|
isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
|
{(droppableProvided) => (
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
<div
|
||||||
<span className="flex items-center justify-start gap-2">
|
className={`py-3 pl-1 pr-3 ${!isUpdating && "max-h-full overflow-y-hidden"}`}
|
||||||
<Plus className="h-4 w-4" />
|
ref={droppableProvided.innerRef}
|
||||||
<span>Add more labels</span>
|
{...droppableProvided.droppableProps}
|
||||||
</span>
|
>
|
||||||
</CustomMenu.MenuItem>
|
<>
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
<div className="relative flex cursor-pointer items-center justify-between gap-2">
|
||||||
<span className="flex items-center justify-start gap-2">
|
{isEditLabelForm ? (
|
||||||
<Pencil className="h-4 w-4" />
|
<CreateUpdateLabelInline
|
||||||
<span>Edit label</span>
|
labelForm={isEditLabelForm}
|
||||||
</span>
|
setLabelForm={setEditLabelForm}
|
||||||
</CustomMenu.MenuItem>
|
isUpdating={true}
|
||||||
<CustomMenu.MenuItem onClick={handleLabelDelete}>
|
labelToUpdate={label}
|
||||||
<span className="flex items-center justify-start gap-2">
|
onClose={() => {
|
||||||
<Trash2 className="h-4 w-4" />
|
setEditLabelForm(false);
|
||||||
<span>Delete label</span>
|
setIsUpdating(false);
|
||||||
</span>
|
}}
|
||||||
</CustomMenu.MenuItem>
|
/>
|
||||||
</CustomMenu>
|
) : (
|
||||||
|
<LabelItemBlock
|
||||||
|
label={label}
|
||||||
|
isDragging={groupDragSnapshot.isDragging}
|
||||||
|
customMenuItems={customMenuItems}
|
||||||
|
dragHandleProps={dragHandleProps}
|
||||||
|
handleLabelDelete={handleLabelDelete}
|
||||||
|
isLabelGroup={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Disclosure.Button>
|
<Disclosure.Button>
|
||||||
<span>
|
<span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
@ -80,7 +118,6 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
|||||||
</span>
|
</span>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
enter="transition duration-100 ease-out"
|
enter="transition duration-100 ease-out"
|
||||||
@ -92,55 +129,36 @@ export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="mt-2.5 ml-6">
|
<div className="mt-2.5 ml-6">
|
||||||
{labelChildren.map((child) => (
|
{labelChildren.map((child, index) => (
|
||||||
<div
|
<div key={child.id} className={`group w-full flex items-center text-sm`}>
|
||||||
key={child.id}
|
<Draggable
|
||||||
className="group flex items-center justify-between border-b-[0.5px] border-custom-border-200 px-4 py-2.5 text-sm last:border-0"
|
draggableId={`label.draggable.${child.id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={groupDragSnapshot.isDragging || isUpdating}
|
||||||
>
|
>
|
||||||
<h5 className="flex items-center gap-3">
|
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||||
<span
|
<div className="w-full py-1" ref={provided.innerRef} {...provided.draggableProps}>
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
<ProjectSettingLabelItem
|
||||||
style={{
|
label={child}
|
||||||
backgroundColor: child.color && child.color !== "" ? child.color : "#000000",
|
handleLabelDelete={() => handleLabelDelete(child)}
|
||||||
}}
|
draggableSnapshot={snapshot}
|
||||||
|
dragHandleProps={provided.dragHandleProps!}
|
||||||
|
setIsUpdating={setIsUpdating}
|
||||||
|
isChild
|
||||||
/>
|
/>
|
||||||
{child.name}
|
|
||||||
</h5>
|
|
||||||
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100">
|
|
||||||
<div className="h-4 w-4">
|
|
||||||
<CustomMenu
|
|
||||||
customButton={
|
|
||||||
<div className="h-4 w-4">
|
|
||||||
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span>Remove from group</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
<span>Edit label</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}>
|
|
||||||
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</Draggable>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
{droppableProvided.placeholder}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
|
91
web/components/labels/project-setting-label-item.tsx
Normal file
91
web/components/labels/project-setting-label-item.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||||
|
// types
|
||||||
|
import { IIssueLabel } from "types";
|
||||||
|
//icons
|
||||||
|
import { X, Pencil } from "lucide-react";
|
||||||
|
//components
|
||||||
|
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
|
||||||
|
import { CreateUpdateLabelInline } from "./create-update-label-inline";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: IIssueLabel;
|
||||||
|
handleLabelDelete: (label: IIssueLabel) => void;
|
||||||
|
draggableSnapshot: DraggableStateSnapshot;
|
||||||
|
dragHandleProps: DraggableProvidedDragHandleProps;
|
||||||
|
setIsUpdating: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isChild: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
||||||
|
const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props;
|
||||||
|
|
||||||
|
const { combineTargetFor, isDragging } = draggableSnapshot;
|
||||||
|
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
// store
|
||||||
|
const { projectLabel: projectLabelStore } = useMobxStore();
|
||||||
|
|
||||||
|
//state
|
||||||
|
const [isEditLabelForm, setEditLabelForm] = useState(false);
|
||||||
|
|
||||||
|
const removeFromGroup = (label: IIssueLabel) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
|
||||||
|
parent: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const customMenuItems: ICustomMenuItem[] = [
|
||||||
|
{
|
||||||
|
CustomIcon: X,
|
||||||
|
onClick: removeFromGroup,
|
||||||
|
isVisible: !!label.parent,
|
||||||
|
text: "Remove from group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CustomIcon: Pencil,
|
||||||
|
onClick: () => {
|
||||||
|
setEditLabelForm(true);
|
||||||
|
setIsUpdating(true);
|
||||||
|
},
|
||||||
|
isVisible: true,
|
||||||
|
text: "Edit label",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative group flex items-center justify-between gap-2 space-y-3 rounded border-[0.5px] border-custom-border-200 ${
|
||||||
|
!isChild && combineTargetFor ? "bg-custom-background-80" : ""
|
||||||
|
} ${isDragging ? "shadow-custom-shadow-xs bg-custom-background-80" : ""} bg-custom-background-100 px-1 py-2.5`}
|
||||||
|
>
|
||||||
|
{isEditLabelForm ? (
|
||||||
|
<CreateUpdateLabelInline
|
||||||
|
labelForm={isEditLabelForm}
|
||||||
|
setLabelForm={setEditLabelForm}
|
||||||
|
isUpdating={true}
|
||||||
|
labelToUpdate={label}
|
||||||
|
onClose={() => {
|
||||||
|
setEditLabelForm(false);
|
||||||
|
setIsUpdating(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LabelItemBlock
|
||||||
|
label={label}
|
||||||
|
isDragging={isDragging}
|
||||||
|
customMenuItems={customMenuItems}
|
||||||
|
dragHandleProps={dragHandleProps}
|
||||||
|
handleLabelDelete={handleLabelDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,73 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
|
|
||||||
//hook
|
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { IIssueLabels } from "types";
|
|
||||||
//icons
|
|
||||||
import { Component, X, Pencil } from "lucide-react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
label: IIssueLabels;
|
|
||||||
addLabelToGroup: (parentLabel: IIssueLabels) => void;
|
|
||||||
editLabel: (label: IIssueLabels) => void;
|
|
||||||
handleLabelDelete: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
|
|
||||||
const { label, addLabelToGroup, editLabel, handleLabelDelete } = props;
|
|
||||||
|
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
|
||||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative group flex items-center justify-between gap-2 space-y-3 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span
|
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h6 className="text-sm">{label.name}</h6>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={actionSectionRef}
|
|
||||||
className={`absolute -top-0.5 right-3 flex items-start gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${
|
|
||||||
isMenuActive ? "opacity-100" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CustomMenu
|
|
||||||
customButton={
|
|
||||||
<div className="h-4 w-4" onClick={() => setIsMenuActive(!isMenuActive)}>
|
|
||||||
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
|
||||||
<span>Convert to group</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
<span>Edit label</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
<div className="py-0.5">
|
|
||||||
<button className="flex h-4 w-4 items-center justify-start gap-2" onClick={handleLabelDelete}>
|
|
||||||
<X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,75 +1,96 @@
|
|||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Draggable,
|
||||||
|
DraggableProvided,
|
||||||
|
DraggableStateSnapshot,
|
||||||
|
DropResult,
|
||||||
|
Droppable,
|
||||||
|
} from "@hello-pangea/dnd";
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import {
|
import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup } from "components/labels";
|
||||||
CreateUpdateLabelInline,
|
|
||||||
DeleteLabelModal,
|
|
||||||
LabelsListModal,
|
|
||||||
ProjectSettingLabelItem,
|
|
||||||
ProjectSettingLabelGroup,
|
|
||||||
} from "components/labels";
|
|
||||||
// ui
|
// ui
|
||||||
import { Button, Loader } from "@plane/ui";
|
import { Button, Loader } from "@plane/ui";
|
||||||
import { EmptyState } from "components/common";
|
import { EmptyState } from "components/common";
|
||||||
// images
|
// images
|
||||||
import emptyLabel from "public/empty-state/label.svg";
|
import emptyLabel from "public/empty-state/label.svg";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
//component
|
||||||
|
import { ProjectSettingLabelItem } from "./project-setting-label-item";
|
||||||
|
import useDraggableInPortal from "hooks/use-draggable-portal";
|
||||||
|
|
||||||
|
const LABELS_ROOT = "labels.root";
|
||||||
|
|
||||||
export const ProjectSettingsLabelList: React.FC = observer(() => {
|
export const ProjectSettingsLabelList: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const renderDraggable = useDraggableInPortal();
|
||||||
|
|
||||||
// store
|
// store
|
||||||
const { project: projectStore } = useMobxStore();
|
const {
|
||||||
|
projectLabel: { fetchProjectLabels, projectLabels, updateLabelPosition, projectLabelsTree },
|
||||||
|
} = useMobxStore();
|
||||||
// states
|
// states
|
||||||
const [labelForm, setLabelForm] = useState(false);
|
const [showLabelForm, setLabelForm] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [labelsListModal, setLabelsListModal] = useState(false);
|
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null);
|
||||||
const [labelToUpdate, setLabelToUpdate] = useState<IIssueLabels | null>(null);
|
const [isDraggingGroup, setIsDraggingGroup] = useState(false);
|
||||||
const [parentLabel, setParentLabel] = useState<IIssueLabels | undefined>(undefined);
|
|
||||||
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabels | null>(null);
|
|
||||||
|
|
||||||
// ref
|
// ref
|
||||||
const scrollToRef = useRef<HTMLFormElement>(null);
|
const scrollToRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
// api call to fetch project details
|
// api call to fetch project details
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
|
workspaceSlug && projectId ? "PROJECT_LABELS" : null,
|
||||||
workspaceSlug && projectId
|
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
|
||||||
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// derived values
|
|
||||||
const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null;
|
|
||||||
|
|
||||||
const newLabel = () => {
|
const newLabel = () => {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setLabelForm(true);
|
setLabelForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addLabelToGroup = (parentLabel: IIssueLabels) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
setLabelsListModal(true);
|
const { combine, draggableId, destination, source } = result;
|
||||||
setParentLabel(parentLabel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const editLabel = (label: IIssueLabels) => {
|
// return if dropped outside the DragDropContext
|
||||||
setLabelForm(true);
|
if (!combine && !destination) return;
|
||||||
setIsUpdating(true);
|
|
||||||
setLabelToUpdate(label);
|
const childLabel = draggableId.split(".")[2];
|
||||||
|
let parentLabel: string | undefined | null = destination?.droppableId?.split(".")[3];
|
||||||
|
const index = destination?.index || 0;
|
||||||
|
|
||||||
|
const prevParentLabel: string | undefined | null = source?.droppableId?.split(".")[3];
|
||||||
|
const prevIndex = source?.index;
|
||||||
|
|
||||||
|
if (combine && combine.draggableId) parentLabel = combine?.draggableId?.split(".")[2];
|
||||||
|
|
||||||
|
if (destination?.droppableId === LABELS_ROOT) parentLabel = null;
|
||||||
|
|
||||||
|
if (result.reason == "DROP" && childLabel != parentLabel) {
|
||||||
|
updateLabelPosition(
|
||||||
|
workspaceSlug?.toString()!,
|
||||||
|
projectId?.toString()!,
|
||||||
|
childLabel,
|
||||||
|
parentLabel,
|
||||||
|
index,
|
||||||
|
prevParentLabel == parentLabel,
|
||||||
|
prevIndex
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LabelsListModal isOpen={labelsListModal} parent={parentLabel} handleClose={() => setLabelsListModal(false)} />
|
|
||||||
<DeleteLabelModal
|
<DeleteLabelModal
|
||||||
isOpen={!!selectDeleteLabel}
|
isOpen={!!selectDeleteLabel}
|
||||||
data={selectDeleteLabel ?? null}
|
data={selectDeleteLabel ?? null}
|
||||||
@ -82,64 +103,105 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
Add label
|
Add label
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 py-6 h-full w-full">
|
<div className="w-full">
|
||||||
{labelForm && (
|
{showLabelForm && (
|
||||||
|
<div className="w-full rounded border border-custom-border-200 px-3.5 py-2">
|
||||||
<CreateUpdateLabelInline
|
<CreateUpdateLabelInline
|
||||||
labelForm={labelForm}
|
labelForm={showLabelForm}
|
||||||
setLabelForm={setLabelForm}
|
setLabelForm={setLabelForm}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
labelToUpdate={labelToUpdate}
|
|
||||||
ref={scrollToRef}
|
ref={scrollToRef}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setLabelForm(false);
|
setLabelForm(false);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setLabelToUpdate(null);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* labels */}
|
{/* labels */}
|
||||||
{issueLabels &&
|
<>
|
||||||
issueLabels.map((label) => {
|
{projectLabelsTree && (
|
||||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
<DragDropContext
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
if (children && children.length === 0) {
|
autoScrollerOptions={{
|
||||||
if (!label.parent)
|
startFromPercentage: 1,
|
||||||
return (
|
disabled: false,
|
||||||
<ProjectSettingLabelItem
|
maxScrollAtPercentage: 0,
|
||||||
key={label.id}
|
maxPixelScroll: 2,
|
||||||
label={label}
|
|
||||||
addLabelToGroup={() => addLabelToGroup(label)}
|
|
||||||
editLabel={(label) => {
|
|
||||||
editLabel(label);
|
|
||||||
scrollToRef.current?.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
handleLabelDelete={() => setSelectDeleteLabel(label)}
|
>
|
||||||
/>
|
<Droppable
|
||||||
);
|
droppableId={LABELS_ROOT}
|
||||||
} else {
|
isCombineEnabled={!isDraggingGroup}
|
||||||
|
ignoreContainerClipping={true}
|
||||||
|
isDropDisabled={isUpdating}
|
||||||
|
>
|
||||||
|
{(droppableProvided, droppableSnapshot) => (
|
||||||
|
<div className={`mt-3`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||||
|
{projectLabelsTree.map((label, index) => {
|
||||||
|
if (label.children && label.children.length) {
|
||||||
return (
|
return (
|
||||||
|
<Draggable
|
||||||
|
key={`label.draggable.${label.id}`}
|
||||||
|
draggableId={`label.draggable.${label.id}.group`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={isUpdating}
|
||||||
|
>
|
||||||
|
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
|
||||||
|
const isGroup = droppableSnapshot.draggingFromThisWith?.split(".")[3] === "group";
|
||||||
|
setIsDraggingGroup(isGroup);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
|
||||||
<ProjectSettingLabelGroup
|
<ProjectSettingLabelGroup
|
||||||
key={label.id}
|
key={label.id}
|
||||||
label={label}
|
label={label}
|
||||||
labelChildren={children}
|
labelChildren={label.children || []}
|
||||||
addLabelToGroup={addLabelToGroup}
|
isDropDisabled={isGroup}
|
||||||
editLabel={(label) => {
|
dragHandleProps={provided.dragHandleProps!}
|
||||||
editLabel(label);
|
handleLabelDelete={(label: IIssueLabel) => setSelectDeleteLabel(label)}
|
||||||
scrollToRef.current?.scrollIntoView({
|
draggableSnapshot={snapshot}
|
||||||
behavior: "smooth",
|
isUpdating={isUpdating}
|
||||||
});
|
setIsUpdating={setIsUpdating}
|
||||||
}}
|
|
||||||
handleLabelDelete={() => setSelectDeleteLabel(label)}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Draggable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
key={`label.draggable.${label.id}`}
|
||||||
|
draggableId={`label.draggable.${label.id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={isUpdating}
|
||||||
|
>
|
||||||
|
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||||
|
<div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
|
||||||
|
<ProjectSettingLabelItem
|
||||||
|
dragHandleProps={provided.dragHandleProps!}
|
||||||
|
draggableSnapshot={snapshot}
|
||||||
|
label={label}
|
||||||
|
setIsUpdating={setIsUpdating}
|
||||||
|
handleLabelDelete={(label) => setSelectDeleteLabel(label)}
|
||||||
|
isChild={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
{droppableProvided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
{/* loading state */}
|
{/* loading state */}
|
||||||
{!issueLabels && (
|
{!projectLabels && (
|
||||||
<Loader className="space-y-5">
|
<Loader className="space-y-5">
|
||||||
<Loader.Item height="42px" />
|
<Loader.Item height="42px" />
|
||||||
<Loader.Item height="42px" />
|
<Loader.Item height="42px" />
|
||||||
@ -149,7 +211,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* empty state */}
|
{/* empty state */}
|
||||||
{issueLabels && issueLabels.length === 0 && (
|
{projectLabels && projectLabels.length === 0 && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title="No labels yet"
|
title="No labels yet"
|
||||||
description="Create labels to help organize and filter issues in you project"
|
description="Create labels to help organize and filter issues in you project"
|
||||||
|
@ -24,7 +24,6 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
|
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
|
||||||
projectMember: { projectMembers, fetchProjectMembers },
|
projectMember: { projectMembers, fetchProjectMembers },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ import { FC } from "react";
|
|||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssueLabels } from "types";
|
import { IIssueLabel } from "types";
|
||||||
|
|
||||||
type IssueLabelsListProps = {
|
type IssueLabelsListProps = {
|
||||||
labels?: (IIssueLabels | undefined)[];
|
labels?: (IIssueLabel | undefined)[];
|
||||||
length?: number;
|
length?: number;
|
||||||
showLength?: boolean;
|
showLength?: boolean;
|
||||||
};
|
};
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user