Merge branch 'develop' into refactor/editor-wrapper

This commit is contained in:
M. Palanikannan 2024-01-11 20:24:11 +05:30 committed by Palanikannan1437
commit c199d90628
73 changed files with 1755 additions and 1518 deletions

View File

@ -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

View File

@ -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",

View File

@ -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;

View 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",
},
});

View File

@ -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();
}, },
}; };
}, },

View File

@ -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 lets 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;
},
});
}

View File

@ -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;
},
},
});
}

View File

@ -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;
},
},
});
}

View 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;
},
});

View File

@ -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,

View File

@ -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: {

View File

@ -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"
} }
} }

View File

@ -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>

View File

@ -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 {

View File

@ -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>
</> </>
); );

View File

@ -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}
/>
))} ))}
</> </>
); );

View File

@ -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) => {

View File

@ -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>
); );

View File

@ -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>

View File

@ -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>

View File

@ -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";

View File

@ -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>
); );
}; };

View File

@ -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}
/> />
))} ))}
</> </>

View File

@ -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>
); );
}); });

View File

@ -1,9 +0,0 @@
import { FC } from "react";
type TLabelExistingSelect = {};
export const LabelExistingSelect: FC<TLabelExistingSelect> = (props) => {
const {} = props;
return <></>;
};

View 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>
</>
);
});

View 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} />
);
};

View File

@ -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 &&

View File

@ -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>
</> </>

View File

@ -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">

View File

@ -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>

View File

@ -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>
); );
}) })

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
} }

View File

@ -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);
}} }}
> >

View File

@ -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);
}} }}
> >

View File

@ -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);
}} }}
> >

View File

@ -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);
}} }}
> >

View File

@ -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);
}} }}
> >

View File

@ -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}

View File

@ -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;

View File

@ -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}

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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"
/>
)}
</>
);
});

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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>
);
};

View File

@ -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";

View File

@ -1,2 +0,0 @@
export * from "./spreadsheet-issue-column";
export * from "./issue-column";

View File

@ -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>
</>
);
};

View File

@ -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}
/>
))}
</>
);
};

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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}
/>
))}
</>
); );
}; });

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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>
))}
</>
); );
}; });

View File

@ -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";

View 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}
/>
))}
</>
);
});

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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",
];

View File

@ -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,
}; };

View File

@ -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);
}
}; };
} }

View File

@ -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==