mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' into refactor/editor-wrapper
This commit is contained in:
commit
c199d90628
17
.github/workflows/create-sync-pr.yml
vendored
17
.github/workflows/create-sync-pr.yml
vendored
@ -3,14 +3,14 @@ name: Create Sync Action
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop # Change this to preview
|
- preview
|
||||||
types:
|
types:
|
||||||
- closed
|
- closed
|
||||||
env:
|
env:
|
||||||
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
SOURCE_BRANCH_NAME: ${{github.event.pull_request.base.ref}}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_pr:
|
sync_changes:
|
||||||
# Only run the job when a PR is merged
|
# Only run the job when a PR is merged
|
||||||
if: github.event.pull_request.merged == true
|
if: github.event.pull_request.merged == true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -33,23 +33,14 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install gh -y
|
sudo apt install gh -y
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Push Changes to Target Repo
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
|
||||||
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
|
||||||
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
|
|
||||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
||||||
|
|
||||||
git checkout $SOURCE_BRANCH
|
git checkout $SOURCE_BRANCH
|
||||||
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
||||||
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
|
||||||
|
|
||||||
PR_TITLE=${{secrets.SYNC_PR_TITLE}}
|
|
||||||
|
|
||||||
gh pr create \
|
|
||||||
--base $TARGET_BASE_BRANCH \
|
|
||||||
--head $TARGET_BRANCH \
|
|
||||||
--title "$PR_TITLE" \
|
|
||||||
--repo $TARGET_REPO
|
|
||||||
|
@ -30,10 +30,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/core": "^2.1.13",
|
"@tiptap/core": "^2.1.13",
|
||||||
"@tiptap/extension-blockquote": "^2.1.13",
|
"@tiptap/extension-blockquote": "^2.1.13",
|
||||||
|
"@tiptap/extension-code": "^2.1.13",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
"@tiptap/extension-code-block-lowlight": "^2.1.13",
|
||||||
"@tiptap/extension-color": "^2.1.13",
|
"@tiptap/extension-color": "^2.1.13",
|
||||||
"@tiptap/extension-image": "^2.1.13",
|
"@tiptap/extension-image": "^2.1.13",
|
||||||
"@tiptap/extension-link": "^2.1.13",
|
|
||||||
"@tiptap/extension-list-item": "^2.1.13",
|
"@tiptap/extension-list-item": "^2.1.13",
|
||||||
"@tiptap/extension-mention": "^2.1.13",
|
"@tiptap/extension-mention": "^2.1.13",
|
||||||
"@tiptap/extension-task-item": "^2.1.13",
|
"@tiptap/extension-task-item": "^2.1.13",
|
||||||
@ -48,6 +48,7 @@
|
|||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"jsx-dom-cjs": "^8.0.3",
|
"jsx-dom-cjs": "^8.0.3",
|
||||||
|
"linkifyjs": "^4.1.3",
|
||||||
"lowlight": "^3.0.0",
|
"lowlight": "^3.0.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react-moveable": "^0.54.2",
|
"react-moveable": "^0.54.2",
|
||||||
|
@ -12,6 +12,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror code::before,
|
||||||
|
.ProseMirror code::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror .is-empty::before {
|
.ProseMirror .is-empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
|
31
packages/editor/core/src/ui/extensions/code-inline/index.tsx
Normal file
31
packages/editor/core/src/ui/extensions/code-inline/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { markInputRule, markPasteRule } from "@tiptap/core";
|
||||||
|
import Code from "@tiptap/extension-code";
|
||||||
|
|
||||||
|
export const inputRegex = /(?<!`)`([^`]*)`(?!`)/;
|
||||||
|
export const pasteRegex = /(?<!`)`([^`]+)`(?!`)/g;
|
||||||
|
|
||||||
|
export const CustomCodeInlineExtension = Code.extend({
|
||||||
|
exitable: true,
|
||||||
|
inclusive: false,
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
markInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addPasteRules() {
|
||||||
|
return [
|
||||||
|
markPasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-[2px] font-mono font-medium text-custom-text-1000",
|
||||||
|
spellcheck: "false",
|
||||||
|
},
|
||||||
|
});
|
@ -6,10 +6,61 @@ import ts from "highlight.js/lib/languages/typescript";
|
|||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
lowlight.register("ts", ts);
|
lowlight.register("ts", ts);
|
||||||
|
|
||||||
export const CustomCodeBlock = CodeBlockLowlight.extend({
|
import { Selection } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
|
||||||
addKeyboardShortcuts() {
|
addKeyboardShortcuts() {
|
||||||
return {
|
return {
|
||||||
Tab: ({ editor }) => {
|
Tab: ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { selection } = state;
|
||||||
|
const { $from, empty } = selection;
|
||||||
|
|
||||||
|
if (!empty || $from.parent.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use ProseMirror's insertText transaction to insert the tab character
|
||||||
|
const tr = state.tr.insertText("\t", $from.pos, $from.pos);
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
ArrowUp: ({ editor }) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { selection } = state;
|
||||||
|
const { $from, empty } = selection;
|
||||||
|
|
||||||
|
if (!empty || $from.parent.type !== this.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAtStart = $from.parentOffset === 0;
|
||||||
|
|
||||||
|
if (!isAtStart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if codeBlock is the first node
|
||||||
|
const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0;
|
||||||
|
|
||||||
|
if (isFirstNode) {
|
||||||
|
// Insert a new paragraph at the start of the document and move the cursor to it
|
||||||
|
return editor.commands.command(({ tr }) => {
|
||||||
|
const node = editor.schema.nodes.paragraph.create();
|
||||||
|
tr.insert(0, node);
|
||||||
|
tr.setSelection(Selection.near(tr.doc.resolve(1)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
ArrowDown: ({ editor }) => {
|
||||||
|
if (!this.options.exitOnArrowDown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const { state } = editor;
|
const { state } = editor;
|
||||||
const { selection, doc } = state;
|
const { selection, doc } = state;
|
||||||
const { $from, empty } = selection;
|
const { $from, empty } = selection;
|
||||||
@ -18,7 +69,28 @@ export const CustomCodeBlock = CodeBlockLowlight.extend({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor.commands.insertContent(" ");
|
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
||||||
|
|
||||||
|
if (!isAtEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const after = $from.after();
|
||||||
|
|
||||||
|
if (after === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeAfter = doc.nodeAt(after);
|
||||||
|
|
||||||
|
if (nodeAfter) {
|
||||||
|
return editor.commands.command(({ tr }) => {
|
||||||
|
tr.setSelection(Selection.near(doc.resolve(after)));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor.commands.exitCode();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
import {
|
||||||
|
combineTransactionSteps,
|
||||||
|
findChildrenInRange,
|
||||||
|
getChangedRanges,
|
||||||
|
getMarksBetween,
|
||||||
|
NodeWithPos,
|
||||||
|
} from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { find } from "linkifyjs";
|
||||||
|
|
||||||
|
type AutolinkOptions = {
|
||||||
|
type: MarkType;
|
||||||
|
validate?: (url: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function autolink(options: AutolinkOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("autolink"),
|
||||||
|
appendTransaction: (transactions, oldState, newState) => {
|
||||||
|
const docChanges = transactions.some((transaction) => transaction.docChanged) && !oldState.doc.eq(newState.doc);
|
||||||
|
const preventAutolink = transactions.some((transaction) => transaction.getMeta("preventAutolink"));
|
||||||
|
|
||||||
|
if (!docChanges || preventAutolink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tr } = newState;
|
||||||
|
const transform = combineTransactionSteps(oldState.doc, [...transactions]);
|
||||||
|
const changes = getChangedRanges(transform);
|
||||||
|
|
||||||
|
changes.forEach(({ newRange }) => {
|
||||||
|
// Now let’s see if we can add new links.
|
||||||
|
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, (node) => node.isTextblock);
|
||||||
|
|
||||||
|
let textBlock: NodeWithPos | undefined;
|
||||||
|
let textBeforeWhitespace: string | undefined;
|
||||||
|
|
||||||
|
if (nodesInChangedRanges.length > 1) {
|
||||||
|
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||||
|
textBlock = nodesInChangedRanges[0];
|
||||||
|
textBeforeWhitespace = newState.doc.textBetween(
|
||||||
|
textBlock.pos,
|
||||||
|
textBlock.pos + textBlock.node.nodeSize,
|
||||||
|
undefined,
|
||||||
|
" "
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
nodesInChangedRanges.length &&
|
||||||
|
// We want to make sure to include the block seperator argument to treat hard breaks like spaces.
|
||||||
|
newState.doc.textBetween(newRange.from, newRange.to, " ", " ").endsWith(" ")
|
||||||
|
) {
|
||||||
|
textBlock = nodesInChangedRanges[0];
|
||||||
|
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, undefined, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBlock && textBeforeWhitespace) {
|
||||||
|
const wordsBeforeWhitespace = textBeforeWhitespace.split(" ").filter((s) => s !== "");
|
||||||
|
|
||||||
|
if (wordsBeforeWhitespace.length <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1];
|
||||||
|
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace);
|
||||||
|
|
||||||
|
if (!lastWordBeforeSpace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(lastWordBeforeSpace)
|
||||||
|
.filter((link) => link.isLink)
|
||||||
|
// Calculate link position.
|
||||||
|
.map((link) => ({
|
||||||
|
...link,
|
||||||
|
from: lastWordAndBlockOffset + link.start + 1,
|
||||||
|
to: lastWordAndBlockOffset + link.end + 1,
|
||||||
|
}))
|
||||||
|
// ignore link inside code mark
|
||||||
|
.filter((link) => {
|
||||||
|
if (!newState.schema.marks.code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code);
|
||||||
|
})
|
||||||
|
// validate link
|
||||||
|
.filter((link) => {
|
||||||
|
if (options.validate) {
|
||||||
|
return options.validate(link.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
// Add link mark.
|
||||||
|
.forEach((link) => {
|
||||||
|
if (getMarksBetween(link.from, link.to, newState.doc).some((item) => item.mark.type === options.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.addMark(
|
||||||
|
link.from,
|
||||||
|
link.to,
|
||||||
|
options.type.create({
|
||||||
|
href: link.href,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tr.steps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { getAttributes } from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
|
||||||
|
type ClickHandlerOptions = {
|
||||||
|
type: MarkType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("handleClickLink"),
|
||||||
|
props: {
|
||||||
|
handleClick: (view, pos, event) => {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTarget = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (eventTarget.nodeName !== "A") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrs = getAttributes(view.state, options.type.name);
|
||||||
|
const link = event.target as HTMLLinkElement;
|
||||||
|
|
||||||
|
const href = link?.href ?? attrs.href;
|
||||||
|
const target = link?.target ?? attrs.target;
|
||||||
|
|
||||||
|
if (link && href) {
|
||||||
|
if (view.editable) {
|
||||||
|
window.open(href, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import { Editor } from "@tiptap/core";
|
||||||
|
import { MarkType } from "@tiptap/pm/model";
|
||||||
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { find } from "linkifyjs";
|
||||||
|
|
||||||
|
type PasteHandlerOptions = {
|
||||||
|
editor: Editor;
|
||||||
|
type: MarkType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
key: new PluginKey("handlePasteLink"),
|
||||||
|
props: {
|
||||||
|
handlePaste: (view, event, slice) => {
|
||||||
|
const { state } = view;
|
||||||
|
const { selection } = state;
|
||||||
|
const { empty } = selection;
|
||||||
|
|
||||||
|
if (empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textContent = "";
|
||||||
|
|
||||||
|
slice.content.forEach((node) => {
|
||||||
|
textContent += node.textContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = find(textContent).find((item) => item.isLink && item.value === textContent);
|
||||||
|
|
||||||
|
if (!textContent || !link) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = event.clipboardData?.getData("text/html");
|
||||||
|
|
||||||
|
const hrefRegex = /href="([^"]*)"/;
|
||||||
|
|
||||||
|
const existingLink = html?.match(hrefRegex);
|
||||||
|
|
||||||
|
const url = existingLink ? existingLink[1] : link.href;
|
||||||
|
|
||||||
|
options.editor.commands.setMark(options.type, {
|
||||||
|
href: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
219
packages/editor/core/src/ui/extensions/custom-link/index.tsx
Normal file
219
packages/editor/core/src/ui/extensions/custom-link/index.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { Mark, markPasteRule, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { Plugin } from "@tiptap/pm/state";
|
||||||
|
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||||
|
|
||||||
|
import { autolink } from "src/ui/extensions/custom-link/helpers/autolink";
|
||||||
|
import { clickHandler } from "src/ui/extensions/custom-link/helpers/clickHandler";
|
||||||
|
import { pasteHandler } from "src/ui/extensions/custom-link/helpers/pasteHandler";
|
||||||
|
|
||||||
|
export interface LinkProtocolOptions {
|
||||||
|
scheme: string;
|
||||||
|
optionalSlashes?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkOptions {
|
||||||
|
autolink: boolean;
|
||||||
|
inclusive: boolean;
|
||||||
|
protocols: Array<LinkProtocolOptions | string>;
|
||||||
|
openOnClick: boolean;
|
||||||
|
linkOnPaste: boolean;
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
validate?: (url: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
link: {
|
||||||
|
setLink: (attributes: {
|
||||||
|
href: string;
|
||||||
|
target?: string | null;
|
||||||
|
rel?: string | null;
|
||||||
|
class?: string | null;
|
||||||
|
}) => ReturnType;
|
||||||
|
toggleLink: (attributes: {
|
||||||
|
href: string;
|
||||||
|
target?: string | null;
|
||||||
|
rel?: string | null;
|
||||||
|
class?: string | null;
|
||||||
|
}) => ReturnType;
|
||||||
|
unsetLink: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomLinkExtension = Mark.create<LinkOptions>({
|
||||||
|
name: "link",
|
||||||
|
|
||||||
|
priority: 1000,
|
||||||
|
|
||||||
|
keepOnSplit: false,
|
||||||
|
|
||||||
|
onCreate() {
|
||||||
|
this.options.protocols.forEach((protocol) => {
|
||||||
|
if (typeof protocol === "string") {
|
||||||
|
registerCustomProtocol(protocol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
inclusive() {
|
||||||
|
return this.options.inclusive;
|
||||||
|
},
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
openOnClick: true,
|
||||||
|
linkOnPaste: true,
|
||||||
|
autolink: true,
|
||||||
|
inclusive: false,
|
||||||
|
protocols: [],
|
||||||
|
HTMLAttributes: {
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer nofollow",
|
||||||
|
class: null,
|
||||||
|
},
|
||||||
|
validate: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
href: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
default: this.options.HTMLAttributes.target,
|
||||||
|
},
|
||||||
|
rel: {
|
||||||
|
default: this.options.HTMLAttributes.rel,
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: this.options.HTMLAttributes.class,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: "a[href]",
|
||||||
|
getAttrs: (node) => {
|
||||||
|
if (typeof node === "string" || !(node instanceof HTMLElement)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const href = node.getAttribute("href")?.toLowerCase() || "";
|
||||||
|
if (href.startsWith("javascript:") || href.startsWith("data:") || href.startsWith("vbscript:")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
const href = HTMLAttributes.href?.toLowerCase() || "";
|
||||||
|
if (href.startsWith("javascript:") || href.startsWith("data:") || href.startsWith("vbscript:")) {
|
||||||
|
return ["a", mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: "" }), 0];
|
||||||
|
}
|
||||||
|
return ["a", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setLink:
|
||||||
|
(attributes) =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().setMark(this.name, attributes).setMeta("preventAutolink", true).run(),
|
||||||
|
|
||||||
|
toggleLink:
|
||||||
|
(attributes) =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain()
|
||||||
|
.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
|
||||||
|
.setMeta("preventAutolink", true)
|
||||||
|
.run(),
|
||||||
|
|
||||||
|
unsetLink:
|
||||||
|
() =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addPasteRules() {
|
||||||
|
return [
|
||||||
|
markPasteRule({
|
||||||
|
find: (text) =>
|
||||||
|
find(text)
|
||||||
|
.filter((link) => {
|
||||||
|
if (this.options.validate) {
|
||||||
|
return this.options.validate(link.value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.filter((link) => link.isLink)
|
||||||
|
.map((link) => ({
|
||||||
|
text: link.value,
|
||||||
|
index: link.start,
|
||||||
|
data: link,
|
||||||
|
})),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match, pasteEvent) => {
|
||||||
|
const html = pasteEvent?.clipboardData?.getData("text/html");
|
||||||
|
const hrefRegex = /href="([^"]*)"/;
|
||||||
|
|
||||||
|
const existingLink = html?.match(hrefRegex);
|
||||||
|
|
||||||
|
if (existingLink) {
|
||||||
|
return {
|
||||||
|
href: existingLink[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
href: match.data?.href,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const plugins: Plugin[] = [];
|
||||||
|
|
||||||
|
if (this.options.autolink) {
|
||||||
|
plugins.push(
|
||||||
|
autolink({
|
||||||
|
type: this.type,
|
||||||
|
validate: this.options.validate,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.openOnClick) {
|
||||||
|
plugins.push(
|
||||||
|
clickHandler({
|
||||||
|
type: this.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.linkOnPaste) {
|
||||||
|
plugins.push(
|
||||||
|
pasteHandler({
|
||||||
|
editor: this.editor,
|
||||||
|
type: this.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
},
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import TiptapLink from "@tiptap/extension-link";
|
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
@ -19,13 +18,15 @@ import { isValidHttpUrl } from "src/lib/utils";
|
|||||||
import { Mentions } from "src/ui/mentions";
|
import { Mentions } from "src/ui/mentions";
|
||||||
|
|
||||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
import { CustomKeymap } from "src/ui/extensions/keymap";
|
||||||
import { CustomCodeBlock } from "src/ui/extensions/code";
|
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
||||||
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
import { ListKeymap } from "src/ui/extensions/custom-list-keymap";
|
||||||
|
|
||||||
import { DeleteImage } from "src/types/delete-image";
|
import { DeleteImage } from "src/types/delete-image";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
import { RestoreImage } from "src/types/restore-image";
|
import { RestoreImage } from "src/types/restore-image";
|
||||||
|
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
|
||||||
|
import { CustomCodeInlineExtension } from "./code-inline";
|
||||||
|
|
||||||
export const CoreEditorExtensions = (
|
export const CoreEditorExtensions = (
|
||||||
mentionConfig: {
|
mentionConfig: {
|
||||||
@ -52,12 +53,7 @@ export const CoreEditorExtensions = (
|
|||||||
class: "leading-normal -mb-2",
|
class: "leading-normal -mb-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
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: {
|
||||||
@ -70,10 +66,12 @@ export const CoreEditorExtensions = (
|
|||||||
}),
|
}),
|
||||||
CustomKeymap,
|
CustomKeymap,
|
||||||
ListKeymap,
|
ListKeymap,
|
||||||
TiptapLink.configure({
|
CustomLinkExtension.configure({
|
||||||
autolink: false,
|
openOnClick: true,
|
||||||
|
autolink: true,
|
||||||
|
linkOnPaste: true,
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url: string) => isValidHttpUrl(url),
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class:
|
class:
|
||||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||||
@ -92,13 +90,14 @@ 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",
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
|
CustomCodeBlockExtension,
|
||||||
|
CustomCodeInlineExtension,
|
||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import TiptapLink from "@tiptap/extension-link";
|
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import { Color } from "@tiptap/extension-color";
|
import { Color } from "@tiptap/extension-color";
|
||||||
@ -18,6 +17,7 @@ import { ReadOnlyImageExtension } from "src/ui/extensions/image/read-only-image"
|
|||||||
import { isValidHttpUrl } from "src/lib/utils";
|
import { isValidHttpUrl } from "src/lib/utils";
|
||||||
import { Mentions } from "src/ui/mentions";
|
import { Mentions } from "src/ui/mentions";
|
||||||
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
import { IMentionSuggestion } from "src/types/mention-suggestion";
|
||||||
|
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
|
||||||
|
|
||||||
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||||
mentionSuggestions: IMentionSuggestion[];
|
mentionSuggestions: IMentionSuggestion[];
|
||||||
@ -59,7 +59,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
gapcursor: false,
|
gapcursor: false,
|
||||||
}),
|
}),
|
||||||
Gapcursor,
|
Gapcursor,
|
||||||
TiptapLink.configure({
|
CustomLinkExtension.configure({
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url) => isValidHttpUrl(url),
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"react-popper": "^2.3.0"
|
"react-popper": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
// react-poppper
|
// react-poppper
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
// hooks
|
// hooks
|
||||||
@ -29,8 +29,10 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
optionsClassName = "",
|
optionsClassName = "",
|
||||||
verticalEllipsis = false,
|
verticalEllipsis = false,
|
||||||
width = "auto",
|
width = "auto",
|
||||||
|
portalElement,
|
||||||
menuButtonOnClick,
|
menuButtonOnClick,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
|
closeOnSelect,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
const [referenceElement, setReferenceElement] = React.useState<HTMLButtonElement | null>(null);
|
||||||
@ -51,6 +53,39 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||||
|
|
||||||
|
let menuItems = (
|
||||||
|
<Menu.Items
|
||||||
|
className="fixed z-10"
|
||||||
|
onClick={() => {
|
||||||
|
if (closeOnSelect) closeDropdown();
|
||||||
|
}}
|
||||||
|
static
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
||||||
|
maxHeight === "lg"
|
||||||
|
? "max-h-60"
|
||||||
|
: maxHeight === "md"
|
||||||
|
? "max-h-48"
|
||||||
|
: maxHeight === "rg"
|
||||||
|
? "max-h-36"
|
||||||
|
: maxHeight === "sm"
|
||||||
|
? "max-h-28"
|
||||||
|
: ""
|
||||||
|
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (portalElement) {
|
||||||
|
menuItems = ReactDOM.createPortal(menuItems, portalElement);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
as="div"
|
as="div"
|
||||||
@ -118,28 +153,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isOpen && (
|
{isOpen && menuItems}
|
||||||
<Menu.Items className="fixed z-10" static>
|
|
||||||
<div
|
|
||||||
className={`my-1 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 p-1 text-xs shadow-custom-shadow-rg focus:outline-none ${
|
|
||||||
maxHeight === "lg"
|
|
||||||
? "max-h-60"
|
|
||||||
: maxHeight === "md"
|
|
||||||
? "max-h-48"
|
|
||||||
: maxHeight === "rg"
|
|
||||||
? "max-h-36"
|
|
||||||
: maxHeight === "sm"
|
|
||||||
? "max-h-28"
|
|
||||||
: ""
|
|
||||||
} ${width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width} ${optionsClassName}`}
|
|
||||||
ref={setPopperElement}
|
|
||||||
style={styles.popper}
|
|
||||||
{...attributes.popper}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -24,6 +24,8 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
|||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
verticalEllipsis?: boolean;
|
verticalEllipsis?: boolean;
|
||||||
menuButtonOnClick?: (...args: any) => void;
|
menuButtonOnClick?: (...args: any) => void;
|
||||||
|
closeOnSelect?: boolean;
|
||||||
|
portalElement?: Element | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomSelectProps extends IDropdownProps {
|
export interface ICustomSelectProps extends IDropdownProps {
|
||||||
|
@ -13,16 +13,20 @@ import { getFileIcon } from "components/icons";
|
|||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
|
import { convertBytesToSize, getFileExtension, getFileName } from "helpers/attachment.helper";
|
||||||
// type
|
// types
|
||||||
import { TIssueAttachmentsList } from "./attachments-list";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
export type TIssueAttachmentsDetail = TIssueAttachmentsList & {
|
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
|
type TIssueAttachmentsDetail = {
|
||||||
attachmentId: string;
|
attachmentId: string;
|
||||||
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { attachmentId, handleAttachmentOperations } = props;
|
const { attachmentId, handleAttachmentOperations, disabled } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
const {
|
const {
|
||||||
@ -75,13 +79,15 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
{!disabled && (
|
||||||
onClick={() => {
|
<button
|
||||||
setAttachmentDeleteModal(true);
|
onClick={() => {
|
||||||
}}
|
setAttachmentDeleteModal(true);
|
||||||
>
|
}}
|
||||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
>
|
||||||
</button>
|
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,25 +7,35 @@ import { IssueAttachmentsDetail } from "./attachment-detail";
|
|||||||
// types
|
// types
|
||||||
import { TAttachmentOperations } from "./root";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
export type TIssueAttachmentsList = {
|
type TIssueAttachmentsList = {
|
||||||
|
issueId: string;
|
||||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
|
export const IssueAttachmentsList: FC<TIssueAttachmentsList> = observer((props) => {
|
||||||
const { handleAttachmentOperations } = props;
|
const { issueId, handleAttachmentOperations, disabled } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
attachment: { issueAttachments },
|
attachment: { getAttachmentsByIssueId },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const issueAttachments = getAttachmentsByIssueId(issueId);
|
||||||
|
|
||||||
|
if (!issueAttachments) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{issueAttachments &&
|
{issueAttachments &&
|
||||||
issueAttachments.length > 0 &&
|
issueAttachments.length > 0 &&
|
||||||
issueAttachments.map((attachmentId) => (
|
issueAttachments.map((attachmentId) => (
|
||||||
<IssueAttachmentsDetail attachmentId={attachmentId} handleAttachmentOperations={handleAttachmentOperations} />
|
<IssueAttachmentsDetail
|
||||||
|
attachmentId={attachmentId}
|
||||||
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -8,12 +8,15 @@ import { Button } from "@plane/ui";
|
|||||||
import { getFileName } from "helpers/attachment.helper";
|
import { getFileName } from "helpers/attachment.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssueAttachment } from "@plane/types";
|
import type { TIssueAttachment } from "@plane/types";
|
||||||
import { TIssueAttachmentsList } from "./attachments-list";
|
import { TAttachmentOperations } from "./root";
|
||||||
|
|
||||||
type Props = TIssueAttachmentsList & {
|
export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "create">;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
data: TIssueAttachment;
|
data: TIssueAttachment;
|
||||||
|
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||||
|
@ -10,8 +10,7 @@ export type TIssueAttachmentRoot = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_archived: boolean;
|
disabled?: boolean;
|
||||||
is_editable: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAttachmentOperations = {
|
export type TAttachmentOperations = {
|
||||||
@ -21,7 +20,7 @@ export type TAttachmentOperations = {
|
|||||||
|
|
||||||
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId, issueId, is_archived, is_editable } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { createAttachment, removeAttachment } = useIssueDetail();
|
const { createAttachment, removeAttachment } = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -72,10 +71,14 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<IssueAttachmentUpload
|
<IssueAttachmentUpload
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
disabled={is_editable}
|
disabled={disabled}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
|
<IssueAttachmentsList
|
||||||
|
issueId={issueId}
|
||||||
|
disabled={disabled}
|
||||||
handleAttachmentOperations={handleAttachmentOperations}
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
/>
|
/>
|
||||||
<IssueAttachmentsList handleAttachmentOperations={handleAttachmentOperations} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import debounce from "lodash/debounce";
|
|||||||
// components
|
// components
|
||||||
import { TextArea } from "@plane/ui";
|
import { TextArea } from "@plane/ui";
|
||||||
import { RichTextEditor } from "components/editor/rich-text-editor";
|
import { RichTextEditor } from "components/editor/rich-text-editor";
|
||||||
|
import { RichTextReadOnlyEditor } from "components/editor/rich-text-read-only-editor";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { TIssueOperations } from "./issue-detail";
|
import { TIssueOperations } from "./issue-detail";
|
||||||
@ -26,13 +27,13 @@ export interface IssueDetailsProps {
|
|||||||
project_id?: string;
|
project_id?: string;
|
||||||
};
|
};
|
||||||
issueOperations: TIssueOperations;
|
issueOperations: TIssueOperations;
|
||||||
isAllowed: boolean;
|
disabled: boolean;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, issue, issueOperations, isAllowed, isSubmitting, setIsSubmitting } = props;
|
const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props;
|
||||||
// states
|
// states
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isAllowed ? (
|
{!disabled ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@ -134,14 +135,13 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
className="min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||||
hasError={Boolean(errors?.name)}
|
hasError={Boolean(errors?.name)}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
disabled={!isAllowed}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
||||||
)}
|
)}
|
||||||
{characterLimit && isAllowed && (
|
{characterLimit && !disabled && (
|
||||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
||||||
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
||||||
{watch("name").length}
|
{watch("name").length}
|
||||||
@ -150,31 +150,36 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
<>{errors.name ? errors.name.message : null}</>
|
|
||||||
</span>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Controller
|
<Controller
|
||||||
name="description_html"
|
name="description_html"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange } }) => (
|
render={({ field: { onChange } }) =>
|
||||||
<RichTextEditor
|
!disabled ? (
|
||||||
workspaceSlug={workspaceSlug}
|
<RichTextEditor
|
||||||
value={localIssueDescription.description_html}
|
workspaceSlug={workspaceSlug}
|
||||||
rerenderOnPropsChange={localIssueDescription}
|
value={localIssueDescription.description_html}
|
||||||
setShouldShowAlert={setShowAlert}
|
rerenderOnPropsChange={localIssueDescription}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setShouldShowAlert={setShowAlert}
|
||||||
dragDropEnabled
|
setIsSubmitting={setIsSubmitting}
|
||||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
dragDropEnabled
|
||||||
noBorder={!isAllowed}
|
customClassName="min-h-[150px] shadow-sm"
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
setShowAlert(true);
|
setShowAlert(true);
|
||||||
setIsSubmitting("submitting");
|
setIsSubmitting("submitting");
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
debouncedFormSave();
|
debouncedFormSave();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
|
<RichTextReadOnlyEditor
|
||||||
|
value={localIssueDescription.description_html}
|
||||||
|
customClassName="!p-0 !pt-2 text-custom-text-200"
|
||||||
|
noBorder={disabled}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,15 +74,11 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-2 group hover:border-red-500/50 hover:bg-red-500/20"
|
className="flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-2 hover:bg-custom-background-90 text-custom-text-300 hover:text-custom-text-200"
|
||||||
onClick={handleIsCreateToggle}
|
onClick={handleIsCreateToggle}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{isCreateToggle ? (
|
{isCreateToggle ? <X className="h-2.5 w-2.5" /> : <Plus className="h-2.5 w-2.5" />}
|
||||||
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
|
||||||
) : (
|
|
||||||
<Plus className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">{isCreateToggle ? "Cancel" : "New"}</div>
|
<div className="flex-shrink-0">{isCreateToggle ? "Cancel" : "New"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,3 +3,5 @@ export * from "./root";
|
|||||||
export * from "./label-list";
|
export * from "./label-list";
|
||||||
export * from "./label-list-item";
|
export * from "./label-list-item";
|
||||||
export * from "./create-label";
|
export * from "./create-label";
|
||||||
|
export * from "./select/root";
|
||||||
|
export * from "./select/label-select";
|
||||||
|
@ -10,10 +10,11 @@ type TLabelListItem = {
|
|||||||
issueId: string;
|
issueId: string;
|
||||||
labelId: string;
|
labelId: string;
|
||||||
labelOperations: TLabelOperations;
|
labelOperations: TLabelOperations;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelListItem: FC<TLabelListItem> = (props) => {
|
export const LabelListItem: FC<TLabelListItem> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, labelId, labelOperations } = props;
|
const { workspaceSlug, projectId, issueId, labelId, labelOperations, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -34,7 +35,9 @@ export const LabelListItem: FC<TLabelListItem> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={labelId}
|
key={labelId}
|
||||||
className="transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-1 group hover:border-red-500/50 hover:bg-red-500/20"
|
className={`transition-all relative flex items-center gap-1 border border-custom-border-100 rounded-full text-xs p-0.5 px-1 group ${
|
||||||
|
!disabled ? "cursor-pointer hover:border-red-500/50 hover:bg-red-500/20" : "cursor-not-allowed"
|
||||||
|
} `}
|
||||||
onClick={handleLabel}
|
onClick={handleLabel}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -44,9 +47,11 @@ export const LabelListItem: FC<TLabelListItem> = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex-shrink-0">{label.name}</div>
|
<div className="flex-shrink-0">{label.name}</div>
|
||||||
<div className="flex-shrink-0">
|
{!disabled && (
|
||||||
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
<div className="flex-shrink-0">
|
||||||
</div>
|
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,10 +11,11 @@ type TLabelList = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
labelOperations: TLabelOperations;
|
labelOperations: TLabelOperations;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LabelList: FC<TLabelList> = (props) => {
|
export const LabelList: FC<TLabelList> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, labelOperations } = props;
|
const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -33,6 +34,7 @@ export const LabelList: FC<TLabelList> = (props) => {
|
|||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
labelId={labelId}
|
labelId={labelId}
|
||||||
labelOperations={labelOperations}
|
labelOperations={labelOperations}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { LabelList, LabelCreate } from "./";
|
import { LabelList, LabelCreate, IssueLabelSelectRoot } from "./";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useLabel } from "hooks/store";
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
@ -77,16 +76,26 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
labelOperations={labelOperations}
|
labelOperations={labelOperations}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div>select existing labels</div> */}
|
{!disabled && (
|
||||||
|
<IssueLabelSelectRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LabelCreate
|
{!disabled && (
|
||||||
workspaceSlug={workspaceSlug}
|
<LabelCreate
|
||||||
projectId={projectId}
|
workspaceSlug={workspaceSlug}
|
||||||
issueId={issueId}
|
projectId={projectId}
|
||||||
labelOperations={labelOperations}
|
issueId={issueId}
|
||||||
/>
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
type TLabelExistingSelect = {};
|
|
||||||
|
|
||||||
export const LabelExistingSelect: FC<TLabelExistingSelect> = (props) => {
|
|
||||||
const {} = props;
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
159
web/components/issues/issue-detail/label/select/label-select.tsx
Normal file
159
web/components/issues/issue-detail/label/select/label-select.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { usePopper } from "react-popper";
|
||||||
|
import { Check, Search, Tag } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { Combobox } from "@headlessui/react";
|
||||||
|
|
||||||
|
export interface IIssueLabelSelect {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
onSelect: (_labelIds: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, onSelect } = props;
|
||||||
|
// store hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
const {
|
||||||
|
project: { fetchProjectLabels, projectLabels },
|
||||||
|
} = useLabel();
|
||||||
|
// states
|
||||||
|
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<Boolean>(false);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
const fetchLabels = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = (projectLabels ?? []).map((label) => ({
|
||||||
|
value: label.id,
|
||||||
|
query: label.name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center justify-start gap-2 overflow-hidden">
|
||||||
|
<span
|
||||||
|
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="line-clamp-1 inline-block truncate">{label.name}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredOptions =
|
||||||
|
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
options: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const issueLabels = issue?.label_ids ?? [];
|
||||||
|
|
||||||
|
const label = (
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer rounded-full text-xs p-0.5 px-2 hover:bg-custom-background-90 py-0.5 text-custom-text-300 hover:text-custom-text-200 border border-custom-border-100`}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Tag className="h-2.5 w-2.5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">Select Label</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Combobox
|
||||||
|
as="div"
|
||||||
|
className={`w-auto max-w-full flex-shrink-0 text-left`}
|
||||||
|
value={issueLabels}
|
||||||
|
onChange={(value) => onSelect(value)}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<Combobox.Button as={Fragment}>
|
||||||
|
<button
|
||||||
|
ref={setReferenceElement}
|
||||||
|
type="button"
|
||||||
|
className="rounded cursor-pointer"
|
||||||
|
onClick={() => !projectLabels && fetchLabels()}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
<Combobox.Options className="fixed z-10">
|
||||||
|
<div
|
||||||
|
className={`z-10 my-1 w-48 whitespace-nowrap rounded border border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none`}
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={styles.popper}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||||
|
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||||
|
<Combobox.Input
|
||||||
|
className="w-full bg-transparent px-2 py-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
displayValue={(assigned: any) => assigned?.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-center text-custom-text-200">Loading...</p>
|
||||||
|
) : filteredOptions.length > 0 ? (
|
||||||
|
filteredOptions.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${
|
||||||
|
selected ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
{option.content}
|
||||||
|
{selected && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Check className={`h-3.5 w-3.5`} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2 p-1">
|
||||||
|
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
24
web/components/issues/issue-detail/label/select/root.tsx
Normal file
24
web/components/issues/issue-detail/label/select/root.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { IssueLabelSelect } from "./label-select";
|
||||||
|
// types
|
||||||
|
import { TLabelOperations } from "../root";
|
||||||
|
|
||||||
|
type TIssueLabelSelectRoot = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
labelOperations: TLabelOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, labelOperations } = props;
|
||||||
|
|
||||||
|
const handleLabel = async (_labelIds: string[]) => {
|
||||||
|
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: _labelIds });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IssueLabelSelect workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} onSelect={handleLabel} />
|
||||||
|
);
|
||||||
|
};
|
@ -9,20 +9,25 @@ import { TLinkOperations } from "./root";
|
|||||||
export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
|
||||||
|
|
||||||
export type TIssueLinkList = {
|
export type TIssueLinkList = {
|
||||||
|
issueId: string;
|
||||||
linkOperations: TLinkOperationsModal;
|
linkOperations: TLinkOperationsModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
export const IssueLinkList: FC<TIssueLinkList> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { linkOperations } = props;
|
const { issueId, linkOperations } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
link: { issueLinks },
|
link: { getLinksByIssueId },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
|
const issueLinks = getLinksByIssueId(issueId);
|
||||||
|
|
||||||
|
if (!issueLinks) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{issueLinks &&
|
{issueLinks &&
|
||||||
|
@ -19,13 +19,12 @@ export type TIssueLinkRoot = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_editable: boolean;
|
disabled?: boolean;
|
||||||
is_archived: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { workspaceSlug, projectId, issueId, is_editable, is_archived } = props;
|
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail();
|
const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail();
|
||||||
// state
|
// state
|
||||||
@ -108,17 +107,17 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
linkOperations={handleLinkOperations}
|
linkOperations={handleLinkOperations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={`py-1 text-xs ${is_archived ? "opacity-60" : ""}`}>
|
<div className={`py-1 text-xs ${disabled ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h4>Links</h4>
|
<h4>Links</h4>
|
||||||
{is_editable && (
|
{!disabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||||
is_archived ? "cursor-not-allowed" : "cursor-pointer"
|
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleIssueLinkModal(true)}
|
onClick={() => toggleIssueLinkModal(true)}
|
||||||
disabled={is_archived}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -126,7 +125,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<IssueLinkList linkOperations={handleLinkOperations} />
|
<IssueLinkList issueId={issueId} linkOperations={handleLinkOperations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -11,8 +11,6 @@ import { SubIssuesRoot } from "../sub-issues";
|
|||||||
import { StateGroupIcon } from "@plane/ui";
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssueOperations } from "./root";
|
import { TIssueOperations } from "./root";
|
||||||
// constants
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -28,10 +26,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { currentUser } = useUser();
|
||||||
currentUser,
|
|
||||||
membership: { currentProjectRole },
|
|
||||||
} = useUser();
|
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const {
|
const {
|
||||||
@ -44,8 +39,6 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const projectDetails = projectId ? getProjectById(projectId) : null;
|
const projectDetails = projectId ? getProjectById(projectId) : null;
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg space-y-4">
|
<div className="rounded-lg space-y-4">
|
||||||
@ -78,7 +71,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
isAllowed={isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
@ -107,8 +100,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
is_archived={is_archived}
|
disabled={!is_editable}
|
||||||
is_editable={is_editable}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="space-y-5 pt-3">
|
{/* <div className="space-y-5 pt-3">
|
||||||
|
@ -30,16 +30,20 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer(
|
|||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
const parentIssue = issue && issue.parent_id ? getIssueById(issue.parent_id) : undefined;
|
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
|
||||||
const parentIssueProjectDetails =
|
const parentIssueProjectDetails =
|
||||||
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
||||||
|
|
||||||
const handleParentIssue = async (_issueId: string | null = null) => {
|
const handleParentIssue = async (_issueId: string | null = null) => {
|
||||||
setUpdating(true);
|
setUpdating(true);
|
||||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }).finally(() => {
|
try {
|
||||||
|
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||||
|
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||||
toggleParentIssueModal(false);
|
toggleParentIssueModal(false);
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error("something went wrong while fetching the issue");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!issue) return <></>;
|
if (!issue) return <></>;
|
||||||
@ -61,14 +65,14 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer(
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<div onClick={() => toggleParentIssueModal(true)}>
|
<div onClick={() => toggleParentIssueModal(true)}>
|
||||||
{parentIssue ? (
|
{issue?.parent_id && parentIssue ? (
|
||||||
`${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}`
|
`${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}`
|
||||||
) : (
|
) : (
|
||||||
<span className="text-custom-text-200">Select issue</span>
|
<span className="text-custom-text-200">Select issue</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{parentIssue && (
|
{issue?.parent_id && parentIssue && !disabled && (
|
||||||
<div onClick={() => handleParentIssue(null)}>
|
<div onClick={() => handleParentIssue(null)}>
|
||||||
<X className="h-2.5 w-2.5" />
|
<X className="h-2.5 w-2.5" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,22 +126,24 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||||||
{issueRelationObject[relationKey].icon(10)}
|
{issueRelationObject[relationKey].icon(10)}
|
||||||
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
{`${projectDetails?.identifier}-${currentIssue?.sequence_id}`}
|
||||||
</a>
|
</a>
|
||||||
<button
|
{!disabled && (
|
||||||
type="button"
|
<button
|
||||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
type="button"
|
||||||
onClick={() => {
|
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||||
if (!currentUser) return;
|
onClick={() => {
|
||||||
removeRelation(
|
if (!currentUser) return;
|
||||||
workspaceSlug as string,
|
removeRelation(
|
||||||
projectId as string,
|
workspaceSlug as string,
|
||||||
issueId,
|
projectId as string,
|
||||||
relationKey,
|
issueId,
|
||||||
relationIssueId
|
relationKey,
|
||||||
);
|
relationIssueId
|
||||||
}}
|
);
|
||||||
>
|
}}
|
||||||
<X className="h-2 w-2" />
|
>
|
||||||
</button>
|
<X className="h-2 w-2" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -8,12 +8,15 @@ import { EmptyState } from "components/common";
|
|||||||
// images
|
// images
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
import emptyIssue from "public/empty-state/issue.svg";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
export type TIssueOperations = {
|
export type TIssueOperations = {
|
||||||
|
fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
@ -27,16 +30,16 @@ export type TIssueDetailRoot = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
is_archived?: boolean;
|
is_archived?: boolean;
|
||||||
is_editable?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||||
const { workspaceSlug, projectId, issueId, is_archived = false, is_editable = true } = props;
|
const { workspaceSlug, projectId, issueId, is_archived = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
@ -45,9 +48,19 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
removeIssueFromModule,
|
removeIssueFromModule,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
const {
|
||||||
|
membership: { currentProjectRole },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
const issueOperations: TIssueOperations = useMemo(
|
const issueOperations: TIssueOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await fetchIssue(workspaceSlug, projectId, issueId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching the parent issue");
|
||||||
|
}
|
||||||
|
},
|
||||||
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
try {
|
try {
|
||||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
@ -146,6 +159,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
fetchIssue,
|
||||||
updateIssue,
|
updateIssue,
|
||||||
removeIssue,
|
removeIssue,
|
||||||
addIssueToCycle,
|
addIssueToCycle,
|
||||||
@ -156,7 +170,10 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Issue details
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
|
// Check if issue is editable, based on user role
|
||||||
|
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -189,7 +206,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
|||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
is_archived={is_archived}
|
is_archived={is_archived}
|
||||||
is_editable={true}
|
is_editable={is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,8 +25,6 @@ import { ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon
|
|||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssueOperations } from "./root";
|
import type { TIssueOperations } from "./root";
|
||||||
// fetch-keys
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -72,10 +70,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const { inboxIssueId } = router.query;
|
const { inboxIssueId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const {
|
const { currentUser } = useUser();
|
||||||
currentUser,
|
|
||||||
membership: { currentProjectRole },
|
|
||||||
} = useUser();
|
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -124,8 +119,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||||
maxDate?.setDate(maxDate.getDate());
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -166,7 +159,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
currentUserId={currentUser?.id}
|
currentUserId={currentUser?.id}
|
||||||
disabled={!isAllowed || !is_editable}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -193,7 +185,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full overflow-y-auto px-5">
|
<div className="h-full w-full overflow-y-auto px-5">
|
||||||
<div className={`divide-y-2 divide-custom-border-200 ${is_editable ? "opacity-60" : ""}`}>
|
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||||
{showFirstSection && (
|
{showFirstSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
@ -208,7 +200,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
value={issue?.state_id ?? undefined}
|
value={issue?.state_id ?? undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -228,7 +220,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })
|
issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })
|
||||||
}
|
}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
placeholder="Assignees"
|
placeholder="Assignees"
|
||||||
multiple
|
multiple
|
||||||
@ -252,7 +244,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<PriorityDropdown
|
<PriorityDropdown
|
||||||
value={issue?.priority || undefined}
|
value={issue?.priority || undefined}
|
||||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -274,7 +266,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||||
}
|
}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -297,7 +289,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -309,7 +301,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="blocking"
|
relationKey="blocking"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -319,7 +311,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="blocked_by"
|
relationKey="blocked_by"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -329,7 +321,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="duplicate"
|
relationKey="duplicate"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -339,7 +331,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
relationKey="relates_to"
|
relationKey="relates_to"
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -358,7 +350,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
maxDate={maxDate ?? undefined}
|
maxDate={maxDate ?? undefined}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -379,7 +371,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
minDate={minDate ?? undefined}
|
minDate={minDate ?? undefined}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -401,7 +393,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -419,7 +411,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
issueOperations={issueOperations}
|
issueOperations={issueOperations}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -429,7 +421,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||||
<div className="flex flex-wrap items-start py-2">
|
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
@ -439,7 +431,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={!isAllowed || !is_editable}
|
disabled={!is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -450,8 +442,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
is_editable={is_editable}
|
disabled={!is_editable}
|
||||||
is_archived={is_archived}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,17 +5,17 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Button } from "@plane/ui";
|
import { Button } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
export type TIssueSubscription = {
|
export type TIssueSubscription = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
disabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, issueId, currentUserId, disabled } = props;
|
const { workspaceSlug, projectId, issueId, currentUserId } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
issue: { getIssueById },
|
issue: { getIssueById },
|
||||||
@ -23,16 +23,32 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
|||||||
createSubscription,
|
createSubscription,
|
||||||
removeSubscription,
|
removeSubscription,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
// state
|
// state
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const subscription = getSubscriptionByIssueId(issueId);
|
const subscription = getSubscriptionByIssueId(issueId);
|
||||||
|
|
||||||
const handleSubscription = () => {
|
const handleSubscription = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (subscription?.subscribed) removeSubscription(workspaceSlug, projectId, issueId);
|
try {
|
||||||
else createSubscription(workspaceSlug, projectId, issueId);
|
if (subscription?.subscribed) await removeSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
else await createSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||||
|
message: `Issue ${subscription?.subscribed ? `unsubscribed` : `subscribed`} successfully.!`,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error",
|
||||||
|
message: "Something went wrong. Please try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (issue?.created_by === currentUserId || issue?.assignee_ids.includes(currentUserId)) return <></>;
|
if (issue?.created_by === currentUserId || issue?.assignee_ids.includes(currentUserId)) return <></>;
|
||||||
@ -45,7 +61,6 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
|||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
className="hover:!bg-custom-primary-100/20"
|
className="hover:!bg-custom-primary-100/20"
|
||||||
onClick={handleSubscription}
|
onClick={handleSubscription}
|
||||||
disabled={disabled}
|
|
||||||
>
|
>
|
||||||
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
|
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,4 +4,5 @@ export interface IQuickActionProps {
|
|||||||
handleUpdate?: (data: TIssue) => Promise<void>;
|
handleUpdate?: (data: TIssue) => Promise<void>;
|
||||||
handleRemoveFromView?: () => Promise<void>;
|
handleRemoveFromView?: () => Promise<void>;
|
||||||
customActionButton?: React.ReactElement;
|
customActionButton?: React.ReactElement;
|
||||||
|
portalElement?: HTMLDivElement | null;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
|||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
|
|
||||||
export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
|
||||||
// states
|
// states
|
||||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||||
@ -59,11 +59,15 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
<CustomMenu
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={customActionButton}
|
||||||
|
portalElement={portalElement}
|
||||||
|
closeOnSelect
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyIssueLink();
|
handleCopyIssueLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -74,8 +78,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIssueToEdit(issue);
|
setIssueToEdit(issue);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
@ -87,8 +89,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -99,8 +99,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -12,7 +12,7 @@ import { copyUrlToClipboard } from "helpers/string.helper";
|
|||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
|
|
||||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, customActionButton } = props;
|
const { issue, handleDelete, customActionButton, portalElement } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -40,11 +40,15 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
onSubmit={handleDelete}
|
onSubmit={handleDelete}
|
||||||
/>
|
/>
|
||||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
<CustomMenu
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={customActionButton}
|
||||||
|
portalElement={portalElement}
|
||||||
|
closeOnSelect
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyIssueLink();
|
handleCopyIssueLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -55,8 +59,6 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
|||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
|
|
||||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
|
||||||
// states
|
// states
|
||||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||||
@ -59,11 +59,15 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
<CustomMenu
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={customActionButton}
|
||||||
|
portalElement={portalElement}
|
||||||
|
closeOnSelect
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyIssueLink();
|
handleCopyIssueLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -74,8 +78,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIssueToEdit({
|
setIssueToEdit({
|
||||||
...issue,
|
...issue,
|
||||||
cycle: cycleId?.toString() ?? null,
|
cycle: cycleId?.toString() ?? null,
|
||||||
@ -90,8 +92,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveFromView && handleRemoveFromView();
|
handleRemoveFromView && handleRemoveFromView();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -102,8 +102,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -114,8 +112,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -13,7 +13,7 @@ import { TIssue } from "@plane/types";
|
|||||||
import { IQuickActionProps } from "../list/list-view-types";
|
import { IQuickActionProps } from "../list/list-view-types";
|
||||||
|
|
||||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton } = props;
|
const { issue, handleDelete, handleUpdate, handleRemoveFromView, customActionButton, portalElement } = props;
|
||||||
// states
|
// states
|
||||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||||
@ -59,11 +59,15 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
<CustomMenu
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={customActionButton}
|
||||||
|
portalElement={portalElement}
|
||||||
|
closeOnSelect
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyIssueLink();
|
handleCopyIssueLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -74,8 +78,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
setIssueToEdit({ ...issue, module: moduleId?.toString() ?? null });
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
@ -87,8 +89,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveFromView && handleRemoveFromView();
|
handleRemoveFromView && handleRemoveFromView();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -99,8 +99,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -16,7 +16,7 @@ import { IQuickActionProps } from "../list/list-view-types";
|
|||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||||
const { issue, handleDelete, handleUpdate, customActionButton } = props;
|
const { issue, handleDelete, handleUpdate, customActionButton, portalElement } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -68,11 +68,15 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
if (issueToEdit && handleUpdate) await handleUpdate({ ...issueToEdit, ...data });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
|
<CustomMenu
|
||||||
|
placement="bottom-start"
|
||||||
|
customButton={customActionButton}
|
||||||
|
portalElement={portalElement}
|
||||||
|
closeOnSelect
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleCopyIssueLink();
|
handleCopyIssueLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -85,8 +89,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
<>
|
<>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIssueToEdit(issue);
|
setIssueToEdit(issue);
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
@ -98,8 +100,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setCreateUpdateIssueModal(true);
|
setCreateUpdateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -110,8 +110,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDeleteIssueModal(true);
|
setDeleteIssueModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
|
import { useGlobalView, useIssues, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
|
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
|
||||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||||
@ -37,9 +37,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
membership: { currentWorkspaceAllProjectsRole },
|
membership: { currentWorkspaceAllProjectsRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { fetchAllGlobalViews } = useGlobalView();
|
const { fetchAllGlobalViews } = useGlobalView();
|
||||||
const {
|
|
||||||
workspace: { workspaceLabels },
|
|
||||||
} = useLabel();
|
|
||||||
// derived values
|
// derived values
|
||||||
const currentIssueView = type ?? globalViewId;
|
const currentIssueView = type ?? globalViewId;
|
||||||
|
|
||||||
@ -134,7 +131,6 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
|
|||||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
labels={workspaceLabels || undefined}
|
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
viewId={currentIssueView}
|
viewId={currentIssueView}
|
||||||
|
@ -27,20 +27,12 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||||||
// hooks
|
// hooks
|
||||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||||
|
|
||||||
const {} = useSWR(
|
useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, async () => {
|
||||||
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
if (workspaceSlug && projectId) {
|
||||||
async () => {
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
if (workspaceSlug && projectId) {
|
await issues?.fetchIssues(workspaceSlug.toString(), projectId.toString(), issues?.groupedIssueIds ? "mutation" : "init-loader");
|
||||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
}
|
||||||
await issues?.fetchIssues(
|
});
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { FC, 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";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useLabel, useProjectState, useUser } from "hooks/store";
|
import { useIssues, useUser } from "hooks/store";
|
||||||
// views
|
// views
|
||||||
import { SpreadsheetView } from "./spreadsheet-view";
|
import { SpreadsheetView } from "./spreadsheet-view";
|
||||||
// types
|
// types
|
||||||
@ -40,10 +40,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
|||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const {
|
|
||||||
project: { projectLabels },
|
|
||||||
} = useLabel();
|
|
||||||
const { projectStates } = useProjectState();
|
|
||||||
// derived values
|
// derived values
|
||||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
|
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issueStore?.viewFlags || {};
|
||||||
// user role validation
|
// user role validation
|
||||||
@ -86,27 +82,31 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
|||||||
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
[issueFiltersStore, projectId, workspaceSlug, viewId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderQuickActions = useCallback(
|
||||||
|
(issue: TIssue, customActionButton?: React.ReactElement, portalElement?: HTMLDivElement | null) => (
|
||||||
|
<QuickActions
|
||||||
|
customActionButton={customActionButton}
|
||||||
|
issue={issue}
|
||||||
|
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||||
|
handleUpdate={
|
||||||
|
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
|
||||||
|
}
|
||||||
|
handleRemoveFromView={
|
||||||
|
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||||
|
}
|
||||||
|
portalElement={portalElement}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[handleIssues]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SpreadsheetView
|
<SpreadsheetView
|
||||||
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
|
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
|
||||||
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
|
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
|
||||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||||
issues={issues}
|
issues={issues}
|
||||||
quickActions={(issue, customActionButton) => (
|
quickActions={renderQuickActions}
|
||||||
<QuickActions
|
|
||||||
customActionButton={customActionButton}
|
|
||||||
issue={issue}
|
|
||||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
|
||||||
handleUpdate={
|
|
||||||
issueActions[EIssueActions.UPDATE] ? async (data) => handleIssues(data, EIssueActions.UPDATE) : undefined
|
|
||||||
}
|
|
||||||
handleRemoveFromView={
|
|
||||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
labels={projectLabels ?? []}
|
|
||||||
states={projectStates}
|
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
quickAddCallback={issueStore.quickAddIssue}
|
quickAddCallback={issueStore.quickAddIssue}
|
||||||
|
@ -1,56 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { ProjectMemberDropdown } from "components/dropdowns";
|
import { ProjectMemberDropdown } from "components/dropdowns";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
const { issue, onChange, disabled } = props;
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<ProjectMemberDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
value={issue?.assignee_ids ?? []}
|
||||||
<ProjectMemberDropdown
|
onChange={(data) => onChange(issue, { assignee_ids: data })}
|
||||||
value={issueDetail?.assignee_ids ?? []}
|
projectId={issue?.project_id}
|
||||||
onChange={(data) => onChange(issueDetail, { assignee_ids: data })}
|
disabled={disabled}
|
||||||
projectId={issueDetail?.project_id}
|
multiple
|
||||||
disabled={disabled}
|
placeholder="Assignees"
|
||||||
multiple
|
buttonVariant={
|
||||||
placeholder="Assignees"
|
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"
|
||||||
buttonVariant={issueDetail.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
}
|
||||||
buttonClassName="text-left"
|
buttonClassName="text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId) => (
|
|
||||||
<SpreadsheetAssigneeColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,39 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
// types
|
// types
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
expandedIssues: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetAttachmentColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) => {
|
||||||
const { issueId, expandedIssues } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
|
||||||
{issueDetail?.attachment_count} {issueDetail?.attachment_count === 1 ? "attachment" : "attachments"}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className={`h-11`}>
|
|
||||||
<SpreadsheetAttachmentColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,176 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// hooks
|
|
||||||
import { useProject } from "hooks/store";
|
|
||||||
// components
|
|
||||||
import { SpreadsheetColumn } from "components/issues";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
|
||||||
displayProperties: IIssueDisplayProperties;
|
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
|
||||||
expandedIssues: string[];
|
|
||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
|
||||||
handleUpdateIssue: (issue: TIssue, data: Partial<TIssue>) => void;
|
|
||||||
issues: TIssue[] | undefined;
|
|
||||||
labels?: IIssueLabel[] | undefined;
|
|
||||||
states?: IState[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpreadsheetColumnsList: React.FC<Props> = observer((props) => {
|
|
||||||
const {
|
|
||||||
canEditProperties,
|
|
||||||
displayFilters,
|
|
||||||
displayProperties,
|
|
||||||
expandedIssues,
|
|
||||||
handleDisplayFilterUpdate,
|
|
||||||
handleUpdateIssue,
|
|
||||||
issues,
|
|
||||||
labels,
|
|
||||||
states,
|
|
||||||
} = props;
|
|
||||||
// store hooks
|
|
||||||
const { currentProjectDetails } = useProject();
|
|
||||||
|
|
||||||
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{displayProperties.state && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
states={states}
|
|
||||||
property="state"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.priority && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="priority"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.assignee && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="assignee"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.labels && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
labels={labels}
|
|
||||||
property="labels"
|
|
||||||
/>
|
|
||||||
)}{" "}
|
|
||||||
{displayProperties.start_date && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="start_date"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.due_date && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="due_date"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.estimate && isEstimateEnabled && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="estimate"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.created_on && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="created_on"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.updated_on && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="updated_on"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.link && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="link"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.attachment_count && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="attachment_count"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayProperties.sub_issue_count && (
|
|
||||||
<SpreadsheetColumn
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
|
||||||
handleUpdateIssue={handleUpdateIssue}
|
|
||||||
issues={issues}
|
|
||||||
property="sub_issue_count"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,38 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
expandedIssues: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issueId, expandedIssues }) => {
|
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
const { issue } = props;
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||||
{issueDetail && (
|
{renderFormattedDate(issue.created_at)}
|
||||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
</div>
|
||||||
{renderFormattedDate(issueDetail.created_at)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className="h-11">
|
|
||||||
<SpreadsheetCreatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { DateDropdown } from "components/dropdowns";
|
import { DateDropdown } from "components/dropdowns";
|
||||||
// helpers
|
// helpers
|
||||||
@ -9,49 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
const { issue, onChange, disabled } = props;
|
||||||
|
|
||||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<DateDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
value={issue.target_date}
|
||||||
<DateDropdown
|
onChange={(data) => onChange(issue, { target_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||||
value={issueDetail.target_date}
|
disabled={disabled}
|
||||||
onChange={(data) => onChange(issueDetail, { target_date: data ? renderFormattedPayloadDate(data) : null })}
|
placeholder="Due date"
|
||||||
disabled={disabled}
|
buttonVariant="transparent-with-text"
|
||||||
placeholder="Due date"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonVariant="transparent-with-text"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName="rounded-none text-left"
|
/>
|
||||||
buttonContainerClassName="w-full"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId) => (
|
|
||||||
<SpreadsheetDueDateColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,56 +1,29 @@
|
|||||||
// hooks
|
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { EstimateDropdown } from "components/dropdowns";
|
import { EstimateDropdown } from "components/dropdowns";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, onChange, expandedIssues, disabled } = props;
|
const { issue, onChange, disabled } = props;
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<EstimateDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
value={issue.estimate_point}
|
||||||
<EstimateDropdown
|
onChange={(data) => onChange(issue, { estimate_point: data })}
|
||||||
value={issueDetail.estimate_point}
|
projectId={issue.project_id}
|
||||||
onChange={(data) => onChange(issueDetail, { estimate_point: data })}
|
disabled={disabled}
|
||||||
projectId={issueDetail.project_id}
|
buttonVariant="transparent-with-text"
|
||||||
disabled={disabled}
|
buttonClassName="rounded-none text-left"
|
||||||
buttonVariant="transparent-with-text"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName="rounded-none text-left"
|
/>
|
||||||
buttonContainerClassName="w-full"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId) => (
|
|
||||||
<SpreadsheetEstimateColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
//ui
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
import {
|
||||||
|
ArrowDownWideNarrow,
|
||||||
|
ArrowUpNarrowWide,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
Eraser,
|
||||||
|
ListFilter,
|
||||||
|
MoveRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
//hooks
|
||||||
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
//types
|
||||||
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types";
|
||||||
|
//constants
|
||||||
|
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
property: keyof IIssueDisplayProperties;
|
||||||
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpreadsheetHeaderColumn = (props: Props) => {
|
||||||
|
const { displayFilters, handleDisplayFilterUpdate, property } = props;
|
||||||
|
|
||||||
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
|
"spreadsheetViewSorting",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
|
||||||
|
"spreadsheetViewActiveSortingProperty",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property];
|
||||||
|
|
||||||
|
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||||
|
handleDisplayFilterUpdate({ order_by: order });
|
||||||
|
|
||||||
|
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||||
|
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomMenu
|
||||||
|
customButtonClassName="!w-full"
|
||||||
|
className="!w-full"
|
||||||
|
customButton={
|
||||||
|
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
|
||||||
|
{propertyDetails.title}
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 flex">
|
||||||
|
{activeSortingProperty === property && (
|
||||||
|
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full">
|
||||||
|
<ListFilter className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="xl"
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-1.5 px-1 ${
|
||||||
|
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && <CheckIcon className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between gap-1.5 px-1 ${
|
||||||
|
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
|
||||||
|
<span>{propertyDetails.descendingOrderTitle}</span>
|
||||||
|
<MoveRight className="h-3 w-3" />
|
||||||
|
<span>{propertyDetails.ascendingOrderTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
|
||||||
|
<CheckIcon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{selectedMenuItem &&
|
||||||
|
selectedMenuItem !== "" &&
|
||||||
|
displayFilters?.order_by !== "-created_at" &&
|
||||||
|
selectedMenuItem.includes(property) && (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-custom-background-80" : ""}`}
|
||||||
|
key={property}
|
||||||
|
onClick={() => handleOrderBy("-created_at", property)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 px-1">
|
||||||
|
<Eraser className="h-3 w-3" />
|
||||||
|
<span>Clear sorting</span>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,5 @@
|
|||||||
export * from "./issue";
|
|
||||||
export * from "./assignee-column";
|
export * from "./assignee-column";
|
||||||
export * from "./attachment-column";
|
export * from "./attachment-column";
|
||||||
export * from "./columns-list";
|
|
||||||
export * from "./created-on-column";
|
export * from "./created-on-column";
|
||||||
export * from "./due-date-column";
|
export * from "./due-date-column";
|
||||||
export * from "./estimate-column";
|
export * from "./estimate-column";
|
||||||
@ -11,4 +9,4 @@ export * from "./priority-column";
|
|||||||
export * from "./start-date-column";
|
export * from "./start-date-column";
|
||||||
export * from "./state-column";
|
export * from "./state-column";
|
||||||
export * from "./sub-issue-column";
|
export * from "./sub-issue-column";
|
||||||
export * from "./updated-on-column";
|
export * from "./updated-on-column";
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./spreadsheet-issue-column";
|
|
||||||
export * from "./issue-column";
|
|
@ -1,114 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
||||||
// components
|
|
||||||
import { Tooltip } from "@plane/ui";
|
|
||||||
// hooks
|
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueDisplayProperties } from "@plane/types";
|
|
||||||
import { useProject } from "hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issue: TIssue;
|
|
||||||
expanded: boolean;
|
|
||||||
handleToggleExpand: (issueId: string) => void;
|
|
||||||
properties: IIssueDisplayProperties;
|
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
|
||||||
nestingLevel: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssueColumn: React.FC<Props> = ({
|
|
||||||
issue,
|
|
||||||
expanded,
|
|
||||||
handleToggleExpand,
|
|
||||||
properties,
|
|
||||||
quickActions,
|
|
||||||
canEditProperties,
|
|
||||||
nestingLevel,
|
|
||||||
}) => {
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
// hooks
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
// states
|
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
|
||||||
|
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
|
||||||
const { query } = router;
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const paddingLeft = `${nestingLevel * 54}px`;
|
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
|
||||||
|
|
||||||
const customActionButton = (
|
|
||||||
<div
|
|
||||||
ref={menuActionRef}
|
|
||||||
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
|
||||||
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
onClick={() => setIsMenuActive(!isMenuActive)}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="group top-0 flex h-11 w-[28rem] items-center truncate border-b border-custom-border-100 bg-custom-background-100 text-sm">
|
|
||||||
{properties.key && (
|
|
||||||
<div
|
|
||||||
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
|
|
||||||
style={issue.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
|
||||||
>
|
|
||||||
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
|
|
||||||
<span
|
|
||||||
className={`flex items-center justify-center font-medium opacity-100 group-hover:opacity-0 ${
|
|
||||||
isMenuActive ? "!opacity-0" : ""
|
|
||||||
} `}
|
|
||||||
>
|
|
||||||
{getProjectById(issue.project_id)?.identifier}-{issue.sequence_id}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{canEditProperties(issue.project_id) && (
|
|
||||||
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
|
||||||
{quickActions(issue, customActionButton)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{issue.sub_issues_count > 0 && (
|
|
||||||
<div className="flex h-6 w-6 items-center justify-center">
|
|
||||||
<button
|
|
||||||
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-custom-background-90 hover:text-custom-text-100"
|
|
||||||
onClick={() => handleToggleExpand(issue.id)}
|
|
||||||
>
|
|
||||||
<ChevronRight className={`h-3.5 w-3.5 ${expanded ? "rotate-90" : ""}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-full overflow-hidden">
|
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
|
||||||
<div
|
|
||||||
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
|
||||||
>
|
|
||||||
{issue.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,81 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { IssueColumn } from "components/issues";
|
|
||||||
// hooks
|
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueDisplayProperties } from "@plane/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issueId: string;
|
|
||||||
expandedIssues: string[];
|
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
|
||||||
properties: IIssueDisplayProperties;
|
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
|
||||||
nestingLevel?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
|
||||||
issueId,
|
|
||||||
expandedIssues,
|
|
||||||
setExpandedIssues,
|
|
||||||
properties,
|
|
||||||
quickActions,
|
|
||||||
canEditProperties,
|
|
||||||
nestingLevel = 0,
|
|
||||||
}) => {
|
|
||||||
const handleToggleExpand = (issueId: string) => {
|
|
||||||
setExpandedIssues((prevState) => {
|
|
||||||
const newArray = [...prevState];
|
|
||||||
const index = newArray.indexOf(issueId);
|
|
||||||
|
|
||||||
if (index > -1) newArray.splice(index, 1);
|
|
||||||
else newArray.push(issueId);
|
|
||||||
|
|
||||||
return newArray;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{issueDetail && (
|
|
||||||
<IssueColumn
|
|
||||||
issue={issueDetail}
|
|
||||||
expanded={isExpanded}
|
|
||||||
handleToggleExpand={handleToggleExpand}
|
|
||||||
properties={properties}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
nestingLevel={nestingLevel}
|
|
||||||
quickActions={quickActions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<SpreadsheetIssuesColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
setExpandedIssues={setExpandedIssues}
|
|
||||||
properties={properties}
|
|
||||||
quickActions={quickActions}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
nestingLevel={nestingLevel + 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,70 +1,39 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { IssuePropertyLabels } from "../../properties";
|
import { IssuePropertyLabels } from "../../properties";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useLabel } from "hooks/store";
|
import { useLabel } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueLabel } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
labels: IIssueLabel[] | undefined;
|
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, onChange, labels, expandedIssues, disabled } = props;
|
const { issue, onChange, disabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { labelMap } = useLabel();
|
const { labelMap } = useLabel();
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
|
||||||
|
|
||||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
const defaultLabelOptions = issueDetail?.label_ids?.map((id) => labelMap[id]) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<IssuePropertyLabels
|
||||||
{issueDetail && (
|
projectId={issue.project_id ?? null}
|
||||||
<IssuePropertyLabels
|
value={issue.label_ids}
|
||||||
projectId={issueDetail.project_id ?? null}
|
defaultOptions={defaultLabelOptions}
|
||||||
value={issueDetail.label_ids}
|
onChange={(data) => {
|
||||||
defaultOptions={defaultLabelOptions}
|
onChange(issue, { label_ids: data });
|
||||||
onChange={(data) => {
|
}}
|
||||||
onChange(issueDetail, { label_ids: data });
|
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
||||||
}}
|
buttonClassName="px-2.5 h-full"
|
||||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
hideDropdownArrow
|
||||||
buttonClassName="px-2.5 h-full"
|
maxRender={1}
|
||||||
hideDropdownArrow
|
disabled={disabled}
|
||||||
maxRender={1}
|
placeholderText="Select labels"
|
||||||
disabled={disabled}
|
/>
|
||||||
placeholderText="Select labels"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className={`h-11`}>
|
|
||||||
<SpreadsheetLabelColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
labels={labels}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,39 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
expandedIssues: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetLinkColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, expandedIssues } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
|
||||||
{issueDetail?.link_count} {issueDetail?.link_count === 1 ? "link" : "links"}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className={`h-11`}>
|
|
||||||
<SpreadsheetLinkColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,54 +1,29 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { PriorityDropdown } from "components/dropdowns";
|
import { PriorityDropdown } from "components/dropdowns";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetPriorityColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, onChange, expandedIssues, disabled } = props;
|
const { issue, onChange, disabled } = props;
|
||||||
// store hooks
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
// derived values
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<PriorityDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
value={issue.priority}
|
||||||
<PriorityDropdown
|
onChange={(data) => onChange(issue, { priority: data })}
|
||||||
value={issueDetail.priority}
|
disabled={disabled}
|
||||||
onChange={(data) => onChange(issueDetail, { priority: data })}
|
buttonVariant="transparent-with-text"
|
||||||
disabled={disabled}
|
buttonClassName="rounded-none text-left"
|
||||||
buttonVariant="transparent-with-text"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName="rounded-none text-left"
|
/>
|
||||||
buttonContainerClassName="w-full"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<SpreadsheetPriorityColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { DateDropdown } from "components/dropdowns";
|
import { DateDropdown } from "components/dropdowns";
|
||||||
// helpers
|
// helpers
|
||||||
@ -9,50 +8,25 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
|||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, formData: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({ issueId, onChange, expandedIssues, disabled }) => {
|
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
const { issue, onChange, disabled } = props;
|
||||||
|
|
||||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<DateDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
value={issue.start_date}
|
||||||
<DateDropdown
|
onChange={(data) => onChange(issue, { start_date: data ? renderFormattedPayloadDate(data) : null })}
|
||||||
value={issueDetail.start_date}
|
disabled={disabled}
|
||||||
onChange={(data) => onChange(issueDetail, { start_date: data ? renderFormattedPayloadDate(data) : null })}
|
placeholder="Start date"
|
||||||
disabled={disabled}
|
buttonVariant="transparent-with-text"
|
||||||
placeholder="Start date"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonVariant="transparent-with-text"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName="rounded-none text-left"
|
/>
|
||||||
buttonContainerClassName="w-full"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId) => (
|
|
||||||
<SpreadsheetStartDateColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,59 +1,30 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
import { useIssueDetail } from "hooks/store";
|
|
||||||
// components
|
// components
|
||||||
import { StateDropdown } from "components/dropdowns";
|
import { StateDropdown } from "components/dropdowns";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IState } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>) => void;
|
||||||
states: IState[] | undefined;
|
|
||||||
expandedIssues: string[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
||||||
const { issueId, onChange, states, expandedIssues, disabled } = props;
|
const { issue, onChange, disabled } = props;
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading, mutateSubIssues } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
{issueDetail && (
|
<StateDropdown
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
projectId={issue.project_id}
|
||||||
<StateDropdown
|
value={issue.state_id}
|
||||||
projectId={issueDetail.project_id}
|
onChange={(data) => onChange(issue, { state_id: data })}
|
||||||
value={issueDetail.state_id}
|
disabled={disabled}
|
||||||
onChange={(data) => onChange(issueDetail, { state_id: data })}
|
buttonVariant="transparent-with-text"
|
||||||
disabled={disabled}
|
buttonClassName="rounded-none text-left"
|
||||||
buttonVariant="transparent-with-text"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName="rounded-none text-left"
|
/>
|
||||||
buttonContainerClassName="w-full"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId) => (
|
|
||||||
<SpreadsheetStateColumn
|
|
||||||
key={subIssueId}
|
|
||||||
issueId={subIssueId}
|
|
||||||
onChange={onChange}
|
|
||||||
states={states}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,37 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
expandedIssues: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetSubIssueColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, expandedIssues } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||||
<div className="flex h-11 w-full items-center px-2.5 py-1 text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
{issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
||||||
{issueDetail?.sub_issues_count} {issueDetail?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className={`h-11`}>
|
|
||||||
<SpreadsheetSubIssueColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,43 +1,19 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// hooks
|
import { observer } from "mobx-react-lite";
|
||||||
// import useSubIssue from "hooks/use-sub-issue";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issue: TIssue;
|
||||||
expandedIssues: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = (props) => {
|
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issueId, expandedIssues } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issueId) > -1;
|
|
||||||
|
|
||||||
// const { subIssues, isLoading } = useSubIssue(issue.project_id, issue.id, isExpanded);
|
|
||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
||||||
{issueDetail && (
|
{renderFormattedDate(issue.updated_at)}
|
||||||
<div className="flex h-11 w-full items-center justify-center text-xs border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80">
|
</div>
|
||||||
{renderFormattedDate(issueDetail.updated_at)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded &&
|
|
||||||
subIssues &&
|
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<div className={`h-11`}>
|
|
||||||
<SpreadsheetUpdatedOnColumn key={subIssueId} issueId={subIssueId} expandedIssues={expandedIssues} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./columns";
|
export * from "./columns";
|
||||||
export * from "./roots";
|
export * from "./roots";
|
||||||
export * from "./spreadsheet-column";
|
|
||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./quick-add-issue-form";
|
export * from "./quick-add-issue-form";
|
||||||
|
186
web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
Normal file
186
web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { IIssueDisplayProperties, TIssue, TIssueMap } from "@plane/types";
|
||||||
|
import { SPREADSHEET_PROPERTY_DETAILS, SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import { useIssueDetail, useProject } from "hooks/store";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
import { EIssueActions } from "../types";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
displayProperties: IIssueDisplayProperties;
|
||||||
|
isEstimateEnabled: boolean;
|
||||||
|
quickActions: (
|
||||||
|
issue: TIssue,
|
||||||
|
customActionButton?: React.ReactElement,
|
||||||
|
portalElement?: HTMLDivElement | null
|
||||||
|
) => React.ReactNode;
|
||||||
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
nestingLevel: number;
|
||||||
|
issueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||||
|
const {
|
||||||
|
displayProperties,
|
||||||
|
issueId,
|
||||||
|
isEstimateEnabled,
|
||||||
|
nestingLevel,
|
||||||
|
portalElement,
|
||||||
|
handleIssues,
|
||||||
|
quickActions,
|
||||||
|
canEditProperties,
|
||||||
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
// states
|
||||||
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const handleIssuePeekOverview = (issue: TIssue) => {
|
||||||
|
const { query } = router;
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||||
|
|
||||||
|
const issueDetail = issue.getIssueById(issueId);
|
||||||
|
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||||
|
|
||||||
|
const paddingLeft = `${nestingLevel * 54}px`;
|
||||||
|
|
||||||
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
const handleToggleExpand = () => {
|
||||||
|
setExpanded((prevState) => {
|
||||||
|
if (!prevState && workspaceSlug && issueDetail)
|
||||||
|
subIssuesStore.fetchSubIssues(workspaceSlug.toString(), issueDetail.project_id, issueDetail.id);
|
||||||
|
return !prevState;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const customActionButton = (
|
||||||
|
<div
|
||||||
|
ref={menuActionRef}
|
||||||
|
className={`w-full cursor-pointer rounded p-1 text-custom-sidebar-text-400 hover:bg-custom-background-80 ${
|
||||||
|
isMenuActive ? "bg-custom-background-80 text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!issueDetail) return null;
|
||||||
|
|
||||||
|
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
{/* first column/ issue name and key column */}
|
||||||
|
<td className="sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-l-0 after:border-custom-border-100 before:absolute before:h-full before:right-0 before:border before:border-l-0 before:border-custom-border-100">
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
|
<div
|
||||||
|
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
|
||||||
|
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||||
|
>
|
||||||
|
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-custom-text-100">
|
||||||
|
<span
|
||||||
|
className={`flex items-center justify-center font-medium group-hover:opacity-0 ${
|
||||||
|
isMenuActive ? "opacity-0" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{canEditProperties(issueDetail.project_id) && (
|
||||||
|
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
||||||
|
{quickActions(issueDetail, customActionButton, portalElement.current)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{issueDetail.sub_issues_count > 0 && (
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center">
|
||||||
|
<button
|
||||||
|
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-custom-background-90 hover:text-custom-text-100"
|
||||||
|
onClick={() => handleToggleExpand()}
|
||||||
|
>
|
||||||
|
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
<div className="w-full overflow-hidden">
|
||||||
|
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
||||||
|
<div
|
||||||
|
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
||||||
|
onClick={() => handleIssuePeekOverview(issueDetail)}
|
||||||
|
>
|
||||||
|
{issueDetail.name}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/* Rest of the columns */}
|
||||||
|
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
||||||
|
const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
|
||||||
|
|
||||||
|
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WithDisplayPropertiesHOC
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
displayPropertyKey={property}
|
||||||
|
shouldRenderProperty={shouldRenderProperty}
|
||||||
|
>
|
||||||
|
<td className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100">
|
||||||
|
<Column
|
||||||
|
issue={issueDetail}
|
||||||
|
onChange={(issue: TIssue, data: Partial<TIssue>) =>
|
||||||
|
handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)
|
||||||
|
}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{isExpanded &&
|
||||||
|
subIssues &&
|
||||||
|
subIssues.length > 0 &&
|
||||||
|
subIssues.map((subIssueId: string) => (
|
||||||
|
<SpreadsheetIssueRow
|
||||||
|
key={subIssueId}
|
||||||
|
issueId={subIssueId}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
quickActions={quickActions}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
|
nestingLevel={nestingLevel + 1}
|
||||||
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
portalElement={portalElement}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,233 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowDownWideNarrow,
|
|
||||||
ArrowUpNarrowWide,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
Eraser,
|
|
||||||
ListFilter,
|
|
||||||
MoveRight,
|
|
||||||
} from "lucide-react";
|
|
||||||
// hooks
|
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
// components
|
|
||||||
import {
|
|
||||||
SpreadsheetAssigneeColumn,
|
|
||||||
SpreadsheetAttachmentColumn,
|
|
||||||
SpreadsheetCreatedOnColumn,
|
|
||||||
SpreadsheetDueDateColumn,
|
|
||||||
SpreadsheetEstimateColumn,
|
|
||||||
SpreadsheetLabelColumn,
|
|
||||||
SpreadsheetLinkColumn,
|
|
||||||
SpreadsheetPriorityColumn,
|
|
||||||
SpreadsheetStartDateColumn,
|
|
||||||
SpreadsheetStateColumn,
|
|
||||||
SpreadsheetSubIssueColumn,
|
|
||||||
SpreadsheetUpdatedOnColumn,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueDisplayFilterOptions, IIssueLabel, IState, TIssueOrderByOptions } from "@plane/types";
|
|
||||||
// constants
|
|
||||||
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
|
||||||
expandedIssues: string[];
|
|
||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
|
||||||
handleUpdateIssue: (issue: TIssue, data: Partial<TIssue>) => void;
|
|
||||||
issues: TIssue[] | undefined;
|
|
||||||
property: string;
|
|
||||||
labels?: IIssueLabel[] | undefined;
|
|
||||||
states?: IState[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpreadsheetColumn: React.FC<Props> = (props) => {
|
|
||||||
const {
|
|
||||||
canEditProperties,
|
|
||||||
displayFilters,
|
|
||||||
expandedIssues,
|
|
||||||
handleDisplayFilterUpdate,
|
|
||||||
handleUpdateIssue,
|
|
||||||
issues,
|
|
||||||
property,
|
|
||||||
labels,
|
|
||||||
states,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
|
||||||
"spreadsheetViewSorting",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
|
|
||||||
"spreadsheetViewActiveSortingProperty",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
|
||||||
handleDisplayFilterUpdate({ order_by: order });
|
|
||||||
|
|
||||||
setSelectedMenuItem(`${order}_${itemKey}`);
|
|
||||||
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
const propertyDetails = SPREADSHEET_PROPERTY_DETAILS[property];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-max w-full max-w-max flex-col bg-custom-background-100">
|
|
||||||
<div className="sticky top-0 z-[1] flex h-11 w-full min-w-[8rem] items-center border border-l-0 border-custom-border-100 bg-custom-background-90 px-4 py-1 text-sm font-medium">
|
|
||||||
<CustomMenu
|
|
||||||
customButtonClassName="!w-full"
|
|
||||||
className="!w-full"
|
|
||||||
customButton={
|
|
||||||
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
|
|
||||||
{propertyDetails.title}
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 flex">
|
|
||||||
{activeSortingProperty === property && (
|
|
||||||
<div className="flex h-3.5 w-3.5 items-center justify-center rounded-full">
|
|
||||||
<ListFilter className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width="xl"
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between gap-1.5 px-1 ${
|
|
||||||
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
|
|
||||||
<span>{propertyDetails.ascendingOrderTitle}</span>
|
|
||||||
<MoveRight className="h-3 w-3" />
|
|
||||||
<span>{propertyDetails.descendingOrderTitle}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && (
|
|
||||||
<CheckIcon className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between gap-1.5 px-1 ${
|
|
||||||
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
|
|
||||||
<span>{propertyDetails.descendingOrderTitle}</span>
|
|
||||||
<MoveRight className="h-3 w-3" />
|
|
||||||
<span>{propertyDetails.ascendingOrderTitle}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
|
|
||||||
<CheckIcon className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{selectedMenuItem &&
|
|
||||||
selectedMenuItem !== "" &&
|
|
||||||
displayFilters?.order_by !== "-created_at" &&
|
|
||||||
selectedMenuItem.includes(property) && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-custom-background-80" : ""}`}
|
|
||||||
key={property}
|
|
||||||
onClick={() => handleOrderBy("-created_at", property)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 px-1">
|
|
||||||
<Eraser className="h-3 w-3" />
|
|
||||||
<span>Clear sorting</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-full w-full min-w-[8rem]">
|
|
||||||
{issues?.map((issue) => {
|
|
||||||
const disableUserActions = !canEditProperties(issue.project_id);
|
|
||||||
return (
|
|
||||||
<div key={`${property}-${issue.id}`} className={`h-fit ${disableUserActions ? "" : "cursor-pointer"}`}>
|
|
||||||
{property === "state" ? (
|
|
||||||
<SpreadsheetStateColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
states={states}
|
|
||||||
/>
|
|
||||||
) : property === "priority" ? (
|
|
||||||
<SpreadsheetPriorityColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "estimate" ? (
|
|
||||||
<SpreadsheetEstimateColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "assignee" ? (
|
|
||||||
<SpreadsheetAssigneeColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "labels" ? (
|
|
||||||
<SpreadsheetLabelColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
labels={labels}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "start_date" ? (
|
|
||||||
<SpreadsheetStartDateColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "due_date" ? (
|
|
||||||
<SpreadsheetDueDateColumn
|
|
||||||
disabled={disableUserActions}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
issueId={issue.id}
|
|
||||||
onChange={(issue: TIssue, data: Partial<TIssue>) => handleUpdateIssue(issue, data)}
|
|
||||||
/>
|
|
||||||
) : property === "created_on" ? (
|
|
||||||
<SpreadsheetCreatedOnColumn expandedIssues={expandedIssues} issueId={issue.id} />
|
|
||||||
) : property === "updated_on" ? (
|
|
||||||
<SpreadsheetUpdatedOnColumn expandedIssues={expandedIssues} issueId={issue.id} />
|
|
||||||
) : property === "link" ? (
|
|
||||||
<SpreadsheetLinkColumn expandedIssues={expandedIssues} issueId={issue.id} />
|
|
||||||
) : property === "attachment_count" ? (
|
|
||||||
<SpreadsheetAttachmentColumn expandedIssues={expandedIssues} issueId={issue.id} />
|
|
||||||
) : property === "sub_issue_count" ? (
|
|
||||||
<SpreadsheetSubIssueColumn expandedIssues={expandedIssues} issueId={issue.id} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,59 @@
|
|||||||
|
// ui
|
||||||
|
import { LayersIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
|
// components
|
||||||
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import { SpreadsheetHeaderColumn } from "./columns/header-column";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
displayProperties: IIssueDisplayProperties;
|
||||||
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
isEstimateEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SpreadsheetHeader = (props: Props) => {
|
||||||
|
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead className="sticky top-0 left-0 z-[1] border-b-[0.5px] border-custom-border-100">
|
||||||
|
<tr>
|
||||||
|
<th className="sticky left-0 z-[1] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100">
|
||||||
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
|
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
||||||
|
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
||||||
|
</span>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
|
||||||
|
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
|
||||||
|
Issue
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
||||||
|
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WithDisplayPropertiesHOC
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
displayPropertyKey={property}
|
||||||
|
shouldRenderProperty={shouldRenderProperty}
|
||||||
|
>
|
||||||
|
<th className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100">
|
||||||
|
<SpreadsheetHeaderColumn
|
||||||
|
displayFilters={displayFilters}
|
||||||
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
property={property}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
</WithDisplayPropertiesHOC>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
};
|
@ -1,20 +1,26 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues";
|
import { Spinner } from "@plane/ui";
|
||||||
import { Spinner, LayersIcon } from "@plane/ui";
|
import { SpreadsheetQuickAddIssueForm } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState } from "@plane/types";
|
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
|
import { useProject } from "hooks/store";
|
||||||
|
import { SpreadsheetHeader } from "./spreadsheet-header";
|
||||||
|
import { SpreadsheetIssueRow } from "./issue-row";
|
||||||
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
issues: TIssue[] | undefined;
|
issues: TIssue[] | undefined;
|
||||||
labels?: IIssueLabel[] | undefined;
|
quickActions: (
|
||||||
states?: IState[] | undefined;
|
issue: TIssue,
|
||||||
quickActions: (issue: TIssue, customActionButton: any) => React.ReactNode;
|
customActionButton?: React.ReactElement,
|
||||||
|
portalElement?: HTMLDivElement | null
|
||||||
|
) => React.ReactNode;
|
||||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -35,8 +41,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
displayFilters,
|
displayFilters,
|
||||||
handleDisplayFilterUpdate,
|
handleDisplayFilterUpdate,
|
||||||
issues,
|
issues,
|
||||||
labels,
|
|
||||||
states,
|
|
||||||
quickActions,
|
quickActions,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
@ -46,16 +50,36 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const isScrolled = useRef(false);
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
// refs
|
// refs
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLTableElement | null>(null);
|
||||||
|
const portalRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { currentProjectDetails } = useProject();
|
||||||
|
|
||||||
|
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
const scrollLeft = containerRef.current.scrollLeft;
|
const scrollLeft = containerRef.current.scrollLeft;
|
||||||
setIsScrolled(scrollLeft > 0);
|
|
||||||
|
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
|
||||||
|
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
|
||||||
|
|
||||||
|
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
|
||||||
|
if (scrollLeft > 0 !== isScrolled.current) {
|
||||||
|
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
|
||||||
|
|
||||||
|
for (let i = 0; i < firtColumns.length; i++) {
|
||||||
|
const shadow = i === 0 ? headerShadow : columnShadow;
|
||||||
|
if (scrollLeft > 0) {
|
||||||
|
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
|
||||||
|
} else {
|
||||||
|
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isScrolled.current = scrollLeft > 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -76,105 +100,38 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full overflow-x-auto whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
<div className="relative flex flex-col h-full w-full overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||||
<div className="flex h-full w-full flex-col">
|
<div ref={portalRef} className="spreadsheet-menu-portal" />
|
||||||
<div
|
<div ref={containerRef} className="horizontal-scroll-enable h-full w-full">
|
||||||
ref={containerRef}
|
<table className="divide-x-[0.5px] divide-custom-border-200 overflow-y-auto">
|
||||||
className="horizontal-scroll-enable flex divide-x-[0.5px] divide-custom-border-200 overflow-y-auto"
|
<SpreadsheetHeader
|
||||||
>
|
displayProperties={displayProperties}
|
||||||
{issues && issues.length > 0 && (
|
displayFilters={displayFilters}
|
||||||
<>
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
<div className="sticky left-0 z-[2] w-[28rem]">
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
<div
|
/>
|
||||||
className="relative z-[2] flex h-max w-full flex-col bg-custom-background-100"
|
<tbody>
|
||||||
style={{
|
{issues.map(({ id }) => (
|
||||||
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.05)" : "",
|
<SpreadsheetIssueRow
|
||||||
}}
|
key={id}
|
||||||
>
|
issueId={id}
|
||||||
<div className="sticky top-0 z-[2] flex h-11 w-full items-center border border-l-0 border-custom-border-100 bg-custom-background-90 text-sm font-medium">
|
|
||||||
{displayProperties.key && (
|
|
||||||
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
|
||||||
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="flex h-full w-full flex-grow items-center justify-center px-4 py-2.5">
|
|
||||||
<LayersIcon className="mr-1.5 h-4 w-4 text-custom-text-400" />
|
|
||||||
Issue
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{issues.map((issue, index) =>
|
|
||||||
issue ? (
|
|
||||||
<SpreadsheetIssuesColumn
|
|
||||||
key={`${issue?.id}_${index}`}
|
|
||||||
issueId={issue.id}
|
|
||||||
expandedIssues={expandedIssues}
|
|
||||||
setExpandedIssues={setExpandedIssues}
|
|
||||||
properties={displayProperties}
|
|
||||||
quickActions={quickActions}
|
|
||||||
canEditProperties={canEditProperties}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SpreadsheetColumnsList
|
|
||||||
displayFilters={displayFilters}
|
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
expandedIssues={expandedIssues}
|
nestingLevel={0}
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
handleUpdateIssue={(issue, data) => handleIssues({ ...issue, ...data }, EIssueActions.UPDATE)}
|
handleIssues={handleIssues}
|
||||||
issues={issues}
|
portalElement={portalRef}
|
||||||
labels={labels}
|
|
||||||
states={states}
|
|
||||||
/>
|
/>
|
||||||
</>
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-custom-border-100">
|
||||||
|
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||||
|
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||||
|
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||||
)}
|
)}
|
||||||
<div /> {/* empty div to show right most border */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-custom-border-100">
|
|
||||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
|
||||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
|
||||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* {!disableUserActions &&
|
|
||||||
!isInlineCreateIssueFormOpen &&
|
|
||||||
(type === "issue" ? (
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
|
|
||||||
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
New Issue
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<CustomMenu
|
|
||||||
className="sticky left-0 z-10"
|
|
||||||
customButton={
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
New Issue
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
optionsClassName="left-5 !w-36"
|
|
||||||
noBorder
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
|
||||||
Create new
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{openIssuesListModal && (
|
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
))} */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,8 +258,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
issueId={issue?.id}
|
issueId={issue?.id}
|
||||||
is_editable={uneditable}
|
|
||||||
is_archived={isAllowed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,32 +71,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [peekIssue, fetchIssue]);
|
}, [peekIssue, fetchIssue]);
|
||||||
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
|
||||||
|
|
||||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
|
||||||
|
|
||||||
const redirectToIssueDetail = () => {
|
|
||||||
router.push({
|
|
||||||
pathname: `/${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${
|
|
||||||
isArchived ? "archived-issues" : "issues"
|
|
||||||
}/${peekIssue.issueId}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
copyUrlToClipboard(
|
|
||||||
`${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${isArchived ? "archived-issues" : "issues"}/${
|
|
||||||
peekIssue.issueId
|
|
||||||
}`
|
|
||||||
).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Issue link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const issueOperations: TIssuePeekOperations = useMemo(
|
const issueOperations: TIssuePeekOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -168,6 +142,34 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
[addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule, setToastAlert]
|
[addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule, setToastAlert]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||||
|
|
||||||
|
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||||
|
|
||||||
|
const redirectToIssueDetail = () => {
|
||||||
|
router.push({
|
||||||
|
pathname: `/${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${
|
||||||
|
isArchived ? "archived-issues" : "issues"
|
||||||
|
}/${peekIssue.issueId}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copyUrlToClipboard(
|
||||||
|
`${peekIssue.workspaceSlug}/projects/${peekIssue.projectId}/${isArchived ? "archived-issues" : "issues"}/${
|
||||||
|
peekIssue.issueId
|
||||||
|
}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Issue link copied to clipboard.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const issueUpdate = async (_data: Partial<TIssue>) => {
|
const issueUpdate = async (_data: Partial<TIssue>) => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
||||||
|
@ -1,8 +1,22 @@
|
|||||||
import { TIssueOrderByOptions } from "@plane/types";
|
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||||
|
import {
|
||||||
|
SpreadsheetAssigneeColumn,
|
||||||
|
SpreadsheetAttachmentColumn,
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
SpreadsheetLinkColumn,
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
SpreadsheetSubIssueColumn,
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
} from "components/issues/issue-layouts/spreadsheet";
|
||||||
|
|
||||||
export const SPREADSHEET_PROPERTY_DETAILS: {
|
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@ -12,6 +26,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: TIssueOrderByOptions;
|
descendingOrderKey: TIssueOrderByOptions;
|
||||||
descendingOrderTitle: string;
|
descendingOrderTitle: string;
|
||||||
icon: FC<ISvgIcons>;
|
icon: FC<ISvgIcons>;
|
||||||
|
Column: React.FC<{ issue: TIssue; onChange: (issue: TIssue, data: Partial<TIssue>) => void; disabled: boolean }>;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
assignee: {
|
assignee: {
|
||||||
@ -21,6 +36,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "-assignees__first_name",
|
descendingOrderKey: "-assignees__first_name",
|
||||||
descendingOrderTitle: "Z",
|
descendingOrderTitle: "Z",
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
|
Column: SpreadsheetAssigneeColumn,
|
||||||
},
|
},
|
||||||
created_on: {
|
created_on: {
|
||||||
title: "Created on",
|
title: "Created on",
|
||||||
@ -29,6 +45,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "created_at",
|
descendingOrderKey: "created_at",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarDays,
|
icon: CalendarDays,
|
||||||
|
Column: SpreadsheetCreatedOnColumn,
|
||||||
},
|
},
|
||||||
due_date: {
|
due_date: {
|
||||||
title: "Due date",
|
title: "Due date",
|
||||||
@ -37,6 +54,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "target_date",
|
descendingOrderKey: "target_date",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarCheck,
|
icon: CalendarCheck,
|
||||||
|
Column: SpreadsheetDueDateColumn,
|
||||||
},
|
},
|
||||||
estimate: {
|
estimate: {
|
||||||
title: "Estimate",
|
title: "Estimate",
|
||||||
@ -45,6 +63,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "-estimate_point",
|
descendingOrderKey: "-estimate_point",
|
||||||
descendingOrderTitle: "High",
|
descendingOrderTitle: "High",
|
||||||
icon: Triangle,
|
icon: Triangle,
|
||||||
|
Column: SpreadsheetEstimateColumn,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
title: "Labels",
|
title: "Labels",
|
||||||
@ -53,6 +72,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "-labels__name",
|
descendingOrderKey: "-labels__name",
|
||||||
descendingOrderTitle: "Z",
|
descendingOrderTitle: "Z",
|
||||||
icon: Tag,
|
icon: Tag,
|
||||||
|
Column: SpreadsheetLabelColumn,
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
title: "Priority",
|
title: "Priority",
|
||||||
@ -61,6 +81,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "-priority",
|
descendingOrderKey: "-priority",
|
||||||
descendingOrderTitle: "Urgent",
|
descendingOrderTitle: "Urgent",
|
||||||
icon: Signal,
|
icon: Signal,
|
||||||
|
Column: SpreadsheetPriorityColumn,
|
||||||
},
|
},
|
||||||
start_date: {
|
start_date: {
|
||||||
title: "Start date",
|
title: "Start date",
|
||||||
@ -69,6 +90,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "start_date",
|
descendingOrderKey: "start_date",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarClock,
|
icon: CalendarClock,
|
||||||
|
Column: SpreadsheetStartDateColumn,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
title: "State",
|
title: "State",
|
||||||
@ -77,6 +99,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "-state__name",
|
descendingOrderKey: "-state__name",
|
||||||
descendingOrderTitle: "Z",
|
descendingOrderTitle: "Z",
|
||||||
icon: DoubleCircleIcon,
|
icon: DoubleCircleIcon,
|
||||||
|
Column: SpreadsheetStateColumn,
|
||||||
},
|
},
|
||||||
updated_on: {
|
updated_on: {
|
||||||
title: "Updated on",
|
title: "Updated on",
|
||||||
@ -85,6 +108,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "updated_at",
|
descendingOrderKey: "updated_at",
|
||||||
descendingOrderTitle: "Old",
|
descendingOrderTitle: "Old",
|
||||||
icon: CalendarDays,
|
icon: CalendarDays,
|
||||||
|
Column: SpreadsheetUpdatedOnColumn,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
title: "Link",
|
title: "Link",
|
||||||
@ -93,6 +117,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "link_count",
|
descendingOrderKey: "link_count",
|
||||||
descendingOrderTitle: "Least",
|
descendingOrderTitle: "Least",
|
||||||
icon: Link2,
|
icon: Link2,
|
||||||
|
Column: SpreadsheetLinkColumn,
|
||||||
},
|
},
|
||||||
attachment_count: {
|
attachment_count: {
|
||||||
title: "Attachment",
|
title: "Attachment",
|
||||||
@ -101,6 +126,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "attachment_count",
|
descendingOrderKey: "attachment_count",
|
||||||
descendingOrderTitle: "Least",
|
descendingOrderTitle: "Least",
|
||||||
icon: Paperclip,
|
icon: Paperclip,
|
||||||
|
Column: SpreadsheetAttachmentColumn,
|
||||||
},
|
},
|
||||||
sub_issue_count: {
|
sub_issue_count: {
|
||||||
title: "Sub-issue",
|
title: "Sub-issue",
|
||||||
@ -109,5 +135,21 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
descendingOrderKey: "sub_issues_count",
|
descendingOrderKey: "sub_issues_count",
|
||||||
descendingOrderTitle: "Least",
|
descendingOrderTitle: "Least",
|
||||||
icon: LayersIcon,
|
icon: LayersIcon,
|
||||||
|
Column: SpreadsheetSubIssueColumn,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
||||||
|
"state",
|
||||||
|
"priority",
|
||||||
|
"assignee",
|
||||||
|
"labels",
|
||||||
|
"start_date",
|
||||||
|
"due_date",
|
||||||
|
"estimate",
|
||||||
|
"created_on",
|
||||||
|
"updated_on",
|
||||||
|
"link",
|
||||||
|
"attachment_count",
|
||||||
|
"sub_issue_count",
|
||||||
|
];
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
export const SWR_CONFIG = {
|
export const SWR_CONFIG = {
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateIfStale: false,
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnMount: true,
|
||||||
|
refreshInterval: 600000,
|
||||||
errorRetryCount: 3,
|
errorRetryCount: 3,
|
||||||
};
|
};
|
||||||
|
@ -65,12 +65,16 @@ export class FileService extends APIService {
|
|||||||
|
|
||||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
const formData = new FormData();
|
try {
|
||||||
formData.append("asset", file);
|
const formData = new FormData();
|
||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("asset", file);
|
||||||
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
const data = await this.uploadFile(workspaceSlug, formData);
|
const data = await this.uploadFile(workspaceSlug, formData);
|
||||||
return data.asset;
|
return data.asset;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -2492,13 +2492,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.13.tgz#1e9521dea002c8d6de833d9fd928d4617623eab8"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.1.13.tgz#1e9521dea002c8d6de833d9fd928d4617623eab8"
|
||||||
integrity sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==
|
integrity sha512-HyDJfuDn5hzwGKZiANcvgz6wcum6bEgb4wmJnfej8XanTMJatNVv63TVxCJ10dSc9KGpPVcIkg6W8/joNXIEbw==
|
||||||
|
|
||||||
"@tiptap/extension-link@^2.1.13":
|
|
||||||
version "2.1.13"
|
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.1.13.tgz#ae4abd7c43292e3a1841488bfc7a687b2f014249"
|
|
||||||
integrity sha512-wuGMf3zRtMHhMrKm9l6Tft5M2N21Z0UP1dZ5t1IlOAvOeYV2QZ5UynwFryxGKLO0NslCBLF/4b/HAdNXbfXWUA==
|
|
||||||
dependencies:
|
|
||||||
linkifyjs "^4.1.0"
|
|
||||||
|
|
||||||
"@tiptap/extension-list-item@^2.1.13":
|
"@tiptap/extension-list-item@^2.1.13":
|
||||||
version "2.1.13"
|
version "2.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.13.tgz#3c62127df97974f3196866ec00ee397f4c9acdc4"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.13.tgz#3c62127df97974f3196866ec00ee397f4c9acdc4"
|
||||||
@ -2802,7 +2795,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
"@types/react@*", "@types/react@^18.2.42":
|
||||||
version "18.2.42"
|
version "18.2.42"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||||
@ -6071,7 +6064,7 @@ linkify-it@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro "^2.0.0"
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
linkifyjs@^4.1.0:
|
linkifyjs@^4.1.3:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
|
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
|
||||||
integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
|
integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
|
||||||
|
Loading…
Reference in New Issue
Block a user