Compare commits

...

86 Commits

Author SHA1 Message Date
dakshesh14
b49b1bf8fa refactor: divided modules into multiple components 2023-10-04 19:24:24 +05:30
guru_sainath
547a265169
chore: issue list layout (#2367) 2023-10-04 15:29:56 +05:30
Aaryan Khandelwal
0f47762e6d
dev: setup module and module filter store (#2364)
* dev: implement module issues using mobx store

* dev: module filter store setup

* chore: module store crud operations
2023-10-04 15:21:40 +05:30
gurusainath
844a3e4b42 chore: filters import conflict 2023-10-04 14:46:26 +05:30
guru_sainath
b5b809500d
chore: issue properties for list and kanban layouts and implemented estimates in project store (#2363)
* chore: issue properties for state, priorit, labels and members

* feat: implemented assignee, labels properties

* fix: implemented estimates in project store and issue properties

* chore: staer_date and due_date and validation properties in kanban
2023-10-04 14:38:49 +05:30
Aaryan Khandelwal
7be038ac5a
refactor: filter components (#2359)
* fix: calendar layout dividers

* refactor: filter selection components

* fix: dropdown closing after selection

* refactor: filters components
2023-10-04 12:04:55 +05:30
sriramveeraghanta
41fd9ce6e8 fix: layout fixes 2023-10-03 00:33:03 +05:30
sriramveeraghanta
b1448c947e fix: cycles list rendering fixes 2023-10-02 23:20:14 +05:30
sriram veeraghanta
9c2ea8a7ae fix: cycles views list and board 2023-10-02 20:33:28 +05:30
sriram veeraghanta
a39aa80e76 Merge branch 'fix/issues-layout-mobx' of github.com:makeplane/plane into fix/issues-layout-mobx 2023-10-02 16:55:09 +05:30
Aaryan Khandelwal
569a6c3383
dev: applied filters list implementation using MobX (#2325)
* dev: applied filters list UI

* fix: filter item height

* chore: remove unnecessary classes

* fix: params generator
2023-10-02 12:41:40 +05:30
sriram veeraghanta
405a398c6b feat: adding additional ui components 2023-09-29 17:33:17 +05:30
sriram veeraghanta
f22705846d chore: refactoring cycles list 2023-09-29 17:32:47 +05:30
Aaryan Khandelwal
727042468a
dev: spreadsheet layout implementation using MobX (#2306)
* dev: implement spreadsheet view using mobx

* refactor: remove console logs and props
2023-09-29 16:14:47 +05:30
Aaryan Khandelwal
9ad1e73666
dev: gantt chart implementation using MobX (#2302)
* dev: fetch project gantt issues using mobx

* chore: handle group by options in the kanban layout
2023-09-29 15:00:51 +05:30
Aaryan Khandelwal
479c145b02
refactor: filter components, constants and helper functions (#2297)
* refactor: filters and display filters to accept handlers as props

* refactor: filters and display filters folder structure

* refactor: change issue layout options constant structure

* chore: display filters validations

* chore: view less filters functionality

* fix: display filters validation

* refactor: wrap functions around useCallback

* chore: start and target date filter options added

* refactor: query params generator function

* fix: query params generator function
2023-09-29 13:09:38 +05:30
guru_sainath
b70047b1d5
chore: issues grouped kanban and swimlanes UI and functionality (#2294)
* chore: updated the all the group_by and sub_group_by UI and functionality render in kanban

* chore: kanban sorting in mobx and ui updates

* chore: ui changes and drag and drop functionality changes in kanban

* chore: issues count render in kanban default and swimlanes

* chore: Added icons to the group_by and sub_group_by in kanban and swimlanes
2023-09-29 12:30:54 +05:30
sriram veeraghanta
f60dcdc599 Merge branch 'fix/issues-layout-mobx' of github.com:makeplane/plane into fix/issues-layout-mobx 2023-09-28 20:31:00 +05:30
sriram veeraghanta
2643de80af cycles changes 2023-09-28 20:30:48 +05:30
gurusainath
5af753f475 chore: removed demo m-store routes 2023-09-28 17:18:50 +05:30
Aaryan Khandelwal
3bf590b67e
dev: calendar view layout revamp (#2293)
* dev: calendar view init

* chore: new render logic

* chore: implement calendar view

* chore: calendar view

* refactor: calendar payload

* chore: remove active month logic from backend

* chore: setup new store for calendar

* refactor: issues fetching structure

* chore: months dropdown

* chore: modify request query params for calendar layout

* refactor: remove console logs and add comments
2023-09-28 15:16:24 +05:30
sriram veeraghanta
b2d17e6ec9 fix: minor ui fixes 2023-09-28 13:28:24 +05:30
sriramveeraghanta
ccf6bd4e32 Merge branch 'fix/issues-layout-mobx' of github.com:makeplane/plane into fix/issues-layout-mobx 2023-09-27 16:00:46 +05:30
sriramveeraghanta
151662a442 fix: ui package setup 2023-09-27 16:00:17 +05:30
sriramveeraghanta
c342ab302e fix: ui package setup and project update form refactor 2023-09-27 15:59:37 +05:30
gurusainath
c48cd3ee6e chore: added sub_group_by in params and handled sub-group-by render error in display filter's 2023-09-26 14:43:36 +05:30
Aaryan Khandelwal
7c0c0da0f8 Merge branch 'fix/issues-layout-mobx' of https://github.com/makeplane/plane into fix/issues-layout-mobx 2023-09-26 14:27:34 +05:30
Aaryan Khandelwal
1b8d58a9a6 fix: computed filters logic 2023-09-26 14:27:16 +05:30
guru_sainath
43404bfcdf
Implemented swimlanes and kanban view (#2262)
* chore: issue store for kanban and calendar

* chore: updated ui for kanba and swimlanes

* chore: yarn.lock updated
2023-09-26 13:18:42 +05:30
sriramveeraghanta
310a2ca904 refactor: project card component refactor 2023-09-26 01:03:36 +05:30
sriramveeraghanta
2b419c02a5 fix: leave project fixes 2023-09-25 20:09:39 +05:30
Aaryan Khandelwal
9831418a11
chore: filters dropdown (#2260)
* chore: project issues topbar

* style: theming and minor UI fixes

* refactor: file structure

* chore: layout wise authorization added

* style: filter dropdowns

* chore: add fetch keys

* feat: search option for filters

* fix: sticky headers

* chore: sub_group_by section added
2023-09-25 19:17:40 +05:30
sriram veeraghanta
9a8dcc349f chore: minor fixes 2023-09-25 17:43:55 +05:30
Aaryan Khandelwal
27f78dd283
feat: project issues topbar (#2256)
* chore: project issues topbar

* style: theming and minor UI fixes

* refactor: file structure

* chore: layout wise authorization added

* style: filter dropdowns

* chore: add fetch keys
2023-09-25 13:24:23 +05:30
sriram veeraghanta
0ebe36bdb3 workspace project fixes 2023-09-25 12:35:42 +05:30
sriram veeraghanta
6a430ed480 chore: minor fixes 2023-09-22 12:29:22 +05:30
Aaryan Khandelwal
daa3094911
chore: update issue detail store to handle peek overview (#2237)
* chore: dynamic position dropdown (#2138)

* chore: dynamic position state dropdown for issue view

* style: state select dropdown styling

* fix: state icon attribute names

* chore: state select dynamic dropdown

* chore: member select dynamic dropdown

* chore: label select dynamic dropdown

* chore: priority select dynamic dropdown

* chore: label select dropdown improvement

* refactor: state dropdown location

* chore: dropdown improvement and code refactor

* chore: dynamic dropdown hook type added

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: fields not getting selected in the create issue form (#2212)

* fix: hydration error and draft issue workflow

* fix: build error

* fix: properties getting de-selected after create, module & cycle not getting auto-select on the form

* fix: display layout, props being updated directly

* chore: sub issues count in individual issue (#2221)

* Implemented nested issues in the sub issues section in issue detail page (#2233)

* feat: subissues infinte level

* feat: updated UI for sub issues

* feat: subissues new ui and nested sub issues in issue detail

* chore: removed repeated code

* refactor: product updates modal layout (#2225)

* fix: handle no issues in custom analytics (#2226)

* fix: activity label color (#2227)

* fix: profile issues layout switch (#2228)

* chore: update service imports

* chore: update issue detail store to handle peek overview

---------

Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: guru_sainath <gurusainath007@gmail.com>
2023-09-21 17:50:43 +05:30
sriram veeraghanta
9b41b5baf5 chore: store fixes 2023-09-21 16:54:11 +05:30
Aaryan Khandelwal
2dcaccd4ec
fix: merge conflicts (#2231)
* chore: dynamic position dropdown (#2138)

* chore: dynamic position state dropdown for issue view

* style: state select dropdown styling

* fix: state icon attribute names

* chore: state select dynamic dropdown

* chore: member select dynamic dropdown

* chore: label select dynamic dropdown

* chore: priority select dynamic dropdown

* chore: label select dropdown improvement

* refactor: state dropdown location

* chore: dropdown improvement and code refactor

* chore: dynamic dropdown hook type added

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>

* fix: fields not getting selected in the create issue form (#2212)

* fix: hydration error and draft issue workflow

* fix: build error

* fix: properties getting de-selected after create, module & cycle not getting auto-select on the form

* fix: display layout, props being updated directly

* chore: sub issues count in individual issue (#2221)

* fix: service imports

* chore: rename csv service file

---------

Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2023-09-21 15:00:57 +05:30
sriram veeraghanta
f69d34698a chore: store setup for build fixes 2023-09-21 15:00:19 +05:30
sriramveeraghanta
6d52801ea7 chore: store fixes and static data setup 2023-09-20 23:39:55 +05:30
sriram veeraghanta
e96bc77215 chore: fixing up store 2023-09-20 20:34:29 +05:30
sriram veeraghanta
a328c530d0 chore: store setup 2023-09-20 20:33:25 +05:30
gurusainath
908f6716fe user filter 2023-09-20 12:40:22 +05:30
gurusainath
50c330db65 conflicts 2023-09-20 12:23:38 +05:30
gurusainath
491592614d chore: store setup 2023-09-20 12:22:48 +05:30
gurusainath
906caa636b chore: handled build issues 2023-09-19 12:50:27 +05:30
gurusainath
12ce6e78e2 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-19 11:08:49 +05:30
gurusainath
a25e5accd1 chore: store setup 2023-09-15 20:07:38 +05:30
sriram veeraghanta
12b6ec4b49
Merge pull request #2197 from makeplane/fix/list-sorting
Fix/list sorting
2023-09-15 16:56:16 +05:30
sriram veeraghanta
70fe830151 filtering 2023-09-15 16:55:38 +05:30
gurusainath
e9490314cc Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-15 16:20:29 +05:30
gurusainath
f6d4ac95ed Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-15 15:40:24 +05:30
sriram veeraghanta
cc9ebc58bc
Merge pull request #2195 from makeplane/fix/list-sorting
Implementing list view
2023-09-15 15:39:46 +05:30
sriram veeraghanta
cf34d4afbc merge conflicts resolved 2023-09-15 15:39:18 +05:30
gurusainath
7b04adf03a chore: kanban drag drop logic 2023-09-15 15:37:54 +05:30
sriram veeraghanta
9136258926 Implementing list view 2023-09-15 15:37:47 +05:30
gurusainath
d88a0885d5 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-15 11:16:36 +05:30
gurusainath
fce6907465 chore: filter empty state handling in issue filter selection 2023-09-14 22:34:44 +05:30
gurusainath
3c9e62d308 chore: filter render UI and Functionality implementation 2023-09-14 22:28:42 +05:30
gurusainath
28ce96aaca chore: renamed gantt key to gantt_chart 2023-09-14 17:26:00 +05:30
gurusainath
66022ea478 chore: type check 2023-09-14 16:54:18 +05:30
gurusainath
60883baea7 chore: clean up and resolved import warnings 2023-09-14 16:09:43 +05:30
gurusainath
c67f08fca4 chore: filter, layout, display filters, extra filters and display properties render validation 2023-09-14 16:03:35 +05:30
gurusainath
f579712092 chore: merge conflict for lucide icons resolved 2023-09-14 14:42:47 +05:30
gurusainath
3ffbb6ac17 chore: updating filters, display_filter and display properties 2023-09-14 14:41:41 +05:30
gurusainath
0ec0ad6aba chore: implemented filters and views in kanaban 2023-09-13 19:40:35 +05:30
gurusainath
698021ab8b chore: handled single and multi select in filter cards 2023-09-13 02:02:45 +05:30
gurusainath
ada1bc009b Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-12 23:18:08 +05:30
gurusainath
834e672245 chore: UI theming updates 2023-09-12 23:17:47 +05:30
gurusainath
3b85444e1f chore: implemented filters for issues 2023-09-12 23:05:59 +05:30
gurusainath
04242800c9 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-12 22:03:39 +05:30
gurusainath
a8c5a4155b Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-12 19:16:00 +05:30
gurusainath
0445c610bf chore: created filters and updated the issue filters, display_filter and display_properties in mobx and components 2023-09-12 19:15:36 +05:30
gurusainath
c0e3c81a9b Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-11 11:17:14 +05:30
gurusainath
8c04e770c0 chore: resolved build error 2023-09-08 13:06:02 +05:30
gurusainath
aef71fbc45 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-08 12:42:29 +05:30
gurusainath
b9a6a00470 chore: updated the store for issues and issue filters 2023-09-08 12:42:09 +05:30
gurusainath
7c5936e463 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanban-sorting 2023-09-07 11:22:02 +05:30
gurusainath
b86c30baed chore: updated yarn lock 2023-09-06 11:17:34 +05:30
gurusainath
15ef2bc030 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanaban-sorting 2023-09-06 11:16:41 +05:30
gurusainath
ef630ef663 chore: Implemented new kanaban board UX and implemented draggable using react beautiful dnd 2023-09-05 23:37:52 +05:30
gurusainath
731309a932 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanaban-sorting 2023-09-05 11:34:17 +05:30
gurusainath
9d334cf3a3 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanaban-sorting 2023-09-04 17:26:34 +05:30
gurusainath
e9b6f86882 Merge branch 'develop' of gurusainath:makeplane/plane into fix/kanaban-sorting 2023-09-04 17:14:52 +05:30
gurusainath
8d86087fee chore: kanban refactoring 2023-09-04 13:07:55 +05:30
445 changed files with 19732 additions and 7916 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import * as React from "react";
export const Button = () => {
return <button>button</button>;
};

View File

@ -0,0 +1,2 @@
export * from "./input";
export * from "./textarea";

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

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

View File

@ -0,0 +1,3 @@
export * from "./buttons";
export * from "./form-fields";
export * from "./progress";

View File

@ -0,0 +1 @@
export * from "./radial-progress";

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

View File

@ -1,5 +1,8 @@
{
"extends": "tsconfig/react-library.json",
"compilerOptions": {
"jsx": "react"
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -1,5 +1,5 @@
{
"printWidth": 100,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
// components
import { SingleSpreadsheetIssue } from "components/core";

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export * from "./module-issues";
export * from "./project-issues";

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
import React from "react";
export const IssueCalendarViewRoot = () => {
console.log();
return <div>IssueCalendarViewRoot</div>;
};

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

View 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