forked from github/plane
Compare commits
86 Commits
preview
...
refactor/m
Author | SHA1 | Date | |
---|---|---|---|
|
b49b1bf8fa | ||
|
547a265169 | ||
|
0f47762e6d | ||
|
844a3e4b42 | ||
|
b5b809500d | ||
|
7be038ac5a | ||
|
41fd9ce6e8 | ||
|
b1448c947e | ||
|
9c2ea8a7ae | ||
|
a39aa80e76 | ||
|
569a6c3383 | ||
|
405a398c6b | ||
|
f22705846d | ||
|
727042468a | ||
|
9ad1e73666 | ||
|
479c145b02 | ||
|
b70047b1d5 | ||
|
f60dcdc599 | ||
|
2643de80af | ||
|
5af753f475 | ||
|
3bf590b67e | ||
|
b2d17e6ec9 | ||
|
ccf6bd4e32 | ||
|
151662a442 | ||
|
c342ab302e | ||
|
c48cd3ee6e | ||
|
7c0c0da0f8 | ||
|
1b8d58a9a6 | ||
|
43404bfcdf | ||
|
310a2ca904 | ||
|
2b419c02a5 | ||
|
9831418a11 | ||
|
9a8dcc349f | ||
|
27f78dd283 | ||
|
0ebe36bdb3 | ||
|
6a430ed480 | ||
|
daa3094911 | ||
|
9b41b5baf5 | ||
|
2dcaccd4ec | ||
|
f69d34698a | ||
|
6d52801ea7 | ||
|
e96bc77215 | ||
|
a328c530d0 | ||
|
908f6716fe | ||
|
50c330db65 | ||
|
491592614d | ||
|
906caa636b | ||
|
12ce6e78e2 | ||
|
a25e5accd1 | ||
|
12b6ec4b49 | ||
|
70fe830151 | ||
|
e9490314cc | ||
|
f6d4ac95ed | ||
|
cc9ebc58bc | ||
|
cf34d4afbc | ||
|
7b04adf03a | ||
|
9136258926 | ||
|
d88a0885d5 | ||
|
fce6907465 | ||
|
3c9e62d308 | ||
|
28ce96aaca | ||
|
66022ea478 | ||
|
60883baea7 | ||
|
c67f08fca4 | ||
|
f579712092 | ||
|
3ffbb6ac17 | ||
|
0ec0ad6aba | ||
|
698021ab8b | ||
|
ada1bc009b | ||
|
834e672245 | ||
|
3b85444e1f | ||
|
04242800c9 | ||
|
a8c5a4155b | ||
|
0445c610bf | ||
|
c0e3c81a9b | ||
|
8c04e770c0 | ||
|
aef71fbc45 | ||
|
b9a6a00470 | ||
|
7c5936e463 | ||
|
b86c30baed | ||
|
15ef2bc030 | ||
|
ef630ef663 | ||
|
731309a932 | ||
|
9d334cf3a3 | ||
|
e9b6f86882 | ||
|
8d86087fee |
@ -330,7 +330,12 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.issue_objects.get(
|
||||
issue = Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
).get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
|
@ -4,8 +4,9 @@
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"lib": ["ES2015"],
|
||||
"lib": ["ES2015", "DOM"],
|
||||
"module": "ESNext",
|
||||
"target": "es6"
|
||||
"target": "es6",
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
|
41
packages/ui/dist/index.d.ts
vendored
Normal file
41
packages/ui/dist/index.d.ts
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
declare const Button: () => JSX.Element;
|
||||
|
||||
interface InputProps {
|
||||
type: string;
|
||||
id: string;
|
||||
value: string;
|
||||
name: string;
|
||||
onChange: () => void;
|
||||
className?: string;
|
||||
mode?: "primary" | "transparent" | "true-transparent";
|
||||
size?: "sm" | "md" | "lg";
|
||||
hasError?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|
||||
|
||||
interface TextAreaProps {
|
||||
id: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
mode?: "primary" | "transparent";
|
||||
hasError?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
declare const TextArea: React.ForwardRefExoticComponent<TextAreaProps & React.RefAttributes<HTMLTextAreaElement>>;
|
||||
|
||||
interface IRadialProgressBar {
|
||||
progress: number;
|
||||
}
|
||||
declare const RadialProgressBar: FC<IRadialProgressBar>;
|
||||
|
||||
export { Button, Input, InputProps, RadialProgressBar, TextArea, TextAreaProps };
|
157
packages/ui/dist/index.js
vendored
Normal file
157
packages/ui/dist/index.js
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.tsx
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
Button: () => Button,
|
||||
Input: () => Input,
|
||||
RadialProgressBar: () => RadialProgressBar,
|
||||
TextArea: () => TextArea
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
|
||||
// src/buttons/index.tsx
|
||||
var React = __toESM(require("react"));
|
||||
var Button = () => {
|
||||
return /* @__PURE__ */ React.createElement("button", null, "button");
|
||||
};
|
||||
|
||||
// src/form-fields/input.tsx
|
||||
var React2 = __toESM(require("react"));
|
||||
var Input = React2.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false
|
||||
} = props;
|
||||
return /* @__PURE__ */ React2.createElement("input", {
|
||||
id,
|
||||
ref,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
|
||||
});
|
||||
});
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
// src/form-fields/textarea.tsx
|
||||
var React3 = __toESM(require("react"));
|
||||
var useAutoSizeTextArea = (textAreaRef, value) => {
|
||||
React3.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
var TextArea = React3.forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = ""
|
||||
} = props;
|
||||
const textAreaRef = React3.useRef(ref);
|
||||
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
|
||||
return /* @__PURE__ */ React3.createElement("textarea", {
|
||||
id,
|
||||
name,
|
||||
ref: textAreaRef,
|
||||
placeholder,
|
||||
value,
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
onChange,
|
||||
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// src/progress/radial-progress.tsx
|
||||
var import_react = __toESM(require("react"));
|
||||
var RadialProgressBar = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = (0, import_react.useState)(0);
|
||||
(0, import_react.useEffect)(() => {
|
||||
const radius = 40;
|
||||
const circumference2 = 2 * Math.PI * radius;
|
||||
setCircumference(circumference2);
|
||||
}, []);
|
||||
const progressOffset = (100 - progress) / 100 * circumference;
|
||||
return /* @__PURE__ */ import_react.default.createElement("div", {
|
||||
className: "relative h-4 w-4"
|
||||
}, /* @__PURE__ */ import_react.default.createElement("svg", {
|
||||
className: "absolute top-0 left-0",
|
||||
viewBox: "0 0 100 100"
|
||||
}, /* @__PURE__ */ import_react.default.createElement("circle", {
|
||||
className: "stroke-current opacity-10",
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`
|
||||
}), /* @__PURE__ */ import_react.default.createElement("circle", {
|
||||
className: `stroke-current`,
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
strokeDashoffset: progressOffset,
|
||||
transform: "rotate(-90 50 50)"
|
||||
})));
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Button,
|
||||
Input,
|
||||
RadialProgressBar,
|
||||
TextArea
|
||||
});
|
121
packages/ui/dist/index.mjs
vendored
Normal file
121
packages/ui/dist/index.mjs
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
// src/buttons/index.tsx
|
||||
import * as React from "react";
|
||||
var Button = () => {
|
||||
return /* @__PURE__ */ React.createElement("button", null, "button");
|
||||
};
|
||||
|
||||
// src/form-fields/input.tsx
|
||||
import * as React2 from "react";
|
||||
var Input = React2.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false
|
||||
} = props;
|
||||
return /* @__PURE__ */ React2.createElement("input", {
|
||||
id,
|
||||
ref,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className: `block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary" : mode === "true-transparent" ? "rounded border-none bg-transparent ring-0" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-500/20" : ""} ${size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`
|
||||
});
|
||||
});
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
// src/form-fields/textarea.tsx
|
||||
import * as React3 from "react";
|
||||
var useAutoSizeTextArea = (textAreaRef, value) => {
|
||||
React3.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
var TextArea = React3.forwardRef(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = ""
|
||||
} = props;
|
||||
const textAreaRef = React3.useRef(ref);
|
||||
ref && useAutoSizeTextArea(textAreaRef == null ? void 0 : textAreaRef.current, value);
|
||||
return /* @__PURE__ */ React3.createElement("textarea", {
|
||||
id,
|
||||
name,
|
||||
ref: textAreaRef,
|
||||
placeholder,
|
||||
value,
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
onChange,
|
||||
className: `no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${mode === "primary" ? "rounded-md border border-custom-border-200" : mode === "transparent" ? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme" : ""} ${hasError ? "border-red-500" : ""} ${hasError && mode === "primary" ? "bg-red-100" : ""} ${className}`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// src/progress/radial-progress.tsx
|
||||
import React4, { useState, useEffect as useEffect2 } from "react";
|
||||
var RadialProgressBar = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
useEffect2(() => {
|
||||
const radius = 40;
|
||||
const circumference2 = 2 * Math.PI * radius;
|
||||
setCircumference(circumference2);
|
||||
}, []);
|
||||
const progressOffset = (100 - progress) / 100 * circumference;
|
||||
return /* @__PURE__ */ React4.createElement("div", {
|
||||
className: "relative h-4 w-4"
|
||||
}, /* @__PURE__ */ React4.createElement("svg", {
|
||||
className: "absolute top-0 left-0",
|
||||
viewBox: "0 0 100 100"
|
||||
}, /* @__PURE__ */ React4.createElement("circle", {
|
||||
className: "stroke-current opacity-10",
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`
|
||||
}), /* @__PURE__ */ React4.createElement("circle", {
|
||||
className: `stroke-current`,
|
||||
cx: "50",
|
||||
cy: "50",
|
||||
r: "40",
|
||||
strokeWidth: "12",
|
||||
fill: "none",
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
strokeDashoffset: progressOffset,
|
||||
transform: "rotate(-90 50 50)"
|
||||
})));
|
||||
};
|
||||
export {
|
||||
Button,
|
||||
Input,
|
||||
RadialProgressBar,
|
||||
TextArea
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
// import * as React from "react";
|
||||
// components
|
||||
// export * from "./breadcrumbs";
|
||||
// export * from "./button";
|
||||
// export * from "./custom-listbox";
|
||||
// export * from "./custom-menu";
|
||||
// export * from "./custom-select";
|
||||
// export * from "./empty-space";
|
||||
// export * from "./header-button";
|
||||
// export * from "./input";
|
||||
// export * from "./loader";
|
||||
// export * from "./outline-button";
|
||||
// export * from "./select";
|
||||
// export * from "./spinner";
|
||||
// export * from "./text-area";
|
||||
// export * from "./tooltip";
|
||||
export * from "./button";
|
@ -1,23 +1,33 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.tsx",
|
||||
"types": "./index.tsx",
|
||||
"name": "@plane/ui",
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint *.ts*"
|
||||
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
|
||||
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
|
||||
"lint": "eslint src/",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@types/node": "^20.5.2",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.0",
|
||||
"classnames": "^2.3.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-custom": "*",
|
||||
"next": "12.3.2",
|
||||
"react": "^18.2.0",
|
||||
"tsconfig": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"tsup": "^5.10.1",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const Button = () => {
|
||||
return <button>button</button>;
|
||||
};
|
2
packages/ui/src/form-fields/index.tsx
Normal file
2
packages/ui/src/form-fields/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./input";
|
||||
export * from "./textarea";
|
61
packages/ui/src/form-fields/input.tsx
Normal file
61
packages/ui/src/form-fields/input.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface InputProps {
|
||||
type: string;
|
||||
id: string;
|
||||
value: string;
|
||||
name: string;
|
||||
onChange: () => void;
|
||||
className?: string;
|
||||
mode?: "primary" | "transparent" | "true-transparent";
|
||||
size?: "sm" | "md" | "lg";
|
||||
hasError?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
className = "",
|
||||
mode = "primary",
|
||||
size = "md",
|
||||
hasError = false,
|
||||
placeholder = "",
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<input
|
||||
id={id}
|
||||
ref={ref}
|
||||
type={type}
|
||||
value={value}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
|
||||
: mode === "true-transparent"
|
||||
? "rounded border-none bg-transparent ring-0"
|
||||
: ""
|
||||
} ${hasError ? "border-red-500" : ""} ${
|
||||
hasError && mode === "primary" ? "bg-red-500/20" : ""
|
||||
} ${
|
||||
size === "sm" ? "px-3 py-2" : size === "lg" ? "p-3" : ""
|
||||
} ${className}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = "form-input-field";
|
||||
|
||||
export { Input };
|
80
packages/ui/src/form-fields/textarea.tsx
Normal file
80
packages/ui/src/form-fields/textarea.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface TextAreaProps {
|
||||
id: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
disabled?: boolean;
|
||||
onChange: () => void;
|
||||
mode?: "primary" | "transparent";
|
||||
hasError?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Updates the height of a <textarea> when the value changes.
|
||||
const useAutoSizeTextArea = (
|
||||
textAreaRef: HTMLTextAreaElement | null,
|
||||
value: any
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (textAreaRef) {
|
||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||
textAreaRef.style.height = "0px";
|
||||
const scrollHeight = textAreaRef.scrollHeight;
|
||||
|
||||
// We then set the height directly, outside of the render loop
|
||||
// Trying to set this with state or a ref will product an incorrect value.
|
||||
textAreaRef.style.height = scrollHeight + "px";
|
||||
}
|
||||
}, [textAreaRef, value]);
|
||||
};
|
||||
|
||||
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
placeholder = "",
|
||||
value = "",
|
||||
rows = 1,
|
||||
cols = 1,
|
||||
disabled,
|
||||
onChange,
|
||||
mode = "primary",
|
||||
hasError = false,
|
||||
className = "",
|
||||
} = props;
|
||||
|
||||
const textAreaRef = React.useRef<any>(ref);
|
||||
|
||||
ref && useAutoSizeTextArea(textAreaRef?.current, value);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
id={id}
|
||||
name={name}
|
||||
ref={textAreaRef}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
className={`no-scrollbar w-full bg-transparent placeholder-custom-text-400 px-3 py-2 outline-none ${
|
||||
mode === "primary"
|
||||
? "rounded-md border border-custom-border-200"
|
||||
: mode === "transparent"
|
||||
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-theme"
|
||||
: ""
|
||||
} ${hasError ? "border-red-500" : ""} ${
|
||||
hasError && mode === "primary" ? "bg-red-100" : ""
|
||||
} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { TextArea };
|
3
packages/ui/src/index.tsx
Normal file
3
packages/ui/src/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./buttons";
|
||||
export * from "./form-fields";
|
||||
export * from "./progress";
|
1
packages/ui/src/progress/index.tsx
Normal file
1
packages/ui/src/progress/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export * from "./radial-progress";
|
45
packages/ui/src/progress/radial-progress.tsx
Normal file
45
packages/ui/src/progress/radial-progress.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useState, useEffect, FC } from "react";
|
||||
|
||||
interface IRadialProgressBar {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export const RadialProgressBar: FC<IRadialProgressBar> = (props) => {
|
||||
const { progress } = props;
|
||||
const [circumference, setCircumference] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const radius = 40;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
setCircumference(circumference);
|
||||
}, []);
|
||||
|
||||
const progressOffset = ((100 - progress) / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="relative h-4 w-4">
|
||||
<svg className="absolute top-0 left-0" viewBox="0 0 100 100">
|
||||
<circle
|
||||
className={"stroke-current opacity-10"}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
/>
|
||||
<circle
|
||||
className={`stroke-current`}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
strokeWidth="12"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={progressOffset}
|
||||
transform="rotate(-90 50 50)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||
import { generateBarColor } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse;
|
||||
|
@ -7,19 +7,14 @@ import analyticsService from "services/analytics.service";
|
||||
import projectService from "services/project.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowPathIcon,
|
||||
CalendarDaysIcon,
|
||||
UserGroupIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon, UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
@ -46,13 +41,7 @@ type Props = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
analytics,
|
||||
params,
|
||||
fullScreen,
|
||||
isProjectLevel = false,
|
||||
user,
|
||||
}) => {
|
||||
export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScreen, isProjectLevel = false, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
@ -61,9 +50,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? PROJECT_DETAILS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
@ -72,24 +59,14 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
const { data: cycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () =>
|
||||
cyclesService.getCycleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString()
|
||||
)
|
||||
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () =>
|
||||
modulesService.getModuleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
moduleId.toString()
|
||||
)
|
||||
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
@ -178,8 +155,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const selectedProjects =
|
||||
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -236,9 +212,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
<h5 className="flex items-center gap-1">
|
||||
<p className="break-words">{truncateText(project.name, 20)}</p>
|
||||
<span className="text-custom-text-200 text-xs ml-1">
|
||||
({project.identifier})
|
||||
</span>
|
||||
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3 pl-2 w-full">
|
||||
@ -344,10 +318,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-custom-text-200">Network</h6>
|
||||
<span>
|
||||
{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ??
|
||||
""}
|
||||
</span>
|
||||
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,15 +13,11 @@ import analyticsService from "services/analytics.service";
|
||||
import projectService from "services/project.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// icons
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IAnalyticsParams, IWorkspace } from "types";
|
||||
// fetch-keys
|
||||
@ -67,9 +63,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
);
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? PROJECT_DETAILS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
@ -78,24 +72,14 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
const { data: cycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () =>
|
||||
cyclesService.getCycleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString()
|
||||
)
|
||||
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () =>
|
||||
modulesService.getModuleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
moduleId.toString()
|
||||
)
|
||||
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
@ -134,8 +118,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
eventPayload.moduleName = moduleDetails.name;
|
||||
}
|
||||
|
||||
const eventType =
|
||||
tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
||||
const eventType = tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
|
||||
|
||||
trackEventServices.trackAnalyticsEvent(
|
||||
eventPayload,
|
||||
@ -150,9 +133,9 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
|
||||
fullScreen ? "p-2 w-full" : "w-1/2"
|
||||
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
||||
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${fullScreen ? "p-2 w-full" : "w-1/2"} ${
|
||||
isOpen ? "right-0" : "-right-full"
|
||||
} duration-300 transition-all`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
||||
@ -161,8 +144,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
|
||||
<h3 className="break-words">
|
||||
Analytics for{" "}
|
||||
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
||||
Analytics for {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
@ -20,18 +20,15 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
|
||||
{
|
||||
id: "issues_closed",
|
||||
color: "rgb(var(--color-primary-100))",
|
||||
data: MONTHS_LIST.map((month) => ({
|
||||
x: month.label.substring(0, 3),
|
||||
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
||||
x: month.shortTitle,
|
||||
y:
|
||||
defaultAnalytics.issue_completed_month_wise.find(
|
||||
(data) => data.month === month.value
|
||||
)?.count || 0,
|
||||
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
|
||||
0,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
|
||||
(data) => data.count
|
||||
)}
|
||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
||||
height="300px"
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
|
@ -12,7 +12,7 @@ import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
import { ArchiveX } from "lucide-react";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
import stateService from "services/project_state.service";
|
||||
// constants
|
||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
@ -34,9 +34,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
@ -57,9 +55,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
|
||||
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
|
||||
|
||||
const selectedOption = states?.find(
|
||||
(s) => s.id === projectDetails?.default_state ?? defaultState
|
||||
);
|
||||
const selectedOption = states?.find((s) => s.id === projectDetails?.default_state ?? defaultState);
|
||||
const currentDefaultState = states?.find((s) => s.id === defaultState);
|
||||
|
||||
const initialValues: Partial<IProject> = {
|
||||
@ -105,15 +101,11 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
<div className="ml-12">
|
||||
<div className="flex flex-col rounded bg-custom-background-90 border border-custom-border-200 p-2">
|
||||
<div className="flex items-center justify-between px-5 py-4 gap-2 w-full">
|
||||
<div className="w-1/2 text-sm font-medium">
|
||||
Auto-close issues that are inactive for
|
||||
</div>
|
||||
<div className="w-1/2 text-sm font-medium">Auto-close issues that are inactive for</div>
|
||||
<div className="w-1/2">
|
||||
<CustomSelect
|
||||
value={projectDetails?.close_in}
|
||||
label={`${projectDetails?.close_in} ${
|
||||
projectDetails?.close_in === 1 ? "Month" : "Months"
|
||||
}`}
|
||||
label={`${projectDetails?.close_in} ${projectDetails?.close_in === 1 ? "Month" : "Months"}`}
|
||||
onChange={(val: number) => {
|
||||
handleChange({ close_in: val });
|
||||
}}
|
||||
@ -142,9 +134,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
<div className="w-1/2 text-sm font-medium">Auto-close Status</div>
|
||||
<div className="w-1/2 ">
|
||||
<CustomSearchSelect
|
||||
value={
|
||||
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
||||
}
|
||||
value={projectDetails?.default_state ? projectDetails?.default_state : defaultState}
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
@ -166,9 +156,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
|
||||
)}
|
||||
{selectedOption?.name
|
||||
? selectedOption.name
|
||||
: currentDefaultState?.name ?? (
|
||||
<span className="text-custom-text-200">State</span>
|
||||
)}
|
||||
: currentDefaultState?.name ?? <span className="text-custom-text-200">State</span>}
|
||||
</div>
|
||||
}
|
||||
onChange={(val: string) => {
|
||||
|
@ -10,7 +10,7 @@ import { Command } from "cmdk";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import workspaceService from "services/workspace.service";
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import inboxService from "services/inbox.service";
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
@ -79,16 +79,13 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: inboxList } = useSWR(
|
||||
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
const updateIssue = useCallback(
|
||||
@ -272,8 +269,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
>
|
||||
{issueDetails && (
|
||||
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "}
|
||||
{issueDetails.name}
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
|
||||
</div>
|
||||
)}
|
||||
{projectId && (
|
||||
@ -324,12 +320,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
</h5>
|
||||
)}
|
||||
|
||||
{!isLoading &&
|
||||
resultsCount === 0 &&
|
||||
searchTerm !== "" &&
|
||||
debouncedSearchTerm !== "" && (
|
||||
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
||||
)}
|
||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
||||
)}
|
||||
|
||||
{(isLoading || isSearching) && (
|
||||
<Command.Loading>
|
||||
@ -362,9 +355,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||
<Icon iconName={currentSection.icon} />
|
||||
<p className="block flex-1 truncate">
|
||||
{currentSection.itemName(item)}
|
||||
</p>
|
||||
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
@ -577,9 +568,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setIsPaletteOpen(false);
|
||||
redirect(
|
||||
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
|
||||
);
|
||||
redirect(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
@ -672,10 +661,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setIsPaletteOpen(false);
|
||||
window.open(
|
||||
"https://github.com/makeplane/plane/issues/new/choose",
|
||||
"_blank"
|
||||
);
|
||||
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
@ -759,29 +745,15 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
|
||||
</>
|
||||
)}
|
||||
{page === "change-issue-state" && issueDetails && (
|
||||
<ChangeIssueState
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||
)}
|
||||
{page === "change-issue-priority" && issueDetails && (
|
||||
<ChangeIssuePriority
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||
)}
|
||||
{page === "change-issue-assignee" && issueDetails && (
|
||||
<ChangeIssueAssignee
|
||||
issue={issueDetails}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
{page === "change-interface-theme" && (
|
||||
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
|
||||
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
|
||||
)}
|
||||
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</Dialog.Panel>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
@ -17,14 +17,11 @@ import { CreateUpdatePageModal } from "components/pages";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import inboxService from "services/inbox.service";
|
||||
import issuesService from "services/issue.service";
|
||||
// fetch keys
|
||||
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
export const CommandPalette: React.FC = observer(() => {
|
||||
const store: any = useMobxStore();
|
||||
@ -50,8 +47,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () =>
|
||||
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
@ -76,7 +72,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
const { key, ctrlKey, metaKey, altKey } = e;
|
||||
if (!key) return;
|
||||
|
||||
const keyPressed = key.toLowerCase();
|
||||
@ -141,11 +137,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
<>
|
||||
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
|
||||
{workspaceSlug && (
|
||||
<CreateProjectModal
|
||||
isOpen={isProjectModalOpen}
|
||||
setIsOpen={setIsProjectModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} user={user} />
|
||||
)}
|
||||
{projectId && (
|
||||
<>
|
||||
@ -184,11 +176,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
handleClose={() => setIsIssueModalOpen(false)}
|
||||
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
|
||||
prePopulateData={
|
||||
cycleId
|
||||
? { cycle: cycleId.toString() }
|
||||
: moduleId
|
||||
? { module: moduleId.toString() }
|
||||
: undefined
|
||||
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
|
||||
}
|
||||
/>
|
||||
<BulkDeleteIssuesModal
|
||||
@ -196,11 +184,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
setIsOpen={setIsBulkDeleteIssuesModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
<CommandK
|
||||
deleteIssue={deleteIssue}
|
||||
isPaletteOpen={isPaletteOpen}
|
||||
setIsPaletteOpen={setIsPaletteOpen}
|
||||
/>
|
||||
<CommandK deleteIssue={deleteIssue} isPaletteOpen={isPaletteOpen} setIsPaletteOpen={setIsPaletteOpen} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ import { mutate } from "swr";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
// hooks
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
// constants
|
||||
|
@ -7,7 +7,7 @@ import { mutate } from "swr";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
|
||||
// constants
|
||||
@ -64,11 +64,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
|
||||
return (
|
||||
<>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<Command.Item
|
||||
key={priority}
|
||||
onSelect={() => handleIssueState(priority)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<PriorityIcon priority={priority} />
|
||||
<span className="capitalize">{priority ?? "None"}</span>
|
||||
|
@ -7,8 +7,8 @@ import useSWR, { mutate } from "swr";
|
||||
// cmdk
|
||||
import { Command } from "cmdk";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import stateService from "services/project_state.service";
|
||||
// ui
|
||||
import { Spinner } from "components/ui";
|
||||
// icons
|
||||
@ -32,9 +32,7 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
||||
|
||||
const { data: stateGroups, mutate: mutateIssueDetails } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
@ -78,18 +76,9 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
|
||||
{states ? (
|
||||
states.length > 0 ? (
|
||||
states.map((state) => (
|
||||
<Command.Item
|
||||
key={state.id}
|
||||
onSelect={() => handleIssueState(state.id)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon
|
||||
stateGroup={state.group}
|
||||
color={state.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issue.service";
|
||||
// icons
|
||||
import { Icon, Tooltip } from "components/ui";
|
||||
import { CopyPlus } from "lucide-react";
|
||||
@ -10,26 +14,22 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
|
||||
}
|
||||
>
|
||||
<Tooltip tooltipContent={activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"}>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${activity.project}/issues/${activity.issue}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{activity.issue_detail
|
||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
||||
: "Issue"}
|
||||
{activity.issue_detail ? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}` : "Issue"}
|
||||
<Icon iconName="launch" className="!text-xs" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
@ -52,13 +52,29 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (
|
||||
activity: IIssueActivity,
|
||||
showIssue: boolean,
|
||||
workspaceSlug: string
|
||||
) => React.ReactNode;
|
||||
message: (activity: IIssueActivity, showIssue: boolean, workspaceSlug: string) => React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
} = {
|
||||
@ -151,8 +167,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the blocking issue{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
removed the blocking issue <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@ -208,8 +223,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
removed the relation from{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
removed the relation from <span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
@ -298,8 +312,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the estimate point to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
set the estimate point to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@ -325,14 +338,8 @@ const activityDetails: {
|
||||
return (
|
||||
<>
|
||||
added a new label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<LabelPill labelId={activity.new_identifier ?? ""} />
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
@ -348,13 +355,7 @@ const activityDetails: {
|
||||
<>
|
||||
removed the label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<LabelPill labelId={activity.old_identifier ?? ""} />
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
@ -509,8 +510,7 @@ const activityDetails: {
|
||||
if (!activity.new_value)
|
||||
return (
|
||||
<>
|
||||
removed the parent{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
removed the parent <span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@ -523,8 +523,7 @@ const activityDetails: {
|
||||
else
|
||||
return (
|
||||
<>
|
||||
set the parent to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
set the parent to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@ -592,8 +591,7 @@ const activityDetails: {
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the state to{" "}
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
set the state to <span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
@ -645,13 +643,7 @@ export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
|
||||
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
|
||||
);
|
||||
|
||||
export const ActivityMessage = ({
|
||||
activity,
|
||||
showIssue = false,
|
||||
}: {
|
||||
activity: IIssueActivity;
|
||||
showIssue?: boolean;
|
||||
}) => {
|
||||
export const ActivityMessage = ({ activity, showIssue = false }: { activity: IIssueActivity; showIssue?: boolean }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// react-datepicker
|
||||
import DatePicker from "react-datepicker";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// components
|
||||
import { DateFilterSelect } from "./date-filter-select";
|
||||
// ui
|
||||
@ -14,15 +11,12 @@ import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
// helpers
|
||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { IIssueFilterOptions } from "types";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
field: keyof IIssueFilterOptions;
|
||||
filters: IIssueFilterOptions;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
onSelect: (option: any) => void;
|
||||
onSelect: (val: string[]) => void;
|
||||
};
|
||||
|
||||
type TFormValues = {
|
||||
@ -37,14 +31,7 @@ const defaultValues: TFormValues = {
|
||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||
};
|
||||
|
||||
export const DateFilterModal: React.FC<Props> = ({
|
||||
title,
|
||||
field,
|
||||
filters,
|
||||
handleClose,
|
||||
isOpen,
|
||||
onSelect,
|
||||
}) => {
|
||||
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
|
||||
const { handleSubmit, watch, control } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
@ -52,32 +39,13 @@ export const DateFilterModal: React.FC<Props> = ({
|
||||
const handleFormSubmit = (formData: TFormValues) => {
|
||||
const { filterType, date1, date2 } = formData;
|
||||
|
||||
if (filterType === "range") {
|
||||
onSelect({
|
||||
key: field,
|
||||
value: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`],
|
||||
});
|
||||
} else {
|
||||
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
|
||||
if (item?.includes(filterType)) return false;
|
||||
if (filterType === "range") onSelect([`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`]);
|
||||
else onSelect([`${renderDateFormat(date1)};${filterType}`]);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||
if (filterOne)
|
||||
onSelect({ key: field, value: [filterOne, `${renderDateFormat(date1)};${filterType}`] });
|
||||
else
|
||||
onSelect({
|
||||
key: field,
|
||||
value: [`${renderDateFormat(date1)};${filterType}`],
|
||||
});
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const isInvalid =
|
||||
watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
||||
const isInvalid = watch("filterType") === "range" ? new Date(watch("date1")) > new Date(watch("date2")) : false;
|
||||
|
||||
const nextDay = new Date(watch("date1"));
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
@ -117,10 +85,7 @@ export const DateFilterModal: React.FC<Props> = ({
|
||||
<DateFilterSelect title={title} value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
<XMarkIcon
|
||||
className="border-base h-4 w-4 cursor-pointer"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<XMarkIcon className="border-base h-4 w-4 cursor-pointer" onClick={handleClose} />
|
||||
</div>
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<Controller
|
||||
@ -165,11 +130,7 @@ export const DateFilterModal: React.FC<Props> = ({
|
||||
<SecondaryButton className="flex items-center gap-2" onClick={handleClose}>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
className="flex items-center gap-2"
|
||||
disabled={isInvalid}
|
||||
>
|
||||
<PrimaryButton type="submit" className="flex items-center gap-2" disabled={isInvalid}>
|
||||
Apply
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
@ -25,11 +25,11 @@ import {
|
||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
||||
// types
|
||||
import { Properties, TIssueViewOptions } from "types";
|
||||
import { Properties, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { GROUP_BY_OPTIONS, ORDER_BY_OPTIONS, FILTER_ISSUE_OPTIONS } from "constants/issue";
|
||||
import { ISSUE_GROUP_BY_OPTIONS, ISSUE_ORDER_BY_OPTIONS, ISSUE_FILTER_OPTIONS } from "constants/issue";
|
||||
|
||||
const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
const issueViewOptions: { type: TIssueLayouts; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: FormatListBulletedOutlined,
|
||||
@ -52,7 +52,7 @@ const issueViewOptions: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const issueViewForDraftIssues: { type: TIssueViewOptions; Icon: any }[] = [
|
||||
const issueViewForDraftIssues: { type: TIssueLayouts; Icon: any }[] = [
|
||||
{
|
||||
type: "list",
|
||||
Icon: FormatListBulletedOutlined,
|
||||
@ -69,19 +69,10 @@ export const IssuesFilterView: React.FC = () => {
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname.includes("draft-issues");
|
||||
|
||||
const {
|
||||
displayFilters,
|
||||
setDisplayFilters,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
setNewFilterDefaultView,
|
||||
} = useIssuesView();
|
||||
const { displayFilters, setDisplayFilters, filters, setFilters, resetFilterToDefault, setNewFilterDefaultView } =
|
||||
useIssuesView();
|
||||
|
||||
const [properties, setProperties] = useIssuesProperties(
|
||||
workspaceSlug as string,
|
||||
projectId as string
|
||||
);
|
||||
const [properties, setProperties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { isEstimateActive } = useEstimateOption();
|
||||
|
||||
@ -92,9 +83,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
{issueViewOptions.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
}
|
||||
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
@ -122,9 +111,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
{issueViewForDraftIssues.map((option) => (
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
}
|
||||
tooltipContent={<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>}
|
||||
position="bottom"
|
||||
>
|
||||
<button
|
||||
@ -164,9 +151,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
if (valueExists)
|
||||
setFilters(
|
||||
{
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter(
|
||||
(val) => val !== option.value
|
||||
),
|
||||
[option.key]: ((filters[key] ?? []) as any[])?.filter((val) => val !== option.value),
|
||||
},
|
||||
!Boolean(viewId)
|
||||
);
|
||||
@ -187,9 +172,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`group flex items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1.5 text-xs hover:bg-custom-sidebar-background-90 hover:text-custom-sidebar-text-100 focus:outline-none duration-300 ${
|
||||
open
|
||||
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
|
||||
: "text-custom-sidebar-text-200"
|
||||
open ? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100" : "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
>
|
||||
Display
|
||||
@ -216,24 +199,24 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.group_by
|
||||
)?.name ?? "Select"
|
||||
ISSUE_GROUP_BY_OPTIONS.find((option) => option.key === displayFilters.group_by)
|
||||
?.title ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{GROUP_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters.layout === "kanban" && option.key === null)
|
||||
return null;
|
||||
{ISSUE_GROUP_BY_OPTIONS.map((option) => {
|
||||
if (displayFilters.layout === "kanban" && option.key === null) return null;
|
||||
if (option.key === "project") return null;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setDisplayFilters({ group_by: option.key })}
|
||||
onClick={() => {
|
||||
// setDisplayFilters({ group_by: option.key })
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
@ -241,79 +224,71 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.order_by
|
||||
)?.name ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ORDER_BY_OPTIONS.map((option) =>
|
||||
displayFilters.group_by === "priority" &&
|
||||
option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setDisplayFilters({ order_by: option.key });
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Order by</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
ISSUE_ORDER_BY_OPTIONS.find((option) => option.key === displayFilters.order_by)?.title ??
|
||||
"Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{ISSUE_ORDER_BY_OPTIONS.map((option) =>
|
||||
displayFilters.group_by === "priority" && option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
// setDisplayFilters({ order_by: option.key });
|
||||
}}
|
||||
>
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Issue type</h4>
|
||||
<div className="w-28">
|
||||
<CustomMenu
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find(
|
||||
(option) => option.key === displayFilters.type
|
||||
)?.name ?? "Select"
|
||||
ISSUE_FILTER_OPTIONS.find((option) => option.key === displayFilters.type)?.title ?? "Select"
|
||||
}
|
||||
className="!w-full"
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||
{ISSUE_FILTER_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() =>
|
||||
setDisplayFilters({
|
||||
type: option.key,
|
||||
})
|
||||
}
|
||||
onClick={() => {
|
||||
// setDisplayFilters({
|
||||
// type: option.key,
|
||||
// })
|
||||
}}
|
||||
>
|
||||
{option.name}
|
||||
{option.title}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.sub_issue ?? true}
|
||||
onChange={() =>
|
||||
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{displayFilters.layout !== "calendar" && displayFilters.layout !== "spreadsheet" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show sub-issues</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.sub_issue ?? true}
|
||||
onChange={() => setDisplayFilters({ sub_issue: !displayFilters.sub_issue })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{displayFilters.layout !== "calendar" &&
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
@ -358,16 +333,11 @@ export const IssuesFilterView: React.FC = () => {
|
||||
|
||||
if (
|
||||
displayFilters.layout === "spreadsheet" &&
|
||||
(key === "attachment_count" ||
|
||||
key === "link" ||
|
||||
key === "sub_issue_count")
|
||||
(key === "attachment_count" || key === "link" || key === "sub_issue_count")
|
||||
)
|
||||
return null;
|
||||
|
||||
if (
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
(key === "created_on" || key === "updated_on")
|
||||
)
|
||||
if (displayFilters.layout !== "spreadsheet" && (key === "created_on" || key === "updated_on"))
|
||||
return null;
|
||||
|
||||
return (
|
||||
|
@ -9,11 +9,10 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesServices from "services/issues.service";
|
||||
import issuesServices from "services/issue.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
// ui
|
||||
import { DangerButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
@ -49,17 +48,12 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||
workspaceSlug && projectId ? () => issuesServices.getIssues(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { order_by, group_by, ...viewGanttParams } = params;
|
||||
|
||||
const {
|
||||
@ -94,14 +88,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
|
||||
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
|
||||
|
||||
const calendarFetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
|
||||
|
||||
const ganttFetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
|
||||
: moduleId
|
||||
@ -126,8 +112,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
message: "Issues deleted successfully!",
|
||||
});
|
||||
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||
else {
|
||||
if (cycleId) {
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));
|
||||
@ -155,9 +140,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
|
||||
: issues?.filter(
|
||||
(issue) =>
|
||||
issue.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
`${issue.project_detail.identifier}-${issue.sequence_id}`
|
||||
.toLowerCase()
|
||||
.includes(query.toLowerCase())
|
||||
`${issue.project_detail.identifier}-${issue.sequence_id}`.toLowerCase().includes(query.toLowerCase())
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
import aiService from "services/ai.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
@ -110,9 +110,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
error ||
|
||||
"You have reached the maximum number of requests of 50 requests per month per user.",
|
||||
message: error || "You have reached the maximum number of requests of 50 requests per month per user.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
@ -166,8 +164,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
)}
|
||||
{invalidResponse && (
|
||||
<div className="text-sm text-red-500">
|
||||
No response could be generated. This may be due to insufficient content or task
|
||||
information. Please try again.
|
||||
No response could be generated. This may be due to insufficient content or task information. Please try again.
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
@ -175,9 +172,7 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
name="task"
|
||||
register={register}
|
||||
placeholder={`${
|
||||
content && content !== ""
|
||||
? "Tell AI what action to perform on this content..."
|
||||
: "Ask AI anything..."
|
||||
content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..."
|
||||
}`}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@ -187,18 +182,8 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
onClick={() => {
|
||||
onResponse(response);
|
||||
onClose();
|
||||
if (block)
|
||||
trackEventServices.trackUseGPTResponseEvent(
|
||||
block,
|
||||
"USE_GPT_RESPONSE_IN_PAGE_BLOCK",
|
||||
user
|
||||
);
|
||||
else if (issue)
|
||||
trackEventServices.trackUseGPTResponseEvent(
|
||||
issue,
|
||||
"USE_GPT_RESPONSE_IN_ISSUE",
|
||||
user
|
||||
);
|
||||
if (block) trackEventServices.trackUseGPTResponseEvent(block, "USE_GPT_RESPONSE_IN_PAGE_BLOCK", user);
|
||||
else if (issue) trackEventServices.trackUseGPTResponseEvent(issue, "USE_GPT_RESPONSE_IN_ISSUE", user);
|
||||
}}
|
||||
>
|
||||
Use this response
|
||||
@ -206,16 +191,8 @@ export const GptAssistantModal: React.FC<Props> = ({
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton onClick={onClose}>Close</SecondaryButton>
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
onClick={handleSubmit(handleResponse)}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? "Generating response..."
|
||||
: response === ""
|
||||
? "Generate response"
|
||||
: "Generate again"}
|
||||
<PrimaryButton type="button" onClick={handleSubmit(handleResponse)} loading={isSubmitting}>
|
||||
{isSubmitting ? "Generating response..." : response === "" ? "Generate response" : "Generate again"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,224 +1,66 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
AllLists,
|
||||
AllBoards,
|
||||
CalendarView,
|
||||
SpreadsheetView,
|
||||
GanttChartView,
|
||||
} from "components/core";
|
||||
// ui
|
||||
import { EmptyState, Spinner } from "components/ui";
|
||||
// icons
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
// images
|
||||
import emptyIssue from "public/empty-state/issue.svg";
|
||||
import emptyIssueArchive from "public/empty-state/issue-archive.svg";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IIssue, IIssueViewProps } from "types";
|
||||
// fetch-keys
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
AppliedFiltersRoot,
|
||||
ListLayout,
|
||||
CalendarLayout,
|
||||
GanttLayout,
|
||||
KanBanLayout,
|
||||
SpreadsheetLayout,
|
||||
} from "components/issues";
|
||||
|
||||
type Props = {
|
||||
addIssueToDate: (date: string) => void;
|
||||
addIssueToGroup: (groupTitle: string) => void;
|
||||
disableUserActions: boolean;
|
||||
dragDisabled?: boolean;
|
||||
emptyState: {
|
||||
title: string;
|
||||
description?: string;
|
||||
primaryButton?: {
|
||||
icon: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
};
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
handleDraftIssueAction?: (issue: IIssue, action: "edit" | "delete") => void;
|
||||
handleOnDragEnd: (result: DropResult) => Promise<void>;
|
||||
openIssuesListModal: (() => void) | null;
|
||||
removeIssue: ((bridgeId: string, issueId: string) => void) | null;
|
||||
disableAddIssueOption?: boolean;
|
||||
trashBox: boolean;
|
||||
setTrashBox: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const AllViews: React.FC<Props> = ({
|
||||
addIssueToDate,
|
||||
addIssueToGroup,
|
||||
disableUserActions,
|
||||
dragDisabled = false,
|
||||
emptyState,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleOnDragEnd,
|
||||
openIssuesListModal,
|
||||
removeIssue,
|
||||
disableAddIssueOption = false,
|
||||
trashBox,
|
||||
setTrashBox,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const AllViews: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const [myIssueProjectId, setMyIssueProjectId] = useState<string | null>(null);
|
||||
|
||||
const { user } = useUser();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
const handleMyIssueOpen = (issue: IIssue) => {
|
||||
setMyIssueProjectId(issue.project);
|
||||
const { workspaceSlug, projectId } = router.query as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycleId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
|
||||
const handleTrashBox = useCallback(
|
||||
(isDragging: boolean) => {
|
||||
if (isDragging && !trashBox) setTrashBox(true);
|
||||
const { issue: issueStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ISSUES` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||
|
||||
await projectStore.fetchProjectStates(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
||||
await projectStore.fetchProjectEstimates(workspaceSlug, projectId);
|
||||
|
||||
await issueStore.fetchIssues(workspaceSlug, projectId);
|
||||
}
|
||||
},
|
||||
[trashBox, setTrashBox]
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<StrictModeDroppable droppableId="trashBox">
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={`${
|
||||
trashBox ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
} fixed top-4 left-1/2 -translate-x-1/2 z-40 w-72 flex items-center justify-center gap-2 rounded border-2 border-red-500/20 bg-custom-background-100 px-3 py-5 text-xs font-medium italic text-red-500 ${
|
||||
snapshot.isDraggingOver ? "bg-red-500 blur-2xl opacity-70" : ""
|
||||
} transition duration-300`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
Drop here to delete the issue.
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
{groupedIssues ? (
|
||||
!isEmpty ||
|
||||
displayFilters?.layout === "kanban" ||
|
||||
displayFilters?.layout === "calendar" ||
|
||||
displayFilters?.layout === "gantt_chart" ? (
|
||||
<>
|
||||
{displayFilters?.layout === "list" ? (
|
||||
<AllLists
|
||||
states={states}
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
removeIssue={removeIssue}
|
||||
myIssueProjectId={myIssueProjectId}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
disableUserActions={disableUserActions}
|
||||
disableAddIssueOption={disableAddIssueOption}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : displayFilters?.layout === "kanban" ? (
|
||||
<AllBoards
|
||||
addIssueToGroup={addIssueToGroup}
|
||||
disableUserActions={disableUserActions}
|
||||
disableAddIssueOption={disableAddIssueOption}
|
||||
dragDisabled={dragDisabled}
|
||||
handleIssueAction={handleIssueAction}
|
||||
handleDraftIssueAction={handleDraftIssueAction}
|
||||
handleTrashBox={handleTrashBox}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
myIssueProjectId={myIssueProjectId}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={removeIssue}
|
||||
states={states}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
viewProps={viewProps}
|
||||
/>
|
||||
) : displayFilters?.layout === "calendar" ? (
|
||||
<CalendarView
|
||||
handleIssueAction={handleIssueAction}
|
||||
addIssueToDate={addIssueToDate}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : displayFilters?.layout === "spreadsheet" ? (
|
||||
<SpreadsheetView
|
||||
handleIssueAction={handleIssueAction}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : (
|
||||
displayFilters?.layout === "gantt_chart" && (
|
||||
<GanttChartView disableUserActions={disableUserActions} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : router.pathname.includes("archived-issues") ? (
|
||||
<EmptyState
|
||||
title="Archived Issues will be shown here"
|
||||
description="All the issues that have been in the completed or canceled groups for the configured period of time can be viewed here."
|
||||
image={emptyIssueArchive}
|
||||
primaryButton={{
|
||||
text: "Go to Automation Settings",
|
||||
onClick: () => {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
title={emptyState.title}
|
||||
description={emptyState.description}
|
||||
image={emptyIssue}
|
||||
primaryButton={
|
||||
emptyState.primaryButton
|
||||
? {
|
||||
icon: emptyState.primaryButton.icon,
|
||||
text: emptyState.primaryButton.text,
|
||||
onClick: emptyState.primaryButton.onClick,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
secondaryButton={emptyState.secondaryButton}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</DragDropContext>
|
||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||
<AppliedFiltersRoot />
|
||||
<div className="w-full h-full">
|
||||
{activeLayout === "list" ? (
|
||||
<ListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<KanBanLayout />
|
||||
) : activeLayout === "calendar" ? (
|
||||
<CalendarLayout />
|
||||
) : activeLayout === "gantt_chart" ? (
|
||||
<GanttLayout />
|
||||
) : activeLayout === "spreadsheet" ? (
|
||||
<SpreadsheetLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -68,23 +68,18 @@ export const AllBoards: React.FC<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
handleMutation={() =>
|
||||
isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues()
|
||||
}
|
||||
handleMutation={() => (isMyIssue ? mutateMyIssues() : isProfileIssue ? mutateProfileIssues() : mutateIssues())}
|
||||
projectId={myIssueProjectId ? myIssueProjectId : projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={disableUserActions}
|
||||
/>
|
||||
{groupedIssues ? (
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-5 bg-custom-background-90">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
|
||||
return null;
|
||||
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0) return null;
|
||||
|
||||
return (
|
||||
<SingleBoard
|
||||
@ -113,15 +108,13 @@ export const AllBoards: React.FC<Props> = ({
|
||||
<div className="space-y-3">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
displayFilters?.group_by === "state"
|
||||
? states?.find((s) => s.id === singleGroup)
|
||||
: null;
|
||||
displayFilters?.group_by === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
||||
if (groupedIssues[singleGroup].length === 0)
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
|
||||
className="flex items-center justify-between gap-2 rounded bg-custom-background-100 p-2 shadow-custom-shadow-2xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentState && (
|
||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
@ -50,8 +50,6 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
@ -106,12 +104,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
icon = currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
|
||||
);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
@ -138,14 +131,8 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
);
|
||||
const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = <span className="h-3.5 w-3.5 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
|
||||
break;
|
||||
case "assignees":
|
||||
case "created_by":
|
||||
@ -196,10 +183,7 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<Icon
|
||||
iconName="close_fullscreen"
|
||||
className="text-base font-medium text-custom-text-900"
|
||||
/>
|
||||
<Icon iconName="close_fullscreen" className="text-base font-medium text-custom-text-900" />
|
||||
) : (
|
||||
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
|
||||
)}
|
||||
|
@ -5,27 +5,17 @@ import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import {
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
DraggingStyle,
|
||||
NotDraggingStyle,
|
||||
} from "react-beautiful-dnd";
|
||||
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
@ -41,10 +31,18 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { handleIssuesMutation } from "helpers/issue.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
@ -147,22 +145,17 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutateIssues();
|
||||
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
|
||||
mutateIssues();
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
},
|
||||
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
|
||||
);
|
||||
|
||||
const getStyle = (
|
||||
style: DraggingStyle | NotDraggingStyle | undefined,
|
||||
snapshot: DraggableStateSnapshot
|
||||
) => {
|
||||
const getStyle = (style: DraggingStyle | NotDraggingStyle | undefined, snapshot: DraggableStateSnapshot) => {
|
||||
if (displayFilters?.order_by === "sort_order") return style;
|
||||
if (!snapshot.isDragging) return {};
|
||||
if (!snapshot.isDropAnimating) return style;
|
||||
@ -174,11 +167,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
@ -188,6 +178,86 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||
}, [snapshot, handleTrashBox]);
|
||||
@ -253,9 +323,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
|
||||
</a>
|
||||
)}
|
||||
</ContextMenu>
|
||||
@ -277,9 +345,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
{!isNotAllowed && (
|
||||
<div
|
||||
ref={actionSectionRef}
|
||||
className={`z-1 absolute top-1.5 right-1.5 hidden group-hover/card:!flex ${
|
||||
isMenuActive ? "!flex" : ""
|
||||
}`}
|
||||
className={`z-1 absolute top-1.5 right-1.5 hidden group-hover/card:!flex ${isMenuActive ? "!flex" : ""}`}
|
||||
>
|
||||
{type && !isNotAllowed && (
|
||||
<CustomMenu
|
||||
@ -343,37 +409,30 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-left break-words line-clamp-2"
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else openPeekOverview();
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex items-center gap-2 text-xs ${
|
||||
isDropdownActive ? "" : "overflow-x-scroll"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center gap-2 text-xs ${isDropdownActive ? "" : "overflow-x-scroll"}`}>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -397,16 +456,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
customButton
|
||||
user={user}
|
||||
selfPositioned
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -7,9 +7,7 @@ import { mutate } from "swr";
|
||||
// react-beautiful-dnd
|
||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import issuesService from "services/issue.service";
|
||||
// components
|
||||
import { SingleCalendarDate, CalendarHeader } from "components/core";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
@ -17,13 +15,7 @@ import { IssuePeekOverview } from "components/issues";
|
||||
import { Spinner } from "components/ui";
|
||||
// helpers
|
||||
import { renderDateFormat } from "helpers/date-time.helper";
|
||||
import {
|
||||
startOfWeek,
|
||||
lastDayOfWeek,
|
||||
eachDayOfInterval,
|
||||
weekDayInterval,
|
||||
formatDate,
|
||||
} from "helpers/calendar.helper";
|
||||
import { startOfWeek, lastDayOfWeek, eachDayOfInterval, weekDayInterval, formatDate } from "helpers/calendar.helper";
|
||||
// types
|
||||
import { ICalendarRange, ICurrentUserResponse, IIssue, UserAuth } from "types";
|
||||
// fetch-keys
|
||||
@ -61,8 +53,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
|
||||
useCalendarIssuesView();
|
||||
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } = useCalendarIssuesView();
|
||||
|
||||
const totalDate = eachDayOfInterval({
|
||||
start: calendarDates.startDate,
|
||||
@ -80,8 +71,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
const filterIssue =
|
||||
calendarIssues.length > 0
|
||||
? calendarIssues.filter(
|
||||
(issue) =>
|
||||
issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
|
||||
(issue) => issue.target_date && renderDateFormat(issue.target_date) === renderDateFormat(date)
|
||||
)
|
||||
: [];
|
||||
return {
|
||||
@ -155,18 +145,16 @@ export const CalendarView: React.FC<Props> = ({
|
||||
});
|
||||
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
|
||||
endDate
|
||||
)};before`,
|
||||
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayFilters || displayFilters.calendar_date_range === "")
|
||||
setDisplayFilters({
|
||||
calendar_date_range: `${renderDateFormat(
|
||||
startOfWeek(currentDate)
|
||||
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
|
||||
calendar_date_range: `${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
|
||||
lastDayOfWeek(currentDate)
|
||||
)};before`,
|
||||
});
|
||||
}, [currentDate, displayFilters, setDisplayFilters]);
|
||||
|
||||
@ -214,11 +202,7 @@ export const CalendarView: React.FC<Props> = ({
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
{isMonthlyView
|
||||
? formatDate(date, "eee").substring(0, 3)
|
||||
: formatDate(date, "eee")}
|
||||
</span>
|
||||
<span>{isMonthlyView ? formatDate(date, "eee").substring(0, 3) : formatDate(date, "eee")}</span>
|
||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
||||
</div>
|
||||
))}
|
||||
|
@ -7,29 +7,23 @@ import { mutate } from "swr";
|
||||
// react-beautiful-dnd
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewLabelSelect,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// icons
|
||||
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helper
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// type
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
@ -65,7 +59,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { params } = useCalendarIssuesView();
|
||||
const params = {};
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
@ -122,13 +116,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id as string, formData, user)
|
||||
.then(() => {
|
||||
mutate(fetchKey);
|
||||
})
|
||||
@ -140,11 +128,8 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
@ -153,9 +138,87 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const displayProperties = properties
|
||||
? Object.values(properties).some((value) => value === true)
|
||||
: false;
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const displayProperties = properties ? Object.values(properties).some((value) => value === true) : false;
|
||||
|
||||
const openPeekOverview = () => {
|
||||
const { query } = router;
|
||||
@ -225,22 +288,19 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{displayProperties && (
|
||||
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -260,21 +320,23 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@ -7,10 +7,10 @@ import useSWR, { mutate } from "swr";
|
||||
// react-beautiful-dnd
|
||||
import { DropResult } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import stateService from "services/state.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import stateService from "services/project_state.service";
|
||||
import modulesService from "services/modules.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
@ -51,10 +51,7 @@ type Props = {
|
||||
disableUserActions?: boolean;
|
||||
};
|
||||
|
||||
export const IssuesView: React.FC<Props> = ({
|
||||
openIssuesListModal,
|
||||
disableUserActions = false,
|
||||
}) => {
|
||||
export const IssuesView: React.FC<Props> = ({ openIssuesListModal, disableUserActions = false }) => {
|
||||
// create issue modal
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [createViewModal, setCreateViewModal] = useState<any>(null);
|
||||
@ -64,9 +61,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
// update issue modal
|
||||
const [editIssueModal, setEditIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<
|
||||
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
||||
>(undefined);
|
||||
const [issueToEdit, setIssueToEdit] = useState<(IIssue & { actionType: "edit" | "delete" }) | undefined>(undefined);
|
||||
|
||||
// delete issue modal
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
@ -87,15 +82,13 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
|
||||
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params, setDisplayFilters } =
|
||||
useIssuesView();
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
@ -108,6 +101,17 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDraftIssues) return;
|
||||
|
||||
if (
|
||||
displayFilters.layout === "calendar" ||
|
||||
displayFilters.layout === "gantt_chart" ||
|
||||
displayFilters.layout === "spreadsheet"
|
||||
)
|
||||
setDisplayFilters({ layout: "list" });
|
||||
}, [isDraftIssues, displayFilters, setDisplayFilters]);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
@ -141,12 +145,10 @@ export const IssuesView: React.FC<Props> = ({
|
||||
// check if dropping in the same group
|
||||
if (source.droppableId === destination.droppableId) {
|
||||
// check if dropping at beginning
|
||||
if (destination.index === 0)
|
||||
newSortOrder = destinationGroupArray[0].sort_order - 10000;
|
||||
if (destination.index === 0) newSortOrder = destinationGroupArray[0].sort_order - 10000;
|
||||
// check if dropping at last
|
||||
else if (destination.index === destinationGroupArray.length - 1)
|
||||
newSortOrder =
|
||||
destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
|
||||
newSortOrder = destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
|
||||
else {
|
||||
if (destination.index > source.index)
|
||||
newSortOrder =
|
||||
@ -161,12 +163,10 @@ export const IssuesView: React.FC<Props> = ({
|
||||
}
|
||||
} else {
|
||||
// check if dropping at beginning
|
||||
if (destination.index === 0)
|
||||
newSortOrder = destinationGroupArray[0].sort_order - 10000;
|
||||
if (destination.index === 0) newSortOrder = destinationGroupArray[0].sort_order - 10000;
|
||||
// check if dropping at last
|
||||
else if (destination.index === destinationGroupArray.length)
|
||||
newSortOrder =
|
||||
destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
|
||||
newSortOrder = destinationGroupArray[destinationGroupArray.length - 1].sort_order + 10000;
|
||||
else
|
||||
newSortOrder =
|
||||
(destinationGroupArray[destination.index - 1].sort_order +
|
||||
@ -180,18 +180,14 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const destinationGroup = destination.droppableId; // destination group id
|
||||
|
||||
if (
|
||||
displayFilters.order_by === "sort_order" ||
|
||||
source.droppableId !== destination.droppableId
|
||||
) {
|
||||
if (displayFilters.order_by === "sort_order" || source.droppableId !== destination.droppableId) {
|
||||
// different group/column;
|
||||
|
||||
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
|
||||
// if the issue is moved to a different group, then we will change the group of the
|
||||
// dragged item(or issue)
|
||||
|
||||
if (displayFilters.group_by === "priority")
|
||||
draggedItem.priority = destinationGroup as TIssuePriorities;
|
||||
if (displayFilters.group_by === "priority") draggedItem.priority = destinationGroup as TIssuePriorities;
|
||||
else if (displayFilters.group_by === "state") {
|
||||
draggedItem.state = destinationGroup;
|
||||
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
|
||||
@ -219,14 +215,8 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
[sourceGroup]: orderArrayBy(
|
||||
sourceGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
[destinationGroup]: orderArrayBy(
|
||||
destinationGroupArray,
|
||||
displayFilters.order_by ?? "-created_at"
|
||||
),
|
||||
[sourceGroup]: orderArrayBy(sourceGroupArray, displayFilters.order_by ?? "-created_at"),
|
||||
[destinationGroup]: orderArrayBy(destinationGroupArray, displayFilters.order_by ?? "-created_at"),
|
||||
};
|
||||
},
|
||||
false
|
||||
@ -246,14 +236,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
user
|
||||
)
|
||||
.then((response) => {
|
||||
const sourceStateBeforeDrag = states?.find(
|
||||
(state) => state.name === source.droppableId
|
||||
);
|
||||
const sourceStateBeforeDrag = states?.find((state) => state.name === source.droppableId);
|
||||
|
||||
if (
|
||||
sourceStateBeforeDrag?.group !== "completed" &&
|
||||
response?.state_detail?.group === "completed"
|
||||
)
|
||||
if (sourceStateBeforeDrag?.group !== "completed" && response?.state_detail?.group === "completed")
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
@ -387,12 +372,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
issuesService
|
||||
.removeIssueFromCycle(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
cycleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId as string, bridgeId)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
@ -430,12 +410,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
);
|
||||
|
||||
modulesService
|
||||
.removeIssueFromModule(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
moduleId as string,
|
||||
bridgeId
|
||||
)
|
||||
.removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId as string, bridgeId)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
@ -450,12 +425,9 @@ export const IssuesView: React.FC<Props> = ({
|
||||
[displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
|
||||
);
|
||||
|
||||
const nullFilters = Object.keys(filters).filter(
|
||||
(key) => filters[key as keyof IIssueFilterOptions] === null
|
||||
);
|
||||
const nullFilters = Object.keys(filters).filter((key) => filters[key as keyof IIssueFilterOptions] === null);
|
||||
|
||||
const areFiltersApplied =
|
||||
Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
|
||||
const areFiltersApplied = Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -518,6 +490,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
labels: null,
|
||||
priority: null,
|
||||
state: null,
|
||||
state_group: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
})
|
||||
@ -581,10 +554,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
: undefined,
|
||||
secondaryButton:
|
||||
cycleId || moduleId ? (
|
||||
<SecondaryButton
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={openIssuesListModal ?? (() => {})}
|
||||
>
|
||||
<SecondaryButton className="flex items-center gap-1.5" onClick={openIssuesListModal ?? (() => {})}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add an existing issue
|
||||
</SecondaryButton>
|
||||
|
@ -5,20 +5,14 @@ import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// icons
|
||||
@ -34,23 +28,20 @@ import {
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { handleIssuesMutation } from "helpers/issue.helper";
|
||||
// types
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
IUserProfileProjectSegregation,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
SUB_ISSUES,
|
||||
USER_PROFILE_PROJECT_SEGREGATION,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES, USER_PROFILE_PROJECT_SEGREGATION } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
type?: string;
|
||||
@ -140,39 +131,24 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
issuesService
|
||||
.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user)
|
||||
.then(() => {
|
||||
mutateIssues();
|
||||
issuesService.patchIssue(workspaceSlug as string, issue.project, issue.id, formData, user).then(() => {
|
||||
mutateIssues();
|
||||
|
||||
if (userId)
|
||||
mutate<IUserProfileProjectSegregation>(
|
||||
USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
);
|
||||
if (userId)
|
||||
mutate<IUserProfileProjectSegregation>(
|
||||
USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug.toString(), userId.toString())
|
||||
);
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
});
|
||||
},
|
||||
[
|
||||
displayFilters,
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
userId,
|
||||
groupTitle,
|
||||
index,
|
||||
mutateIssues,
|
||||
user,
|
||||
]
|
||||
[displayFilters, workspaceSlug, cycleId, moduleId, userId, groupTitle, index, mutateIssues, user]
|
||||
);
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
@ -181,6 +157,86 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const issuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||
: isDraftIssues
|
||||
@ -197,8 +253,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const isNotAllowed =
|
||||
userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions || isArchivedIssues;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -241,9 +296,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a href={issuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>Open issue in new tab</ContextMenu.Item>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
@ -284,27 +337,21 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex flex-shrink-0 items-center gap-2 text-xs ${
|
||||
isArchivedIssues ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<div className={`flex flex-shrink-0 items-center gap-2 text-xs ${isArchivedIssues ? "opacity-60" : ""}`}>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -323,14 +370,24 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={3}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -5,7 +5,7 @@ import useSWR from "swr";
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
@ -77,9 +77,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
workspaceSlug && projectId ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) : null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
@ -120,12 +118,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
switch (displayFilters?.group_by) {
|
||||
case "state":
|
||||
icon = currentState && (
|
||||
<StateGroupIcon
|
||||
stateGroup={currentState.group}
|
||||
color={currentState.color}
|
||||
height="16px"
|
||||
width="16px"
|
||||
/>
|
||||
<StateGroupIcon stateGroup={currentState.group} color={currentState.color} height="16px" width="16px" />
|
||||
);
|
||||
break;
|
||||
case "state_detail.group":
|
||||
@ -152,14 +145,8 @@ export const SingleList: React.FC<Props> = ({
|
||||
: null);
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: labelColor }}
|
||||
/>
|
||||
);
|
||||
const labelColor = issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
icon = <span className="h-3 w-3 flex-shrink-0 rounded-full" style={{ backgroundColor: labelColor }} />;
|
||||
break;
|
||||
case "assignees":
|
||||
case "created_by":
|
||||
@ -181,9 +168,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
|
||||
<Disclosure.Button>
|
||||
<div className="flex items-center gap-x-3">
|
||||
{displayFilters?.group_by !== null && (
|
||||
<div className="flex items-center">{getGroupIcon()}</div>
|
||||
)}
|
||||
{displayFilters?.group_by !== null && <div className="flex items-center">{getGroupIcon()}</div>}
|
||||
{displayFilters?.group_by !== null ? (
|
||||
<h2
|
||||
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
|
||||
@ -226,9 +211,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
@ -256,19 +239,14 @@ export const SingleList: React.FC<Props> = ({
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
handleDraftIssueSelect={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "edit")
|
||||
: undefined
|
||||
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "edit") : undefined
|
||||
}
|
||||
handleDraftIssueDelete={
|
||||
handleDraftIssueAction
|
||||
? () => handleDraftIssueAction(issue, "delete")
|
||||
: undefined
|
||||
handleDraftIssueAction ? () => handleDraftIssueAction(issue, "delete") : undefined
|
||||
}
|
||||
handleMyIssueOpen={handleMyIssueOpen}
|
||||
removeIssue={() => {
|
||||
if (removeIssue !== null && issue.bridge_id)
|
||||
removeIssue(issue.bridge_id, issue.id);
|
||||
if (removeIssue !== null && issue.bridge_id) removeIssue(issue.bridge_id, issue.id);
|
||||
}}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
@ -277,9 +255,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">
|
||||
No issues.
|
||||
</p>
|
||||
<p className="bg-custom-background-100 px-4 py-2.5 text-sm text-custom-text-200">No issues.</p>
|
||||
)
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
|
@ -1,48 +1,27 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import {
|
||||
EllipsisHorizontalIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
import { EllipsisHorizontalIcon, LinkIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// constant
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
||||
// helper
|
||||
import issuesService from "services/issue.service";
|
||||
import trackEventServices from "services/track_event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IState, Properties, TIssuePriorities, UserAuth } from "types";
|
||||
// constant
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
@ -77,9 +56,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -87,65 +64,12 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
sub_issues: (prevData.sub_issues ?? []).map((i) => {
|
||||
if (i.id === issue.id) {
|
||||
return {
|
||||
...i,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
else
|
||||
mutate<IIssue[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((p) => {
|
||||
if (p.id === issue.id) {
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.patchIssue(workspaceSlug as string, projectId as string, issue.id as string, formData, user)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
} else {
|
||||
mutate(fetchKey);
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
@ -154,7 +78,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
|
||||
[workspaceSlug, projectId, cycleId, moduleId, user]
|
||||
);
|
||||
|
||||
const openPeekOverview = () => {
|
||||
@ -167,11 +91,8 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
@ -180,6 +101,86 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 68}px`;
|
||||
|
||||
const tooltipPosition = index === 0 ? "bottom" : "top";
|
||||
@ -283,47 +284,49 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.priority && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={1} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -1,48 +1,46 @@
|
||||
import React from "react";
|
||||
// hooks
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// component
|
||||
import { CustomMenu, Icon } from "components/ui";
|
||||
// icon
|
||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { TIssueOrderByOptions } from "types";
|
||||
import { IIssueDisplayFilterOptions, TIssueOrderByOptions } from "types";
|
||||
|
||||
type Props = {
|
||||
columnData: any;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
gridTemplateColumns: string;
|
||||
handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
|
||||
};
|
||||
|
||||
export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateColumns }) => {
|
||||
export const SpreadsheetColumns: React.FC<Props> = (props) => {
|
||||
const { columnData, displayFilters, gridTemplateColumns, handleDisplayFiltersUpdate } = props;
|
||||
|
||||
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||
"spreadsheetViewSorting",
|
||||
""
|
||||
);
|
||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
||||
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
||||
|
||||
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
|
||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } = useLocalStorage(
|
||||
"spreadsheetViewActiveSortingProperty",
|
||||
""
|
||||
);
|
||||
|
||||
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||
setDisplayFilters({ order_by: order });
|
||||
handleDisplayFiltersUpdate({ order_by: order });
|
||||
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`}
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
<div className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`} style={{ gridTemplateColumns }}>
|
||||
{columnData.map((col: any) => {
|
||||
if (col.isActive) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-custom-background-90 w-full ${
|
||||
col.propertyName === "title"
|
||||
? "sticky left-0 z-20 bg-custom-background-90 pl-24"
|
||||
: ""
|
||||
col.propertyName === "title" ? "sticky left-0 z-20 bg-custom-background-90 pl-24" : ""
|
||||
}`}
|
||||
>
|
||||
{col.propertyName === "title" ? (
|
||||
@ -108,10 +106,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
|
||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||
</span>
|
||||
<span>A</span>
|
||||
@ -123,10 +118,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
col.propertyName === "updated_on" ? (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
|
||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||
</span>
|
||||
<span>New</span>
|
||||
@ -136,10 +128,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
) : (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 rotate-90 text-xs leading-3" />
|
||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||
</span>
|
||||
<span>First</span>
|
||||
@ -151,18 +140,14 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
||||
? "opacity-100"
|
||||
: ""
|
||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}` ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
className={`mt-0.5 ${
|
||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
||||
? "bg-custom-background-80"
|
||||
: ""
|
||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}` ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
key={col.property}
|
||||
onClick={() => {
|
||||
@ -180,10 +165,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
|
||||
<Icon
|
||||
iconName="sort"
|
||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||
@ -196,10 +178,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
) : col.propertyName === "due_date" ? (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
|
||||
<Icon
|
||||
iconName="sort"
|
||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||
@ -212,10 +191,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
) : (
|
||||
<>
|
||||
<span className="relative flex items-center h-6 w-6">
|
||||
<Icon
|
||||
iconName="east"
|
||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||
/>
|
||||
<Icon iconName="east" className="absolute left-0 -rotate-90 text-xs leading-3" />
|
||||
<Icon
|
||||
iconName="sort"
|
||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||
@ -230,9 +206,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
|
||||
<CheckIcon
|
||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
||||
? "opacity-100"
|
||||
: ""
|
||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}` ? "opacity-100" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@ -243,9 +217,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
|
||||
selectedMenuItem.includes(col.propertyName) && (
|
||||
<CustomMenu.MenuItem
|
||||
className={`mt-0.5${
|
||||
selectedMenuItem === `-created_at_${col.propertyName}`
|
||||
? "bg-custom-background-80"
|
||||
: ""
|
||||
selectedMenuItem === `-created_at_${col.propertyName}` ? "bg-custom-background-80" : ""
|
||||
}`}
|
||||
key={col.property}
|
||||
onClick={() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { SingleSpreadsheetIssue } from "components/core";
|
||||
|
@ -9,7 +9,6 @@ import { CustomMenu, Spinner } from "components/ui";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
// constants
|
||||
@ -39,8 +38,6 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
||||
@ -59,89 +56,89 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
.map((column) => column.colSize)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
handleMutation={() => mutateIssues()}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={disableUserActions}
|
||||
/>
|
||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||
</div>
|
||||
{spreadsheetIssues ? (
|
||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<SpreadsheetIssues
|
||||
key={`${issue.id}_${index}`}
|
||||
index={index}
|
||||
issue={issue}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
) : (
|
||||
!disableUserActions && (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-[1]"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return null;
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <IssuePeekOverview
|
||||
// handleMutation={() => mutateIssues()}
|
||||
// projectId={projectId?.toString() ?? ""}
|
||||
// workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
// readOnly={disableUserActions}
|
||||
// />
|
||||
// <div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
// <div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
||||
// <SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||
// </div>
|
||||
// {spreadsheetIssues ? (
|
||||
// <div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
||||
// {spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
// <SpreadsheetIssues
|
||||
// key={`${issue.id}_${index}`}
|
||||
// index={index}
|
||||
// issue={issue}
|
||||
// expandedIssues={expandedIssues}
|
||||
// setExpandedIssues={setExpandedIssues}
|
||||
// gridTemplateColumns={gridTemplateColumns}
|
||||
// properties={properties}
|
||||
// handleIssueAction={handleIssueAction}
|
||||
// disableUserActions={disableUserActions}
|
||||
// user={user}
|
||||
// userAuth={userAuth}
|
||||
// />
|
||||
// ))}
|
||||
// <div
|
||||
// className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
// style={{ gridTemplateColumns }}
|
||||
// >
|
||||
// {type === "issue" ? (
|
||||
// <button
|
||||
// className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
// onClick={() => {
|
||||
// const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
// document.dispatchEvent(e);
|
||||
// }}
|
||||
// >
|
||||
// <PlusIcon className="h-4 w-4" />
|
||||
// Add Issue
|
||||
// </button>
|
||||
// ) : (
|
||||
// !disableUserActions && (
|
||||
// <CustomMenu
|
||||
// className="sticky left-0 z-[1]"
|
||||
// customButton={
|
||||
// <button
|
||||
// className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
// type="button"
|
||||
// >
|
||||
// <PlusIcon className="h-4 w-4" />
|
||||
// Add Issue
|
||||
// </button>
|
||||
// }
|
||||
// position="left"
|
||||
// optionsClassName="left-5 !w-36"
|
||||
// noBorder
|
||||
// >
|
||||
// <CustomMenu.MenuItem
|
||||
// onClick={() => {
|
||||
// const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
// document.dispatchEvent(e);
|
||||
// }}
|
||||
// >
|
||||
// Create new
|
||||
// </CustomMenu.MenuItem>
|
||||
// {openIssuesListModal && (
|
||||
// <CustomMenu.MenuItem onClick={openIssuesListModal}>Add an existing issue</CustomMenu.MenuItem>
|
||||
// )}
|
||||
// </CustomMenu>
|
||||
// )
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <Spinner />
|
||||
// )}
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
};
|
||||
|
@ -127,7 +127,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
366
web/components/cycles/cycles-board-card.tsx
Normal file
366
web/components/cycles/cycles-board-card.tsx
Normal file
@ -0,0 +1,366 @@
|
||||
import React, { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SingleProgressStats } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
import { AssigneesList } from "components/ui/avatar";
|
||||
import { RadialProgressBar } from "@plane/ui";
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
PersonRunningIcon,
|
||||
ArrowRightIcon,
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
} from "components/icons";
|
||||
import { ChevronDownIcon, LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
|
||||
const stateGroups = [
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#dee2e6",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#26b5ce",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#f7ae59",
|
||||
},
|
||||
{
|
||||
key: "cancelled_issues",
|
||||
title: "Cancelled",
|
||||
color: "#d687ff",
|
||||
},
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#09a953",
|
||||
},
|
||||
];
|
||||
|
||||
export interface ICyclesBoardCard {
|
||||
cycle: ICycle;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
|
||||
const { cycle } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const groupedIssues: any = {
|
||||
backlog: cycle.backlog_issues,
|
||||
unstarted: cycle.unstarted_issues,
|
||||
started: cycle.started_issues,
|
||||
completed: cycle.completed_issues,
|
||||
cancelled: cycle.cancelled_issues,
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {};
|
||||
const handleAddToFavorites = () => {};
|
||||
|
||||
const handleEditCycle = () => {};
|
||||
const handleDeleteCycle = () => {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-5 w-5">
|
||||
<ContrastIcon
|
||||
className="h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
|
||||
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 15)}</h3>
|
||||
</Tooltip>
|
||||
</span>
|
||||
<span className="flex items-center gap-1 capitalize">
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
|
||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
Completed
|
||||
</span>
|
||||
) : (
|
||||
cycleStatus
|
||||
)}
|
||||
</span>
|
||||
{cycle.is_favorite ? (
|
||||
<button onClick={handleRemoveFromFavorites}>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleAddToFavorites}>
|
||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-4 items-center justify-start gap-5 text-custom-text-200">
|
||||
{cycleStatus !== "draft" && (
|
||||
<>
|
||||
<div className="flex items-start gap-1">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<div className="flex items-start gap-1">
|
||||
<TargetIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex flex-col gap-2 text-xs text-custom-text-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-16">Creator:</div>
|
||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 items-center gap-2">
|
||||
<div className="w-16">Members:</div>
|
||||
{cycle.assignees.length > 0 ? (
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<AssigneesList users={cycle.assignees} length={4} />
|
||||
</div>
|
||||
) : (
|
||||
"No members"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
{!isCompleted && (
|
||||
<button
|
||||
onClick={handleEditCycle}
|
||||
className="cursor-pointer rounded p-1 text-custom-text-200 duration-300 hover:bg-custom-background-80"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy cycle link</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className="flex h-full flex-col rounded-b-[10px]">
|
||||
<Disclosure>
|
||||
{({ open }) => (
|
||||
<div
|
||||
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
|
||||
open ? "" : "flex-row"
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-full items-center gap-2 px-4 py-1">
|
||||
<span>Progress</span>
|
||||
<Tooltip
|
||||
tooltipContent={
|
||||
<div className="flex w-56 flex-col">
|
||||
{Object.keys(groupedIssues).map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full "
|
||||
style={{
|
||||
backgroundColor: stateGroups[index].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs capitalize">{group}</span>
|
||||
</div>
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={cycle.total_issues}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Disclosure.Button>
|
||||
<span className="p-1">
|
||||
<ChevronDownIcon className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
|
||||
</span>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Transition show={open}>
|
||||
<Disclosure.Panel>
|
||||
<div className="overflow-hidden rounded-b-md bg-custom-background-80 py-3 shadow">
|
||||
<div className="col-span-2 space-y-3 px-4">
|
||||
<div className="space-y-3 text-xs">
|
||||
{stateGroups.map((group) => (
|
||||
<div key={group.key} className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-2 w-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor: group.color,
|
||||
}}
|
||||
/>
|
||||
<h6 className="text-xs">{group.title}</h6>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
{cycle[group.key as keyof ICycle] as number}{" "}
|
||||
<span className="text-custom-text-200">
|
||||
-{" "}
|
||||
{cycle.total_issues > 0
|
||||
? `${Math.round(
|
||||
((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
|
||||
)}%`
|
||||
: "0%"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
53
web/components/cycles/cycles-board.tsx
Normal file
53
web/components/cycles/cycles-board.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { FC } from "react";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// components
|
||||
import { CyclesBoardCard } from "components/cycles";
|
||||
|
||||
export interface ICyclesBoard {
|
||||
cycles: ICycle[];
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
|
||||
const { cycles, filter } = props;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{cycles.length > 0 ? (
|
||||
<>
|
||||
{cycles.map((cycle) => (
|
||||
<CyclesBoardCard key={cycle.id} cycle={cycle} filter={filter} />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full grid place-items-center text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto flex justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
|
||||
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
fill="rgb(var(--color-text-400))"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">{filter === "all" ? "No cycles" : `No ${filter} cycles`}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 text-sm outline-none"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create a new cycle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
0
web/components/cycles/cycles-gantt.tsx
Normal file
0
web/components/cycles/cycles-gantt.tsx
Normal file
328
web/components/cycles/cycles-list-item.tsx
Normal file
328
web/components/cycles/cycles-list-item.tsx
Normal file
@ -0,0 +1,328 @@
|
||||
import { FC, MouseEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { RadialProgressBar } from "@plane/ui";
|
||||
import { CustomMenu, LinearProgressIndicator, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
TargetIcon,
|
||||
ContrastIcon,
|
||||
PersonRunningIcon,
|
||||
ArrowRightIcon,
|
||||
TriangleExclamationIcon,
|
||||
AlarmClockIcon,
|
||||
} from "components/icons";
|
||||
import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// helpers
|
||||
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
type TCyclesListItem = {
|
||||
cycle: ICycle;
|
||||
handleEditCycle?: () => void;
|
||||
handleDeleteCycle?: () => void;
|
||||
handleAddToFavorites?: () => void;
|
||||
handleRemoveFromFavorites?: () => void;
|
||||
};
|
||||
|
||||
const stateGroups = [
|
||||
{
|
||||
key: "backlog_issues",
|
||||
title: "Backlog",
|
||||
color: "#dee2e6",
|
||||
},
|
||||
{
|
||||
key: "unstarted_issues",
|
||||
title: "Unstarted",
|
||||
color: "#26b5ce",
|
||||
},
|
||||
{
|
||||
key: "started_issues",
|
||||
title: "Started",
|
||||
color: "#f7ae59",
|
||||
},
|
||||
{
|
||||
key: "cancelled_issues",
|
||||
title: "Cancelled",
|
||||
color: "#d687ff",
|
||||
},
|
||||
{
|
||||
key: "completed_issues",
|
||||
title: "Completed",
|
||||
color: "#09a953",
|
||||
},
|
||||
];
|
||||
|
||||
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
|
||||
const { cycle } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// toast
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
const isCompleted = cycleStatus === "completed";
|
||||
const endDate = new Date(cycle.end_date ?? "");
|
||||
const startDate = new Date(cycle.start_date ?? "");
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Cycle link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const progressIndicatorData = stateGroups.map((group, index) => ({
|
||||
id: index,
|
||||
name: group.title,
|
||||
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
|
||||
color: group.color,
|
||||
}));
|
||||
|
||||
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
cycleStore.addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
cycleStore.removeCycleFromFavorites(workspaceSlug?.toString(), projectId.toString(), cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col text-xs hover:bg-custom-background-80">
|
||||
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
|
||||
<a className="w-full">
|
||||
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex items-start gap-2">
|
||||
<ContrastIcon
|
||||
className="mt-1 h-5 w-5"
|
||||
color={`${
|
||||
cycleStatus === "current"
|
||||
? "#09A953"
|
||||
: cycleStatus === "upcoming"
|
||||
? "#F7AE59"
|
||||
: cycleStatus === "completed"
|
||||
? "#3F76FF"
|
||||
: cycleStatus === "draft"
|
||||
? "rgb(var(--color-text-200))"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
<div className="max-w-2xl">
|
||||
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
|
||||
<h3 className="break-words w-full text-base font-semibold">{truncateText(cycle.name, 60)}</h3>
|
||||
</Tooltip>
|
||||
<p className="mt-2 text-custom-text-200 break-words w-full">{cycle.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex items-center gap-4">
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-0.5
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
<PersonRunningIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.end_date ?? new Date())} days left
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<AlarmClockIcon className="h-4 w-4" />
|
||||
{findHowManyDaysLeft(cycle.start_date ?? new Date())} days left
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex items-center gap-1">
|
||||
{cycle.total_issues - cycle.completed_issues > 0 && (
|
||||
<Tooltip
|
||||
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
|
||||
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
|
||||
}`}
|
||||
>
|
||||
<span>
|
||||
<TriangleExclamationIcon className="h-3.5 w-3.5 fill-current" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}{" "}
|
||||
Completed
|
||||
</span>
|
||||
) : (
|
||||
cycleStatus
|
||||
)}
|
||||
</span>
|
||||
|
||||
{cycleStatus !== "draft" && (
|
||||
<div className="flex items-center justify-start gap-2 text-custom-text-200">
|
||||
<div className="flex items-start gap-1 whitespace-nowrap">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(startDate)}</span>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<div className="flex items-start gap-1 whitespace-nowrap">
|
||||
<TargetIcon className="h-4 w-4" />
|
||||
<span>{renderShortDateWithYearFormat(endDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2.5 text-custom-text-200">
|
||||
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
|
||||
<img
|
||||
src={cycle.owned_by.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
className="rounded-full"
|
||||
alt={cycle.owned_by.display_name}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
|
||||
{cycle.owned_by.display_name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
position="top-right"
|
||||
tooltipContent={
|
||||
<div className="flex w-80 items-center gap-2 px-4 py-1">
|
||||
<span>Progress</span>
|
||||
<LinearProgressIndicator data={progressIndicatorData} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`rounded-md px-1.5 py-1
|
||||
${
|
||||
cycleStatus === "current"
|
||||
? "border border-green-600 bg-green-600/5 text-green-600"
|
||||
: cycleStatus === "upcoming"
|
||||
? "border border-orange-300 bg-orange-300/5 text-orange-300"
|
||||
: cycleStatus === "completed"
|
||||
? "border border-blue-500 bg-blue-500/5 text-blue-500"
|
||||
: cycleStatus === "draft"
|
||||
? "border border-neutral-400 bg-neutral-400/5 text-neutral-400"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{cycleStatus === "current" ? (
|
||||
<span className="flex gap-1 whitespace-nowrap">
|
||||
{cycle.total_issues > 0 ? (
|
||||
<>
|
||||
<RadialProgressBar progress={(cycle.completed_issues / cycle.total_issues) * 100} />
|
||||
<span>{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="normal-case">No issues present</span>
|
||||
)}
|
||||
</span>
|
||||
) : cycleStatus === "upcoming" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={100} /> Yet to start
|
||||
</span>
|
||||
) : cycleStatus === "completed" ? (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={100} />
|
||||
<span>{100} %</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex gap-1">
|
||||
<RadialProgressBar progress={(cycle.total_issues / cycle.completed_issues) * 100} />
|
||||
{cycleStatus}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{cycle.is_favorite ? (
|
||||
<button type="button" onClick={handleRemoveFromFavorites}>
|
||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleAddToFavorites}>
|
||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<CustomMenu width="auto" verticalEllipsis>
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem onClick={handleEditCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit Cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{!isCompleted && (
|
||||
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete cycle</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy cycle link</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -20,8 +20,7 @@ export const AllCyclesList: React.FC<Props> = ({ viewType }) => {
|
||||
const { data: allCyclesList, mutate } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLES_LIST(projectId.toString()) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
|
||||
? () => cyclesService.getCyclesWithParams(workspaceSlug.toString(), projectId.toString(), "all")
|
||||
: null
|
||||
);
|
||||
|
70
web/components/cycles/cycles-list.tsx
Normal file
70
web/components/cycles/cycles-list.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { FC } from "react";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
import { CyclesListItem } from "./cycles-list-item";
|
||||
|
||||
export interface ICyclesList {
|
||||
cycles: ICycle[];
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export const CyclesList: FC<ICyclesList> = (props) => {
|
||||
const { cycles, filter } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{cycles ? (
|
||||
<>
|
||||
{cycles.length > 0 ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-custom-background-80" key={cycle.id}>
|
||||
<div className="flex flex-col border-custom-border-200">
|
||||
<CyclesListItem cycle={cycle} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full grid place-items-center text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto flex justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
|
||||
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
fill="rgb(var(--color-text-400))"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">
|
||||
{filter === "all" ? "No cycles" : `No ${filter} cycles`}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 text-sm outline-none"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create a new cycle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
254
web/components/cycles/cycles-view-legacy.tsx
Normal file
254
web/components/cycles/cycles-view-legacy.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { KeyedMutator, mutate } from "swr";
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
import {
|
||||
CreateUpdateCycleModal,
|
||||
CyclesListGanttChartView,
|
||||
DeleteCycleModal,
|
||||
SingleCycleCard,
|
||||
SingleCycleList,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// helpers
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
COMPLETED_CYCLES_LIST,
|
||||
CURRENT_CYCLE_LIST,
|
||||
CYCLES_LIST,
|
||||
DRAFT_CYCLES_LIST,
|
||||
UPCOMING_CYCLES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
import { CYCLE_TAB_LIST, CYCLE_VIEWS } from "constants/cycle";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[] | undefined;
|
||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||
viewType: string | null;
|
||||
};
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
|
||||
|
||||
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
||||
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
||||
|
||||
const { storedValue: cycleTab } = useLocalStorage("cycle_tab", "all");
|
||||
console.log("cycleTab", cycleTab);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToUpdate(cycle);
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToDelete(cycle);
|
||||
setDeleteCycleModal(true);
|
||||
};
|
||||
|
||||
const handleAddToFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CURRENT_CYCLE_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? UPCOMING_CYCLES_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? COMPLETED_CYCLES_LIST(projectId as string)
|
||||
: DRAFT_CYCLES_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CURRENT_CYCLE_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? UPCOMING_CYCLES_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? COMPLETED_CYCLES_LIST(projectId as string)
|
||||
: DRAFT_CYCLES_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={createUpdateCycleModal}
|
||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||
data={selectedCycleToUpdate}
|
||||
user={user}
|
||||
/>
|
||||
<DeleteCycleModal
|
||||
isOpen={deleteCycleModal}
|
||||
setIsOpen={setDeleteCycleModal}
|
||||
data={selectedCycleToDelete}
|
||||
user={user}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
viewType === "list" ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-custom-background-80">
|
||||
<div className="flex flex-col border-custom-border-200">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : viewType === "board" ? (
|
||||
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
|
||||
)
|
||||
) : (
|
||||
<div className="h-full grid place-items-center text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto flex justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 66 66" fill="none">
|
||||
<circle cx="34.375" cy="34.375" r="22" stroke="rgb(var(--color-text-400))" strokeLinecap="round" />
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
fill="rgb(var(--color-text-400))"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">
|
||||
{cycleTab === "all" ? "No cycles" : `No ${cycleTab} cycles`}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 text-sm outline-none"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create a new cycle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : viewType === "list" ? (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
) : viewType === "board" ? (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="300px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,271 +1,61 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { KeyedMutator, mutate } from "swr";
|
||||
|
||||
// services
|
||||
import cyclesService from "services/cycles.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import {
|
||||
CreateUpdateCycleModal,
|
||||
CyclesListGanttChartView,
|
||||
DeleteCycleModal,
|
||||
SingleCycleCard,
|
||||
SingleCycleList,
|
||||
} from "components/cycles";
|
||||
// ui
|
||||
import { CyclesBoard, CyclesList } from "components/cycles";
|
||||
import { Loader } from "components/ui";
|
||||
// helpers
|
||||
import { getDateRangeStatus } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
COMPLETED_CYCLES_LIST,
|
||||
CURRENT_CYCLE_LIST,
|
||||
CYCLES_LIST,
|
||||
DRAFT_CYCLES_LIST,
|
||||
UPCOMING_CYCLES_LIST,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[] | undefined;
|
||||
mutateCycles: KeyedMutator<ICycle[]>;
|
||||
viewType: string | null;
|
||||
};
|
||||
export interface ICyclesView {
|
||||
filter: "all" | "current" | "upcoming" | "draft" | "completed" | "incomplete";
|
||||
view: "list" | "board" | "gantt";
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType }) => {
|
||||
const [createUpdateCycleModal, setCreateUpdateCycleModal] = useState(false);
|
||||
const [selectedCycleToUpdate, setSelectedCycleToUpdate] = useState<ICycle | null>(null);
|
||||
export const CyclesView: FC<ICyclesView> = observer((props) => {
|
||||
const { filter, view, workspaceSlug, projectId } = props;
|
||||
// store
|
||||
const { cycle: cycleStore } = useMobxStore();
|
||||
// api call to fetch cycles list
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `CYCLES_LIST_${projectId}_${filter}` : null,
|
||||
workspaceSlug && projectId ? () => cycleStore.fetchCycles(workspaceSlug, projectId, filter) : null
|
||||
);
|
||||
|
||||
const [deleteCycleModal, setDeleteCycleModal] = useState(false);
|
||||
const [selectedCycleToDelete, setSelectedCycleToDelete] = useState<ICycle | null>(null);
|
||||
|
||||
const { storedValue: cycleTab } = useLocalStorage("cycleTab", "All");
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleEditCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToUpdate(cycle);
|
||||
setCreateUpdateCycleModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteCycle = (cycle: ICycle) => {
|
||||
setSelectedCycleToDelete(cycle);
|
||||
setDeleteCycleModal(true);
|
||||
};
|
||||
|
||||
const handleAddToFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CURRENT_CYCLE_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? UPCOMING_CYCLES_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? COMPLETED_CYCLES_LIST(projectId as string)
|
||||
: DRAFT_CYCLES_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? true : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.addCycleToFavorites(workspaceSlug as string, projectId as string, {
|
||||
cycle: cycle.id,
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't add the cycle to favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = (cycle: ICycle) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
|
||||
|
||||
const fetchKey =
|
||||
cycleStatus === "current"
|
||||
? CURRENT_CYCLE_LIST(projectId as string)
|
||||
: cycleStatus === "upcoming"
|
||||
? UPCOMING_CYCLES_LIST(projectId as string)
|
||||
: cycleStatus === "completed"
|
||||
? COMPLETED_CYCLES_LIST(projectId as string)
|
||||
: DRAFT_CYCLES_LIST(projectId as string);
|
||||
|
||||
mutate<ICycle[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((c) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
mutate(
|
||||
CYCLES_LIST(projectId as string),
|
||||
(prevData: any) =>
|
||||
(prevData ?? []).map((c: any) => ({
|
||||
...c,
|
||||
is_favorite: c.id === cycle.id ? false : c.is_favorite,
|
||||
})),
|
||||
false
|
||||
);
|
||||
|
||||
cyclesService
|
||||
.removeCycleFromFavorites(workspaceSlug as string, projectId as string, cycle.id)
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Couldn't remove the cycle from favorites. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
const cyclesList = cycleStore.cycles?.[projectId];
|
||||
console.log("cyclesList", cyclesList);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateCycleModal
|
||||
isOpen={createUpdateCycleModal}
|
||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||
data={selectedCycleToUpdate}
|
||||
user={user}
|
||||
/>
|
||||
<DeleteCycleModal
|
||||
isOpen={deleteCycleModal}
|
||||
setIsOpen={setDeleteCycleModal}
|
||||
data={selectedCycleToDelete}
|
||||
user={user}
|
||||
/>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
viewType === "list" ? (
|
||||
<div className="divide-y divide-custom-border-200">
|
||||
{cycles.map((cycle) => (
|
||||
<div className="hover:bg-custom-background-80">
|
||||
<div className="flex flex-col border-custom-border-200">
|
||||
<SingleCycleList
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : viewType === "board" ? (
|
||||
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{cycles.map((cycle) => (
|
||||
<SingleCycleCard
|
||||
key={cycle.id}
|
||||
cycle={cycle}
|
||||
handleDeleteCycle={() => handleDeleteCycle(cycle)}
|
||||
handleEditCycle={() => handleEditCycle(cycle)}
|
||||
handleAddToFavorites={() => handleAddToFavorites(cycle)}
|
||||
handleRemoveFromFavorites={() => handleRemoveFromFavorites(cycle)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{view === "list" && (
|
||||
<>
|
||||
{!isLoading ? (
|
||||
<CyclesList cycles={cyclesList} filter={filter} />
|
||||
) : (
|
||||
<CyclesListGanttChartView cycles={cycles ?? []} mutateCycles={mutateCycles} />
|
||||
)
|
||||
) : (
|
||||
<div className="h-full grid place-items-center text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto flex justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="66"
|
||||
height="66"
|
||||
viewBox="0 0 66 66"
|
||||
fill="none"
|
||||
>
|
||||
<circle
|
||||
cx="34.375"
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
fill="rgb(var(--color-text-400))"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 className="text-sm text-custom-text-200">
|
||||
{cycleTab === "All"
|
||||
? "No cycles"
|
||||
: `No ${cycleTab === "Drafts" ? "draft" : cycleTab?.toLowerCase()} cycles`}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="text-custom-primary-100 text-sm outline-none"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", {
|
||||
key: "q",
|
||||
});
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create a new cycle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : viewType === "list" ? (
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
) : viewType === "board" ? (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="300px" />
|
||||
</Loader>
|
||||
<Loader className="space-y-4">
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
<Loader.Item height="50px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{view === "board" && (
|
||||
<>
|
||||
{!isLoading ? (
|
||||
<CyclesBoard cycles={cyclesList} filter={filter} />
|
||||
) : (
|
||||
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
<Loader.Item height="200px" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{view === "gantt" && <CyclesList cycles={cyclesList} filter={filter} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,50 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
// react-hook-form
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
// ui
|
||||
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||
import { Input, TextArea } from "@plane/ui";
|
||||
import { DateSelect, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import { ICycle } from "types";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
status: boolean;
|
||||
data?: ICycle | null;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<ICycle> = {
|
||||
name: "",
|
||||
description: "",
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
};
|
||||
|
||||
export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
|
||||
export const CycleForm: React.FC<Props> = (props) => {
|
||||
const { handleFormSubmit, handleClose, data } = props;
|
||||
// form data
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<ICycle>({
|
||||
defaultValues,
|
||||
defaultValues: {
|
||||
name: data?.name || "",
|
||||
description: data?.description || "",
|
||||
start_date: data?.start_date || null,
|
||||
end_date: data?.end_date || null,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateUpdateCycle = async (formData: Partial<ICycle>) => {
|
||||
await handleFormSubmit(formData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...data,
|
||||
});
|
||||
}, [data, reset]);
|
||||
|
||||
const startDate = watch("start_date");
|
||||
const endDate = watch("end_date");
|
||||
|
||||
@ -55,39 +38,50 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
maxDate?.setDate(maxDate.getDate() - 1);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateCycle)}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
{status ? "Update" : "Create"} Cycle
|
||||
</h3>
|
||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{status ? "Update" : "Create"} Cycle</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
id="name"
|
||||
<Controller
|
||||
name="name"
|
||||
type="name"
|
||||
className="resize-none text-xl"
|
||||
placeholder="Title"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
validations={{
|
||||
control={control}
|
||||
rules={{
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Name should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input
|
||||
id="cycle_name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Cycle Name"
|
||||
className="resize-none text-xl w-full p-2"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
hasError={Boolean(errors?.name)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
id="description"
|
||||
<Controller
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="h-32 resize-none text-sm"
|
||||
error={errors.description}
|
||||
register={register}
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<TextArea
|
||||
id="cycle_description"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
className="h-32 resize-none text-sm"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -112,12 +106,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
control={control}
|
||||
name="end_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DateSelect
|
||||
label="End date"
|
||||
value={value}
|
||||
onChange={(val) => onChange(val)}
|
||||
minDate={minDate}
|
||||
/>
|
||||
<DateSelect label="End date" value={value} onChange={(val) => onChange(val)} minDate={minDate} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@ -127,7 +116,7 @@ export const CycleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, stat
|
||||
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{status
|
||||
{data
|
||||
? isSubmitting
|
||||
? "Updating Cycle..."
|
||||
: "Update Cycle"
|
||||
|
@ -4,7 +4,6 @@ import { useRouter } from "next/router";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useUser from "hooks/use-user";
|
||||
import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view";
|
||||
import { updateGanttIssue } from "components/gantt-chart/hooks/block-update";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
// components
|
||||
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
|
||||
@ -47,9 +46,7 @@ export const CycleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions
|
||||
title="Issues"
|
||||
loaderTitle="Issues"
|
||||
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null}
|
||||
blockUpdateHandler={(block, payload) =>
|
||||
updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
|
||||
}
|
||||
blockUpdateHandler={(block, payload) => {}}
|
||||
SidebarBlockRender={IssueGanttSidebarBlock}
|
||||
BlockRender={IssueGanttBlock}
|
||||
enableBlockLeftResize={isAllowed}
|
||||
|
@ -17,7 +17,7 @@ import { ICycle } from "types";
|
||||
|
||||
type Props = {
|
||||
cycles: ICycle[];
|
||||
mutateCycles: KeyedMutator<ICycle[]>;
|
||||
mutateCycles?: KeyedMutator<ICycle[]>;
|
||||
};
|
||||
|
||||
export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) => {
|
||||
@ -29,33 +29,32 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
|
||||
|
||||
const handleCycleUpdate = (cycle: ICycle, payload: IBlockUpdateData) => {
|
||||
if (!workspaceSlug || !user) return;
|
||||
mutateCycles &&
|
||||
mutateCycles((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
mutateCycles((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === cycle.id
|
||||
? {
|
||||
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === cycle.id
|
||||
? {
|
||||
start_date: payload.start_date ? payload.start_date : p.start_date,
|
||||
target_date: payload.target_date ? payload.target_date : p.end_date,
|
||||
sort_order: payload.sort_order ? payload.sort_order.newSortOrder : p.sort_order,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
return newList;
|
||||
}, false);
|
||||
return newList;
|
||||
}, false);
|
||||
|
||||
const newPayload: any = { ...payload };
|
||||
|
||||
if (newPayload.sort_order && payload.sort_order)
|
||||
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
if (newPayload.sort_order && payload.sort_order) newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
|
||||
cyclesService.patchCycle(workspaceSlug.toString(), cycle.project, cycle.id, newPayload, user);
|
||||
};
|
||||
@ -63,9 +62,7 @@ export const CyclesListGanttChartView: FC<Props> = ({ cycles, mutateCycles }) =>
|
||||
const blockFormat = (blocks: ICycle[]) =>
|
||||
blocks && blocks.length > 0
|
||||
? blocks
|
||||
.filter(
|
||||
(b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date)
|
||||
)
|
||||
.filter((b) => b.start_date && b.end_date && new Date(b.start_date) <= new Date(b.end_date))
|
||||
.map((block) => ({
|
||||
data: block,
|
||||
id: block.id,
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from "./cycles-list";
|
||||
export * from "./cycles-view";
|
||||
export * from "./active-cycle-details";
|
||||
export * from "./active-cycle-stats";
|
||||
export * from "./gantt-chart";
|
||||
@ -12,3 +12,8 @@ export * from "./single-cycle-card";
|
||||
export * from "./single-cycle-list";
|
||||
export * from "./transfer-issues-modal";
|
||||
export * from "./transfer-issues";
|
||||
export * from "./cycles-list";
|
||||
export * from "./cycles-list-item";
|
||||
export * from "./cycles-board";
|
||||
export * from "./cycles-board-card";
|
||||
export * from "./cycles-gantt";
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import cycleService from "services/cycles.service";
|
||||
@ -34,12 +30,7 @@ type CycleModalProps = {
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
data,
|
||||
user,
|
||||
}) => {
|
||||
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, handleClose, data, user }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -116,10 +107,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
mutate(DRAFT_CYCLES_LIST(projectId.toString()));
|
||||
}
|
||||
mutate(CYCLES_LIST(projectId.toString()));
|
||||
if (
|
||||
getDateRangeStatus(data?.start_date, data?.end_date) !=
|
||||
getDateRangeStatus(res.start_date, res.end_date)
|
||||
) {
|
||||
if (getDateRangeStatus(data?.start_date, data?.end_date) != getDateRangeStatus(res.start_date, res.end_date)) {
|
||||
switch (getDateRangeStatus(res.start_date, res.end_date)) {
|
||||
case "completed":
|
||||
mutate(COMPLETED_CYCLES_LIST(projectId.toString()));
|
||||
@ -141,7 +129,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
message: "Cycle updated successfully.",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
@ -153,11 +141,9 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
const dateChecker = async (payload: CycleDateCheckData) => {
|
||||
let status = false;
|
||||
|
||||
await cycleService
|
||||
.cycleDateCheck(workspaceSlug as string, projectId as string, payload)
|
||||
.then((res) => {
|
||||
status = res.status;
|
||||
});
|
||||
await cycleService.cycleDateCheck(workspaceSlug as string, projectId as string, payload).then((res) => {
|
||||
status = res.status;
|
||||
});
|
||||
|
||||
return status;
|
||||
};
|
||||
@ -194,8 +180,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message:
|
||||
"You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.",
|
||||
message: "You already have a cycle on the given dates, if you want to create a draft cycle, remove the dates.",
|
||||
});
|
||||
};
|
||||
|
||||
@ -225,12 +210,7 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<CycleForm
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
handleClose={handleClose}
|
||||
status={data ? true : false}
|
||||
data={data}
|
||||
/>
|
||||
<CycleForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} data={data} />
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -25,17 +25,12 @@ const tabOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const EmojiIconPicker: React.FC<Props> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
onIconColorChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const EmojiIconPicker: React.FC<Props> = (props) => {
|
||||
const { label, value, onChange, onIconColorChange, disabled = false } = props;
|
||||
// states
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [openColorPicker, setOpenColorPicker] = useState(false);
|
||||
const [activeColor, setActiveColor] = useState<string>("rgb(var(--color-text-200))");
|
||||
|
||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||
|
||||
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||
@ -52,11 +47,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<Popover className="relative z-[1]">
|
||||
<Popover.Button
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className="outline-none"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Popover.Button onClick={() => setIsOpen((prev) => !prev)} className="outline-none" disabled={disabled}>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
@ -139,15 +130,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
||||
<Tab.Panel className="flex h-full w-full flex-col justify-center">
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between px-1 pb-2">
|
||||
{[
|
||||
"#FF6B00",
|
||||
"#8CC1FF",
|
||||
"#FCBE1D",
|
||||
"#18904F",
|
||||
"#ADF672",
|
||||
"#05C3FF",
|
||||
"#000000",
|
||||
].map((curCol) => (
|
||||
{["#FF6B00", "#8CC1FF", "#FCBE1D", "#18904F", "#ADF672", "#05C3FF", "#000000"].map((curCol) => (
|
||||
<span
|
||||
key={curCol}
|
||||
className="h-4 w-4 cursor-pointer rounded-full"
|
||||
@ -168,9 +151,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
||||
</div>
|
||||
<div>
|
||||
<TwitterPicker
|
||||
className={`!absolute top-4 left-4 z-10 m-2 ${
|
||||
openColorPicker ? "block" : "hidden"
|
||||
}`}
|
||||
className={`!absolute top-4 left-4 z-10 m-2 ${openColorPicker ? "block" : "hidden"}`}
|
||||
color={activeColor}
|
||||
onChange={(color) => {
|
||||
setActiveColor(color.hex);
|
||||
@ -193,10 +174,7 @@ const EmojiIconPicker: React.FC<Props> = ({
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ color: activeColor }}
|
||||
className="material-symbols-rounded text-lg"
|
||||
>
|
||||
<span style={{ color: activeColor }} className="material-symbols-rounded text-lg">
|
||||
{icon.name}
|
||||
</span>
|
||||
</button>
|
||||
|
@ -9,7 +9,7 @@ import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import estimatesService from "services/estimates.service";
|
||||
import estimatesService from "services/project_estimates.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
@ -119,13 +119,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
||||
);
|
||||
|
||||
await estimatesService
|
||||
.patchEstimate(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
data?.id as string,
|
||||
payload,
|
||||
user
|
||||
)
|
||||
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload, user)
|
||||
.then(() => {
|
||||
mutate(ESTIMATES_LIST(projectId.toString()));
|
||||
mutate(ESTIMATE_DETAILS(data.id));
|
||||
@ -257,9 +251,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
|
||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium leading-6">
|
||||
{data ? "Update" : "Create"} Estimate
|
||||
</div>
|
||||
<div className="text-lg font-medium leading-6">{data ? "Update" : "Create"} Estimate</div>
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import CSVIntegrationService from "services/integration/csv.services";
|
||||
import { CSVIntegrationService } from "services/csv.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
@ -23,13 +22,9 @@ type Props = {
|
||||
mutateServices: () => void;
|
||||
};
|
||||
|
||||
export const Exporter: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
user,
|
||||
provider,
|
||||
mutateServices,
|
||||
}) => {
|
||||
const cvsService = new CSVIntegrationService();
|
||||
|
||||
export const Exporter: React.FC<Props> = ({ isOpen, handleClose, user, provider, mutateServices }) => {
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -60,7 +55,8 @@ export const Exporter: React.FC<Props> = ({
|
||||
project: value,
|
||||
multiple: multiple,
|
||||
};
|
||||
await CSVIntegrationService.exportCSVService(workspaceSlug as string, payload, user)
|
||||
await cvsService
|
||||
.exportCSVService(workspaceSlug as string, payload, user)
|
||||
.then(() => {
|
||||
mutateServices();
|
||||
router.push(`/${workspaceSlug}/settings/exports`);
|
||||
@ -69,13 +65,7 @@ export const Exporter: React.FC<Props> = ({
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
message: `You will be able to download the exported ${
|
||||
provider === "csv"
|
||||
? "CSV"
|
||||
: provider === "xlsx"
|
||||
? "Excel"
|
||||
: provider === "json"
|
||||
? "JSON"
|
||||
: ""
|
||||
provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""
|
||||
} from the previous export.`,
|
||||
});
|
||||
})
|
||||
@ -122,13 +112,7 @@ export const Exporter: React.FC<Props> = ({
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
Export to{" "}
|
||||
{provider === "csv"
|
||||
? "CSV"
|
||||
: provider === "xlsx"
|
||||
? "Excel"
|
||||
: provider === "json"
|
||||
? "JSON"
|
||||
: ""}
|
||||
{provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
|
||||
</h3>
|
||||
</span>
|
||||
</div>
|
||||
@ -155,22 +139,12 @@ export const Exporter: React.FC<Props> = ({
|
||||
onClick={() => setMultiple(!multiple)}
|
||||
className="flex items-center gap-2 max-w-min cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={multiple}
|
||||
onChange={() => setMultiple(!multiple)}
|
||||
/>
|
||||
<div className="text-sm whitespace-nowrap">
|
||||
Export the data into separate files
|
||||
</div>
|
||||
<input type="checkbox" checked={multiple} onChange={() => setMultiple(!multiple)} />
|
||||
<div className="text-sm whitespace-nowrap">Export the data into separate files</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={ExportCSVToMail}
|
||||
disabled={exportLoading}
|
||||
loading={exportLoading}
|
||||
>
|
||||
<PrimaryButton onClick={ExportCSVToMail} disabled={exportLoading} loading={exportLoading}>
|
||||
{exportLoading ? "Exporting..." : "Export"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import useSWR, { mutate } from "swr";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import IntegrationService from "services/integration";
|
||||
import IntegrationService from "services/integration.service";
|
||||
// components
|
||||
import { Exporter, SingleExport } from "components/exporter";
|
||||
// ui
|
||||
@ -32,9 +32,7 @@ const IntegrationGuide = () => {
|
||||
const { user } = useUserAuth();
|
||||
|
||||
const { data: exporterServices } = useSWR(
|
||||
workspaceSlug && cursor
|
||||
? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`)
|
||||
: null,
|
||||
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null,
|
||||
workspaceSlug && cursor
|
||||
? () => IntegrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page)
|
||||
: null
|
||||
@ -57,20 +55,11 @@ const IntegrationGuide = () => {
|
||||
<div className="flex items-start justify-between gap-4 w-full">
|
||||
<div className="flex item-center gap-2.5">
|
||||
<div className="relative h-10 w-10 flex-shrink-0">
|
||||
<Image
|
||||
src={service.logo}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt={`${service.title} Logo`}
|
||||
/>
|
||||
<Image src={service.logo} layout="fill" objectFit="cover" alt={`${service.title} Logo`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="flex items-center gap-4 text-sm font-medium">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||
{service.description}
|
||||
</p>
|
||||
<h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
|
||||
<p className="text-sm text-custom-text-200 tracking-tight">{service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
@ -96,9 +85,9 @@ const IntegrationGuide = () => {
|
||||
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
||||
onClick={() => {
|
||||
setRefreshing(true);
|
||||
mutate(
|
||||
EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)
|
||||
).then(() => setRefreshing(false));
|
||||
mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() =>
|
||||
setRefreshing(false)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
|
||||
@ -108,9 +97,7 @@ const IntegrationGuide = () => {
|
||||
<div className="flex gap-2 items-center text-xs">
|
||||
<button
|
||||
disabled={!exporterServices?.prev_page_results}
|
||||
onClick={() =>
|
||||
exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)
|
||||
}
|
||||
onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)}
|
||||
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
|
||||
exporterServices?.prev_page_results
|
||||
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
|
||||
@ -122,9 +109,7 @@ const IntegrationGuide = () => {
|
||||
</button>
|
||||
<button
|
||||
disabled={!exporterServices?.next_page_results}
|
||||
onClick={() =>
|
||||
exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)
|
||||
}
|
||||
onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)}
|
||||
className={`flex items-center border border-custom-primary-100 text-custom-primary-100 px-1 rounded ${
|
||||
exporterServices?.next_page_results
|
||||
? "cursor-pointer hover:bg-custom-primary-100 hover:text-white"
|
||||
@ -147,9 +132,7 @@ const IntegrationGuide = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200 px-4 py-6">
|
||||
No previous export available.
|
||||
</p>
|
||||
<p className="text-sm text-custom-text-200 px-4 py-6">No previous export available.</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
@ -169,9 +152,7 @@ const IntegrationGuide = () => {
|
||||
data={null}
|
||||
user={user}
|
||||
provider={provider}
|
||||
mutateServices={() =>
|
||||
mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))
|
||||
}
|
||||
mutateServices={() => mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,10 +6,7 @@ import { allViewsWithData, currentViewDataWithView } from "../data";
|
||||
|
||||
export const ChartContext = createContext<ChartContextReducer | undefined>(undefined);
|
||||
|
||||
const chartReducer = (
|
||||
state: ChartContextData,
|
||||
action: ChartContextActionPayload
|
||||
): ChartContextData => {
|
||||
const chartReducer = (state: ChartContextData, action: ChartContextActionPayload): ChartContextData => {
|
||||
switch (action.type) {
|
||||
case "CURRENT_VIEW":
|
||||
return { ...state, currentView: action.payload };
|
||||
@ -50,9 +47,7 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<ChartContext.Provider
|
||||
value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}
|
||||
>
|
||||
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
|
||||
{children}
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { KeyedMutator } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
import { IBlockUpdateData } from "../types";
|
||||
|
||||
export const updateGanttIssue = (
|
||||
issue: IIssue,
|
||||
payload: IBlockUpdateData,
|
||||
mutate: KeyedMutator<any>,
|
||||
user: ICurrentUserResponse | undefined,
|
||||
workspaceSlug: string | undefined
|
||||
) => {
|
||||
if (!issue || !workspaceSlug || !user) return;
|
||||
|
||||
mutate((prevData: any) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
const newList = prevData.map((p: any) => ({
|
||||
...p,
|
||||
...(p.id === issue.id ? payload : {}),
|
||||
}));
|
||||
|
||||
if (payload.sort_order) {
|
||||
const removedElement = newList.splice(payload.sort_order.sourceIndex, 1)[0];
|
||||
removedElement.sort_order = payload.sort_order.newSortOrder;
|
||||
newList.splice(payload.sort_order.destinationIndex, 0, removedElement);
|
||||
}
|
||||
|
||||
return newList;
|
||||
}, false);
|
||||
|
||||
const newPayload: any = { ...payload };
|
||||
|
||||
if (newPayload.sort_order && payload.sort_order)
|
||||
newPayload.sort_order = payload.sort_order.newSortOrder;
|
||||
|
||||
issuesService.patchIssue(workspaceSlug, issue.project, issue.id, newPayload, user);
|
||||
};
|
2
web/components/headers/index.ts
Normal file
2
web/components/headers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./module-issues";
|
||||
export * from "./project-issues";
|
94
web/components/headers/module-issues.tsx
Normal file
94
web/components/headers/module-issues.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||
|
||||
const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore } = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
const newValues = moduleFilterStore.userModuleFilters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (moduleFilterStore.userModuleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
|
||||
[key]: newValues,
|
||||
});
|
||||
},
|
||||
[moduleId, moduleFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={moduleFilterStore.userModuleFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
96
web/components/headers/project-issues.tsx
Normal file
96
web/components/headers/project-issues.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
|
||||
export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: TIssueLayouts) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
layout,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newValues = issueFilterStore.userFilters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilterStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
filters: {
|
||||
[key]: newValues,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleDisplayFiltersUpdate = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||
display_filters: {
|
||||
...updatedDisplayFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
[issueFilterStore, projectId, workspaceSlug]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutSelection
|
||||
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
|
||||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
<FiltersDropdown title="Filters">
|
||||
<FilterSelection
|
||||
filters={issueFilterStore.userFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="View">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilterStore.userDisplayFilters}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -20,7 +20,7 @@ export const ModuleCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="url(#clip0_4052_100277)">
|
||||
<path
|
||||
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
|
||||
fill="#ef4444"
|
||||
|
@ -16,7 +16,7 @@ export const ModulePausedIcon: React.FC<Props> = ({ width = "20", height = "20",
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100275)">
|
||||
<g clipPath="url(#clip0_4052_100275)">
|
||||
<path
|
||||
d="M6.4435 10.34C6.6145 10.34 6.75667 10.2825 6.87 10.1675C6.98333 10.0525 7.04 9.91 7.04 9.74V6.24C7.04 6.07 6.98217 5.9275 6.8665 5.8125C6.75082 5.6975 6.60749 5.64 6.4365 5.64C6.2655 5.64 6.12333 5.6975 6.01 5.8125C5.89667 5.9275 5.84 6.07 5.84 6.24V9.74C5.84 9.91 5.89783 10.0525 6.0135 10.1675C6.12918 10.2825 6.27251 10.34 6.4435 10.34ZM9.5635 10.34C9.7345 10.34 9.87667 10.2825 9.99 10.1675C10.1033 10.0525 10.16 9.91 10.16 9.74V6.24C10.16 6.07 10.1022 5.9275 9.9865 5.8125C9.87082 5.6975 9.72749 5.64 9.5565 5.64C9.3855 5.64 9.24333 5.6975 9.13 5.8125C9.01667 5.9275 8.96 6.07 8.96 6.24V9.74C8.96 9.91 9.01783 10.0525 9.1335 10.1675C9.24918 10.2825 9.39251 10.34 9.5635 10.34ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 7.54667 0.0366667 7.09993 0.11 6.6598C0.183333 6.21965 0.293333 5.78639 0.44 5.36C0.493333 5.21333 0.593333 5.11667 0.74 5.07C0.886667 5.02333 1.02667 5.04199 1.16 5.12596C1.30285 5.20993 1.40523 5.33327 1.46714 5.49596C1.52905 5.65865 1.54 5.82 1.5 5.98C1.42 6.31333 1.35 6.64765 1.29 6.98294C1.23 7.31823 1.2 7.65725 1.2 8C1.2 9.89833 1.85875 11.5063 3.17624 12.8238C4.49375 14.1413 6.10167 14.8 8 14.8C9.89833 14.8 11.5063 14.1413 12.8238 12.8238C14.1413 11.5063 14.8 9.89833 14.8 8C14.8 6.10167 14.1413 4.49375 12.8238 3.17624C11.5063 1.85875 9.89833 1.2 8 1.2C7.63235 1.2 7.26852 1.22667 6.90852 1.28C6.54852 1.33333 6.19235 1.41333 5.84 1.52C5.68 1.57333 5.52 1.56667 5.36 1.5C5.2 1.43333 5.08667 1.32667 5.02 1.18C4.95333 1.03333 4.96 0.886667 5.04 0.74C5.12 0.593333 5.23333 0.493333 5.38 0.44C5.79333 0.306667 6.21333 0.2 6.64 0.12C7.06667 0.04 7.49333 0 7.92 0C9.02667 0 10.07 0.21 11.05 0.63C12.03 1.05 12.8863 1.62 13.6189 2.34C14.3516 3.06 14.9316 3.90667 15.3589 4.88C15.7863 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM2.65764 3.62C2.37921 3.62 2.14333 3.52255 1.95 3.32764C1.75667 3.13275 1.66 2.89608 1.66 2.61764C1.66 2.33921 1.75745 2.10333 1.95236 1.91C2.14725 1.71667 2.38392 1.62 2.66236 1.62C2.94079 1.62 3.17667 1.71745 3.37 1.91236C3.56333 2.10725 3.66 2.34392 3.66 2.62236C3.66 2.90079 3.56255 3.13667 3.36764 3.33C3.17275 3.52333 2.93608 3.62 2.65764 3.62Z"
|
||||
fill="#525252"
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupBacklogIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="url(#clip0_4052_100277)">
|
||||
<path
|
||||
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
|
||||
fill={color}
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupStartedIcon: React.FC<Props> = ({
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} stroke-width="0.8" stroke-dasharray="2.4 2.4" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} strokeWidth="0.8" strokeDasharray="2.4 2.4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupUnstartedIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} strokeWidth="1.2" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import useSWR, { mutate } from "swr";
|
||||
// components
|
||||
import { AddComment, IssueActivitySection } from "components/issues";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import issuesService from "services/issue.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useToast from "hooks/use-toast";
|
||||
@ -25,16 +25,9 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||
const { user } = useUser();
|
||||
|
||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||
workspaceSlug && projectId && inboxIssueId ? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString()) : null,
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && inboxIssueId
|
||||
? () =>
|
||||
issuesService.getIssueActivities(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
inboxIssueId.toString()
|
||||
)
|
||||
? () => issuesService.getIssueActivities(workspaceSlug.toString(), projectId.toString(), inboxIssueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
@ -42,14 +35,7 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||
if (!workspaceSlug || !projectId || !inboxIssueId) return;
|
||||
|
||||
await issuesService
|
||||
.patchIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
inboxIssueId as string,
|
||||
commentId,
|
||||
data,
|
||||
user
|
||||
)
|
||||
.patchIssueComment(workspaceSlug as string, projectId as string, inboxIssueId as string, commentId, data, user)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
@ -59,13 +45,7 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||
|
||||
await issuesService
|
||||
.deleteIssueComment(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
inboxIssueId as string,
|
||||
commentId,
|
||||
user
|
||||
)
|
||||
.deleteIssueComment(workspaceSlug as string, projectId as string, inboxIssueId as string, commentId, user)
|
||||
.then(() => mutateIssueActivity());
|
||||
};
|
||||
|
||||
@ -73,13 +53,7 @@ export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||
if (!workspaceSlug || !issueDetails) return;
|
||||
|
||||
await issuesService
|
||||
.createIssueComment(
|
||||
workspaceSlug.toString(),
|
||||
issueDetails.project,
|
||||
issueDetails.id,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.createIssueComment(workspaceSlug.toString(), issueDetails.project, issueDetails.id, formData, user)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||
})
|
||||
|
@ -11,7 +11,7 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import issuesServices from "services/issues.service";
|
||||
import issuesServices from "services/issue.service";
|
||||
// ui
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
@ -39,9 +39,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () =>
|
||||
issuesServices
|
||||
@ -71,8 +69,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const filteredIssues =
|
||||
(query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? [];
|
||||
const filteredIssues = (query === "" ? issues : issues?.filter((issue) => issue.name.includes(query))) ?? [];
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setQuery("")} appear>
|
||||
@ -128,9 +125,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
{filteredIssues.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">
|
||||
Select issue
|
||||
</h2>
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-custom-text-100">Select issue</h2>
|
||||
)}
|
||||
<ul className="text-sm text-custom-text-100">
|
||||
{filteredIssues.map((issue) => (
|
||||
@ -140,9 +135,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
value={issue.id}
|
||||
className={({ active, selected }) =>
|
||||
`flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
active || selected
|
||||
? "bg-custom-background-80 text-custom-text-100"
|
||||
: ""
|
||||
active || selected ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} `
|
||||
}
|
||||
>
|
||||
@ -154,11 +147,8 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{
|
||||
issues?.find((i) => i.id === issue.id)?.project_detail
|
||||
?.identifier
|
||||
}
|
||||
-{issue.sequence_id}
|
||||
{issues?.find((i) => i.id === issue.id)?.project_detail?.identifier}-
|
||||
{issue.sequence_id}
|
||||
</span>
|
||||
<span className="text-custom-text-200">{issue.name}</span>
|
||||
</div>
|
||||
@ -171,10 +161,7 @@ export const SelectDuplicateInboxIssueModal: React.FC<Props> = (props) => {
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-sm text-custom-text-200">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1">
|
||||
C
|
||||
</pre>
|
||||
.
|
||||
<pre className="inline rounded bg-custom-background-80 px-2 py-1">C</pre>.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
@ -7,7 +7,7 @@ import { mutate } from "swr";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import IntegrationService from "services/integration";
|
||||
import IntegrationService from "services/integration.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
@ -92,10 +92,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data,
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<ExclamationTriangleIcon
|
||||
className="h-6 w-6 text-red-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-red-500" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Project</h3>
|
||||
@ -104,17 +101,13 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data,
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete import from{" "}
|
||||
<span className="break-words font-semibold capitalize text-custom-text-100">
|
||||
{data?.service}
|
||||
</span>
|
||||
? All of the data related to the import will be permanently removed. This
|
||||
action cannot be undone.
|
||||
<span className="break-words font-semibold capitalize text-custom-text-100">{data?.service}</span>
|
||||
? All of the data related to the import will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
To confirm, type{" "}
|
||||
<span className="font-medium text-custom-text-100">delete import</span> below:
|
||||
To confirm, type <span className="font-medium text-custom-text-100">delete import</span> below:
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
@ -129,11 +122,7 @@ export const DeleteImportModal: React.FC<Props> = ({ isOpen, handleClose, data,
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||
<DangerButton
|
||||
onClick={handleDeletion}
|
||||
disabled={!confirmDeleteImport}
|
||||
loading={deleteLoading}
|
||||
>
|
||||
<DangerButton onClick={handleDeletion} disabled={!confirmDeleteImport} loading={deleteLoading}>
|
||||
{deleteLoading ? "Deleting..." : "Delete Project"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||
// react-hook-form
|
||||
import { UseFormSetValue } from "react-hook-form";
|
||||
// services
|
||||
import GithubIntegrationService from "services/integration/github.service";
|
||||
import GithubIntegrationService from "services/github.service";
|
||||
// ui
|
||||
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
@ -22,19 +22,12 @@ type Props = {
|
||||
setValue: UseFormSetValue<TFormValues>;
|
||||
};
|
||||
|
||||
export const GithubRepoDetails: FC<Props> = ({
|
||||
selectedRepo,
|
||||
handleStepChange,
|
||||
setUsers,
|
||||
setValue,
|
||||
}) => {
|
||||
export const GithubRepoDetails: FC<Props> = ({ selectedRepo, handleStepChange, setUsers, setValue }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: repoInfo } = useSWR(
|
||||
workspaceSlug && selectedRepo
|
||||
? GITHUB_REPOSITORY_INFO(workspaceSlug as string, selectedRepo.name)
|
||||
: null,
|
||||
workspaceSlug && selectedRepo ? GITHUB_REPOSITORY_INFO(workspaceSlug as string, selectedRepo.name) : null,
|
||||
workspaceSlug && selectedRepo
|
||||
? () =>
|
||||
GithubIntegrationService.getGithubRepoInfo(workspaceSlug as string, {
|
||||
|
@ -9,8 +9,8 @@ import useSWR, { mutate } from "swr";
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
import IntegrationService from "services/integration";
|
||||
import GithubIntegrationService from "services/integration/github.service";
|
||||
import IntegrationService from "services/integration.service";
|
||||
import GithubIntegrationService from "services/github.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
@ -29,18 +29,9 @@ import GithubLogo from "public/services/github.png";
|
||||
// types
|
||||
import { ICurrentUserResponse, IGithubRepoCollaborator, IGithubServiceImportFormData } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
APP_INTEGRATIONS,
|
||||
IMPORTER_SERVICES_LIST,
|
||||
WORKSPACE_INTEGRATIONS,
|
||||
} from "constants/fetch-keys";
|
||||
import { APP_INTEGRATIONS, IMPORTER_SERVICES_LIST, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||
|
||||
export type TIntegrationSteps =
|
||||
| "import-configure"
|
||||
| "import-data"
|
||||
| "repo-details"
|
||||
| "import-users"
|
||||
| "import-confirm";
|
||||
export type TIntegrationSteps = "import-configure" | "import-data" | "repo-details" | "import-users" | "import-confirm";
|
||||
export interface IIntegrationData {
|
||||
state: TIntegrationSteps;
|
||||
}
|
||||
@ -108,21 +99,15 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
defaultValues: defaultFormValues,
|
||||
});
|
||||
|
||||
const { data: appIntegrations } = useSWR(APP_INTEGRATIONS, () =>
|
||||
IntegrationService.getAppIntegrationsList()
|
||||
);
|
||||
const { data: appIntegrations } = useSWR(APP_INTEGRATIONS, () => IntegrationService.getAppIntegrationsList());
|
||||
|
||||
const { data: workspaceIntegrations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||
workspaceSlug
|
||||
? () => IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
|
||||
: null
|
||||
workspaceSlug ? () => IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
const activeIntegrationState = () => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
return currentElementIndex;
|
||||
};
|
||||
@ -133,14 +118,11 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
|
||||
// current integration from all the integrations available
|
||||
const integration =
|
||||
appIntegrations &&
|
||||
appIntegrations.length > 0 &&
|
||||
appIntegrations.find((i) => i.provider === provider);
|
||||
appIntegrations && appIntegrations.length > 0 && appIntegrations.find((i) => i.provider === provider);
|
||||
|
||||
// current integration from workspace integrations
|
||||
const workspaceIntegration =
|
||||
integration &&
|
||||
workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id);
|
||||
integration && workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id);
|
||||
|
||||
const createGithubImporterService = async (formData: TFormValues) => {
|
||||
if (!formData.github || !formData.project) return;
|
||||
@ -214,9 +196,7 @@ export const GithubImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
<div
|
||||
key={index}
|
||||
className={`border-b px-7 ${
|
||||
index <= activeIntegrationState() - 1
|
||||
? `border-custom-primary`
|
||||
: `border-custom-border-200`
|
||||
index <= activeIntegrationState() - 1 ? `border-custom-primary` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
|
@ -9,14 +9,9 @@ import useSWR, { mutate } from "swr";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
// services
|
||||
import IntegrationService from "services/integration";
|
||||
import IntegrationService from "services/integration.service";
|
||||
// components
|
||||
import {
|
||||
DeleteImportModal,
|
||||
GithubImporterRoot,
|
||||
JiraImporterRoot,
|
||||
SingleImport,
|
||||
} from "components/integration";
|
||||
import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "components/integration";
|
||||
// ui
|
||||
import { Loader, PrimaryButton } from "components/ui";
|
||||
// icons
|
||||
@ -85,18 +80,11 @@ const IntegrationGuide = () => {
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="relative h-10 w-10 flex-shrink-0">
|
||||
<Image
|
||||
src={service.logo}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt={`${service.title} Logo`}
|
||||
/>
|
||||
<Image src={service.logo} layout="fill" objectFit="cover" alt={`${service.title} Logo`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="flex items-center gap-4 text-sm font-medium">{service.title}</h3>
|
||||
<p className="text-sm text-custom-text-200 tracking-tight">
|
||||
{service.description}
|
||||
</p>
|
||||
<p className="text-sm text-custom-text-200 tracking-tight">{service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
@ -119,9 +107,7 @@ const IntegrationGuide = () => {
|
||||
className="flex flex-shrink-0 items-center gap-1 rounded bg-custom-background-80 py-1 px-1.5 text-xs outline-none"
|
||||
onClick={() => {
|
||||
setRefreshing(true);
|
||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
|
||||
setRefreshing(false)
|
||||
);
|
||||
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() => setRefreshing(false));
|
||||
}}
|
||||
>
|
||||
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
|
||||
@ -145,9 +131,7 @@ const IntegrationGuide = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-custom-text-200 px-4 py-6">
|
||||
No previous imports available.
|
||||
</p>
|
||||
<p className="text-sm text-custom-text-200 px-4 py-6">No previous imports available.</p>
|
||||
)
|
||||
) : (
|
||||
<Loader className="mt-6 grid grid-cols-1 gap-3">
|
||||
|
@ -10,7 +10,7 @@ import useSWR from "swr";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
// services
|
||||
import jiraImporterService from "services/integration/jira.service";
|
||||
import jiraImporterService from "services/jira.service";
|
||||
|
||||
// fetch keys
|
||||
import { JIRA_IMPORTER_DETAIL } from "constants/fetch-keys";
|
||||
@ -157,9 +157,7 @@ export const JiraProjectDetail: React.FC<Props> = (props) => {
|
||||
<Controller
|
||||
control={control}
|
||||
name="config.epics_to_modules"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<ToggleSwitch onChange={onChange} value={value} />
|
||||
)}
|
||||
render={({ field: { value, onChange } }) => <ToggleSwitch onChange={onChange} value={value} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@ import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
|
||||
import { CogIcon, UsersIcon, CheckIcon } from "components/icons";
|
||||
|
||||
// services
|
||||
import jiraImporterService from "services/integration/jira.service";
|
||||
import jiraImporterService from "services/jira.service";
|
||||
|
||||
// fetch keys
|
||||
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
|
||||
@ -100,9 +100,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
};
|
||||
|
||||
const activeIntegrationState = () => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
return currentElementIndex;
|
||||
};
|
||||
@ -155,9 +153,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
<div
|
||||
key={index}
|
||||
className={`border-b px-7 ${
|
||||
index <= activeIntegrationState() - 1
|
||||
? `border-custom-primary`
|
||||
: `border-custom-border-200`
|
||||
index <= activeIntegrationState() - 1 ? `border-custom-primary` : `border-custom-border-200`
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
@ -174,10 +170,7 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{currentStep.state === "import-configure" && <JiraGetImportDetail />}
|
||||
{currentStep.state === "display-import-data" && (
|
||||
<JiraProjectDetail
|
||||
setDisableTopBarAfter={setDisableTopBarAfter}
|
||||
setCurrentStep={setCurrentStep}
|
||||
/>
|
||||
<JiraProjectDetail setDisableTopBarAfter={setDisableTopBarAfter} setCurrentStep={setCurrentStep} />
|
||||
)}
|
||||
{currentStep?.state === "import-users" && <JiraImportUsers />}
|
||||
{currentStep?.state === "import-confirmation" && <JiraConfirmImport />}
|
||||
@ -199,15 +192,9 @@ export const JiraImporterRoot: React.FC<Props> = ({ user }) => {
|
||||
</SecondaryButton>
|
||||
)}
|
||||
<PrimaryButton
|
||||
disabled={
|
||||
disableTopBarAfter === currentStep?.state ||
|
||||
!isValid ||
|
||||
methods.formState.isSubmitting
|
||||
}
|
||||
disabled={disableTopBarAfter === currentStep?.state || !isValid || methods.formState.isSubmitting}
|
||||
onClick={() => {
|
||||
const currentElementIndex = integrationWorkflowData.findIndex(
|
||||
(i) => i?.key === currentStep?.state
|
||||
);
|
||||
const currentElementIndex = integrationWorkflowData.findIndex((i) => i?.key === currentStep?.state);
|
||||
|
||||
if (currentElementIndex === integrationWorkflowData.length - 1) {
|
||||
methods.handleSubmit(onSubmit)();
|
||||
|
@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import IntegrationService from "services/integration";
|
||||
import IntegrationService from "services/integration.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useIntegrationPopup from "hooks/use-integration-popup";
|
||||
@ -50,25 +50,17 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
|
||||
|
||||
const { data: workspaceIntegrations } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||
() =>
|
||||
workspaceSlug
|
||||
? IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string)
|
||||
: null
|
||||
() => (workspaceSlug ? IntegrationService.getWorkspaceIntegrationsList(workspaceSlug as string) : null)
|
||||
);
|
||||
|
||||
const handleRemoveIntegration = async () => {
|
||||
if (!workspaceSlug || !integration || !workspaceIntegrations) return;
|
||||
|
||||
const workspaceIntegrationId = workspaceIntegrations?.find(
|
||||
(i) => i.integration === integration.id
|
||||
)?.id;
|
||||
const workspaceIntegrationId = workspaceIntegrations?.find((i) => i.integration === integration.id)?.id;
|
||||
|
||||
setDeletingIntegration(true);
|
||||
|
||||
await IntegrationService.deleteWorkspaceIntegration(
|
||||
workspaceSlug as string,
|
||||
workspaceIntegrationId ?? ""
|
||||
)
|
||||
await IntegrationService.deleteWorkspaceIntegration(workspaceSlug as string, workspaceIntegrationId ?? "")
|
||||
.then(() => {
|
||||
mutate<IWorkspaceIntegration[]>(
|
||||
WORKSPACE_INTEGRATIONS(workspaceSlug as string),
|
||||
@ -94,18 +86,13 @@ export const SingleIntegrationCard: React.FC<Props> = ({ integration }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isInstalled = workspaceIntegrations?.find(
|
||||
(i: any) => i.integration_detail.id === integration.id
|
||||
);
|
||||
const isInstalled = workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 border-b border-custom-border-200 bg-custom-background-100 px-4 py-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<Image
|
||||
src={integrationDetails[integration.provider].logo}
|
||||
alt={`${integration.title} Logo`}
|
||||
/>
|
||||
<Image src={integrationDetails[integration.provider].logo} alt={`${integration.title} Logo`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="flex items-center gap-2 text-sm font-medium">
|
||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
// services
|
||||
import appinstallationsService from "services/app-installations.service";
|
||||
import appinstallationsService from "services/app_installation.service";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
// hooks
|
||||
@ -20,8 +20,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
||||
const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] =
|
||||
useState<boolean>(false);
|
||||
const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] = useState<boolean>(false);
|
||||
const [slackChannel, setSlackChannel] = useState<ISlackIntegration | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
@ -65,12 +64,7 @@ export const SelectChannel: React.FC<Props> = ({ integration }) => {
|
||||
setSlackChannel(null);
|
||||
});
|
||||
appinstallationsService
|
||||
.removeSlackChannel(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
integration.id as string,
|
||||
slackChannel?.id
|
||||
)
|
||||
.removeSlackChannel(workspaceSlug as string, projectId as string, integration.id as string, slackChannel?.id)
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
|
6
web/components/issue-layouts/calendar/index.tsx
Normal file
6
web/components/issue-layouts/calendar/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const IssueCalendarViewRoot = () => {
|
||||
console.log();
|
||||
return <div>IssueCalendarViewRoot</div>;
|
||||
};
|
73
web/components/issue-layouts/filters-preview/assignees.tsx
Normal file
73
web/components/issue-layouts/filters-preview/assignees.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const MemberIcons = ({ display_name, avatar }: { display_name: string; avatar: string | null }) => (
|
||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[16px] h-[16px] flex justify-center items-center">
|
||||
{avatar ? (
|
||||
<img src={avatar} alt={display_name || ""} className="" />
|
||||
) : (
|
||||
<div className="text-xs w-full h-full flex justify-center items-center capitalize font-medium bg-gray-700 text-white">
|
||||
{(display_name ?? "U")[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FilterAssignees = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "assignees", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.assignees != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader
|
||||
title={`Assignees (${issueFilterStore?.userFilters?.filters?.assignees?.length || 0})`}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.projectMembers &&
|
||||
issueFilterStore?.projectMembers.length > 0 &&
|
||||
issueFilterStore?.projectMembers.map(
|
||||
(_member) =>
|
||||
issueFilterStore?.userFilters?.filters?.assignees != null &&
|
||||
issueFilterStore?.userFilters?.filters?.assignees.includes(_member?.member?.id) && (
|
||||
<FilterPreviewContent
|
||||
key={`assignees-${_member?.member?.id}`}
|
||||
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
|
||||
title={`${_member?.member?.display_name}`}
|
||||
onClick={() => handleFilter("assignees", _member?.member?.id)}
|
||||
className="border border-custom-border-100 bg-custom-background-100"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
63
web/components/issue-layouts/filters-preview/created-by.tsx
Normal file
63
web/components/issue-layouts/filters-preview/created-by.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { MemberIcons } from "./assignees";
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const FilterCreatedBy = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "created_by", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.created_by != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader
|
||||
title={`Created By (${issueFilterStore?.userFilters?.filters?.created_by?.length || 0})`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.projectMembers &&
|
||||
issueFilterStore?.projectMembers.length > 0 &&
|
||||
issueFilterStore?.projectMembers.map(
|
||||
(_member) =>
|
||||
issueFilterStore?.userFilters?.filters?.created_by != null &&
|
||||
issueFilterStore?.userFilters?.filters?.created_by.includes(_member?.member?.id) && (
|
||||
<FilterPreviewContent
|
||||
key={`create-by-${_member?.member?.id}`}
|
||||
title={`${_member?.member?.display_name}`}
|
||||
icon={<MemberIcons display_name={_member?.member.display_name} avatar={_member?.member.avatar} />}
|
||||
onClick={() => handleFilter("created_by", _member?.member?.id)}
|
||||
className="border border-custom-border-100 bg-custom-background-100"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
// lucide icons
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface IFilterPreviewClear {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const FilterPreviewClear = ({ onClick }: IFilterPreviewClear) => (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
>
|
||||
<X width={12} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
@ -0,0 +1,26 @@
|
||||
import { FilterPreviewClear } from "./clear";
|
||||
|
||||
interface IFilterPreviewContent {
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export const FilterPreviewContent = ({ icon, title, onClick, className, style }: IFilterPreviewContent) => (
|
||||
<div
|
||||
className={`flex-shrink-0 flex items-center gap-1.5 rounded-full px-[8px] transition-all ${className}`}
|
||||
style={style ? style : {}}
|
||||
>
|
||||
<div className="flex-shrink-0">{icon}</div>
|
||||
<div className="text-xs w-full whitespace-nowrap font-medium">{title}</div>
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear
|
||||
onClick={() => {
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -0,0 +1,12 @@
|
||||
interface IFilterPreviewHeader {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const FilterPreviewHeader = ({ title }: IFilterPreviewHeader) => {
|
||||
console.log();
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-gray-500 text-xs text-custom-text-300 font-medium">{title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
69
web/components/issue-layouts/filters-preview/index.tsx
Normal file
69
web/components/issue-layouts/filters-preview/index.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { FilterPriority } from "./priority";
|
||||
import { FilterState } from "./state";
|
||||
import { FilterStateGroup } from "./state-group";
|
||||
import { FilterAssignees } from "./assignees";
|
||||
import { FilterCreatedBy } from "./created-by";
|
||||
import { FilterLabels } from "./labels";
|
||||
import { FilterStartDate } from "./start-date";
|
||||
import { FilterTargetDate } from "./target-date";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// default data
|
||||
// import { issueFilterVisibilityData } from "store/helpers/issue-data";
|
||||
|
||||
export const FilterPreview = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilter: issueFilterStore } = store;
|
||||
|
||||
const handleFilterSectionVisibility = (section_key: string) => {
|
||||
// issueFilterStore?.issueView &&
|
||||
// issueFilterStore?.issueLayout &&
|
||||
// issueFilterVisibilityData[issueFilterStore?.issueView === "my_issues" ? "my_issues" : "issues"]?.filters?.[
|
||||
// issueFilterStore?.issueLayout
|
||||
// ]?.includes(section_key);
|
||||
};
|
||||
|
||||
const validateFiltersAvailability =
|
||||
issueFilterStore?.userFilters?.filters != null &&
|
||||
Object.keys(issueFilterStore?.userFilters?.filters).length > 0 &&
|
||||
Object.keys(issueFilterStore?.userFilters?.filters)
|
||||
.map((key) => issueFilterStore?.userFilters?.filters?.[key]?.length)
|
||||
.filter((v) => v != undefined || v != null).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{validateFiltersAvailability && (
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto relative max-h-[500px] flex flex-wrap p-2 border-b border-custom-border-80 shadow-sm">
|
||||
{/* priority */}
|
||||
{handleFilterSectionVisibility("priority") && <FilterPriority />}
|
||||
|
||||
{/* state group */}
|
||||
{handleFilterSectionVisibility("state_group") && <FilterStateGroup />}
|
||||
|
||||
{/* state */}
|
||||
{handleFilterSectionVisibility("state") && <FilterState />}
|
||||
|
||||
{/* assignees */}
|
||||
{handleFilterSectionVisibility("assignees") && <FilterAssignees />}
|
||||
|
||||
{/* created_by */}
|
||||
{handleFilterSectionVisibility("created_by") && <FilterCreatedBy />}
|
||||
|
||||
{/* labels */}
|
||||
{handleFilterSectionVisibility("labels") && <FilterLabels />}
|
||||
|
||||
{/* start_date */}
|
||||
{handleFilterSectionVisibility("start_date") && <FilterStartDate />}
|
||||
|
||||
{/* due_date */}
|
||||
{handleFilterSectionVisibility("due_date") && <FilterTargetDate />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
73
web/components/issue-layouts/filters-preview/labels.tsx
Normal file
73
web/components/issue-layouts/filters-preview/labels.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const LabelIcons = ({ color }: { color: string }) => (
|
||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
||||
<div className={`w-[12px] h-[12px] rounded-full`} style={{ backgroundColor: color }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FilterLabels = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const stateStyles = (color: any) => ({ color: color, backgroundColor: `${color}20` });
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "labels", null);
|
||||
};
|
||||
|
||||
const handleLabels =
|
||||
issueFilterStore.issueView && issueFilterStore.issueView === "my_issues"
|
||||
? issueFilterStore?.workspaceLabels
|
||||
: issueFilterStore?.projectLabels;
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.labels != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader title={`Labels (${issueFilterStore?.userFilters?.filters?.labels?.length || 0})`} />
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{handleLabels &&
|
||||
handleLabels.length > 0 &&
|
||||
handleLabels.map(
|
||||
(_label) =>
|
||||
issueFilterStore?.userFilters?.filters?.labels != null &&
|
||||
issueFilterStore?.userFilters?.filters?.labels.includes(_label?.id) && (
|
||||
<FilterPreviewContent
|
||||
key={_label?.id}
|
||||
onClick={() => handleFilter("labels", _label?.id)}
|
||||
icon={<LabelIcons color={_label.color} />}
|
||||
title={_label.name}
|
||||
style={stateStyles(_label.color)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
79
web/components/issue-layouts/filters-preview/priority.tsx
Normal file
79
web/components/issue-layouts/filters-preview/priority.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
// lucide icons
|
||||
import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
const PriorityIcons = ({ priority }: { priority: string }) => {
|
||||
if (priority === "urgent") return <AlertCircle size={12} strokeWidth={2} />;
|
||||
if (priority === "high") return <SignalHigh size={12} strokeWidth={4} />;
|
||||
if (priority === "medium") return <SignalMedium size={12} strokeWidth={4} />;
|
||||
if (priority === "low") return <SignalLow size={12} strokeWidth={4} />;
|
||||
return <Ban size={12} strokeWidth={2} />;
|
||||
};
|
||||
|
||||
const classNamesStyling = (priority: string) => {
|
||||
if (priority == "urgent") return "bg-red-500/20 text-red-500";
|
||||
if (priority == "high") return "bg-orange-500/20 text-orange-500 !-pt-[30px]";
|
||||
if (priority == "medium") return "bg-orange-500/20 text-orange-500 -pt-2";
|
||||
if (priority == "low") return "bg-green-500/20 text-green-500 -pt-2";
|
||||
return "bg-gray-500/10 text-gray-500";
|
||||
};
|
||||
|
||||
export const FilterPriority = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "priority", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.priority != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader
|
||||
title={`Priority (${issueFilterStore?.userFilters?.filters?.priority?.length || 0})`}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.issueRenderFilters?.priority &&
|
||||
issueFilterStore?.issueRenderFilters?.priority.length > 0 &&
|
||||
issueFilterStore?.issueRenderFilters?.priority.map(
|
||||
(_priority) =>
|
||||
issueFilterStore?.userFilters?.filters?.priority != null &&
|
||||
issueFilterStore?.userFilters?.filters?.priority.includes(_priority?.key) && (
|
||||
<FilterPreviewContent
|
||||
key={_priority?.key}
|
||||
icon={<PriorityIcons priority={_priority.key} />}
|
||||
title={_priority.title}
|
||||
className={classNamesStyling(_priority?.key)}
|
||||
onClick={() => handleFilter("priority", _priority?.key)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
56
web/components/issue-layouts/filters-preview/start-date.tsx
Normal file
56
web/components/issue-layouts/filters-preview/start-date.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const FilterStartDate = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "start_date", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.start_date != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader title={`Start Date`} />
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.issueRenderFilters?.start_date &&
|
||||
issueFilterStore?.issueRenderFilters?.start_date.length > 0 &&
|
||||
issueFilterStore?.issueRenderFilters?.start_date.map((_startDate) => (
|
||||
<FilterPreviewContent
|
||||
key={_startDate?.key}
|
||||
title={_startDate.title}
|
||||
className="border border-custom-border-100 bg-custom-background-100"
|
||||
onClick={() => handleFilter("start_date", _startDate?.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
129
web/components/issue-layouts/filters-preview/state-group.tsx
Normal file
129
web/components/issue-layouts/filters-preview/state-group.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import {
|
||||
StateGroupBacklogIcon,
|
||||
StateGroupCancelledIcon,
|
||||
StateGroupCompletedIcon,
|
||||
StateGroupStartedIcon,
|
||||
StateGroupUnstartedIcon,
|
||||
} from "components/icons";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
export const StateGroupIcons = ({ stateGroup, color = null }: { stateGroup: string; color?: string | null }) => {
|
||||
if (stateGroup === "cancelled")
|
||||
return (
|
||||
<StateGroupCancelledIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
||||
);
|
||||
if (stateGroup === "completed")
|
||||
return (
|
||||
<StateGroupCompletedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
||||
);
|
||||
if (stateGroup === "started")
|
||||
return (
|
||||
<StateGroupStartedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
||||
);
|
||||
if (stateGroup === "unstarted")
|
||||
return (
|
||||
<StateGroupUnstartedIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
||||
);
|
||||
if (stateGroup === "backlog")
|
||||
return (
|
||||
<div className="flex-shrink-0 rounded-sm overflow-hidden w-[20px] h-[20px] flex justify-center items-center">
|
||||
<StateGroupBacklogIcon width={"12px"} height={"12px"} color={color ? color : STATE_GROUP_COLORS[stateGroup]} />
|
||||
</div>
|
||||
);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const stateStyles = (stateGroup: string, color: any) => {
|
||||
if (stateGroup === "cancelled") {
|
||||
return {
|
||||
color: color ? color : STATE_GROUP_COLORS[stateGroup],
|
||||
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
|
||||
};
|
||||
}
|
||||
if (stateGroup === "completed") {
|
||||
return {
|
||||
color: color ? color : STATE_GROUP_COLORS[stateGroup],
|
||||
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
|
||||
};
|
||||
}
|
||||
if (stateGroup === "started") {
|
||||
return {
|
||||
color: color ? color : STATE_GROUP_COLORS[stateGroup],
|
||||
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
|
||||
};
|
||||
}
|
||||
if (stateGroup === "unstarted") {
|
||||
return {
|
||||
color: color ? color : STATE_GROUP_COLORS[stateGroup],
|
||||
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
|
||||
};
|
||||
}
|
||||
if (stateGroup === "backlog") {
|
||||
return {
|
||||
color: color ? color : STATE_GROUP_COLORS[stateGroup],
|
||||
backgroundColor: `${color ? color : STATE_GROUP_COLORS[stateGroup]}20`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const FilterStateGroup = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "state_group", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.state_group != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader
|
||||
title={`State Group (${issueFilterStore?.userFilters?.filters?.state_group?.length || 0})`}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.issueRenderFilters?.state_group &&
|
||||
issueFilterStore?.issueRenderFilters?.state_group.length > 0 &&
|
||||
issueFilterStore?.issueRenderFilters?.state_group.map(
|
||||
(_stateGroup) =>
|
||||
issueFilterStore?.userFilters?.filters?.state_group != null &&
|
||||
issueFilterStore?.userFilters?.filters?.state_group.includes(_stateGroup?.key) && (
|
||||
<FilterPreviewContent
|
||||
key={_stateGroup?.key}
|
||||
icon={<StateGroupIcons stateGroup={_stateGroup.key} />}
|
||||
title={_stateGroup.title}
|
||||
style={stateStyles(_stateGroup?.key, null)}
|
||||
onClick={() => handleFilter("state_group", _stateGroup?.key)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
66
web/components/issue-layouts/filters-preview/state.tsx
Normal file
66
web/components/issue-layouts/filters-preview/state.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { StateGroupIcons, stateStyles } from "./state-group";
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
// store default data
|
||||
import { stateGroups } from "store/helpers/issue-data";
|
||||
|
||||
export const FilterState = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "state", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.state != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader title={`State (${issueFilterStore?.userFilters?.filters?.state?.length || 0})`} />
|
||||
</div>
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{stateGroups.map(
|
||||
(_stateGroup) =>
|
||||
issueFilterStore?.projectStates &&
|
||||
issueFilterStore?.projectStates[_stateGroup?.key] &&
|
||||
issueFilterStore?.projectStates[_stateGroup?.key].length > 0 &&
|
||||
issueFilterStore?.projectStates[_stateGroup?.key].map(
|
||||
(_state: any) =>
|
||||
issueFilterStore?.userFilters?.filters?.state != null &&
|
||||
issueFilterStore?.userFilters?.filters?.state.includes(_state?.id) && (
|
||||
<FilterPreviewContent
|
||||
key={_state?.id}
|
||||
icon={<StateGroupIcons stateGroup={_stateGroup?.key} color={_state?.color} />}
|
||||
title={_state?.name}
|
||||
style={stateStyles(_state?.group, _state?.color)}
|
||||
onClick={() => handleFilter("state", _state?.id)}
|
||||
/>
|
||||
)
|
||||
)
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
56
web/components/issue-layouts/filters-preview/target-date.tsx
Normal file
56
web/components/issue-layouts/filters-preview/target-date.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
// components
|
||||
import { FilterPreviewHeader } from "./helpers/header";
|
||||
import { FilterPreviewContent } from "./helpers/content";
|
||||
import { FilterPreviewClear } from "./helpers/clear";
|
||||
// mobx react lite
|
||||
import { observer } from "mobx-react-lite";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export const FilterTargetDate = observer(() => {
|
||||
const store: RootStore = useMobxStore();
|
||||
const { issueFilters: issueFilterStore } = store;
|
||||
|
||||
const handleFilter = (key: string, value: string) => {
|
||||
let _value =
|
||||
issueFilterStore?.userFilters?.filters?.[key] != null &&
|
||||
issueFilterStore?.userFilters?.filters?.[key].filter((p: string) => p != value);
|
||||
_value = _value && _value.length > 0 ? _value : null;
|
||||
|
||||
issueFilterStore.handleUserFilter("filters", key, _value);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
issueFilterStore.handleUserFilter("filters", "target_date", null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueFilterStore?.userFilters?.filters?.target_date != null && (
|
||||
<div className="border border-custom-border-200 bg-custom-background-80 rounded-full overflow-hidden flex items-center gap-2 px-2 py-1">
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewHeader title={`Target Date`} />
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center flex-wrap gap-2">
|
||||
{issueFilterStore?.issueRenderFilters?.due_date &&
|
||||
issueFilterStore?.issueRenderFilters?.due_date.length > 0 &&
|
||||
issueFilterStore?.issueRenderFilters?.due_date.map((_targetDate) => (
|
||||
<FilterPreviewContent
|
||||
key={_targetDate?.key}
|
||||
title={_targetDate.title}
|
||||
className="border border-custom-border-100 bg-custom-background-100"
|
||||
onClick={() => handleFilter("target_date", _targetDate?.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<FilterPreviewClear onClick={clearFilter} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user