reverted back types and fixed tailwind

This commit is contained in:
Palanikannan1437 2023-09-22 13:54:52 +05:30
parent 448d433876
commit f0e246ccfd
22 changed files with 889 additions and 704 deletions

View File

@ -1,361 +1,3 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.text-1\.5xl {
font-size: 1.375rem;
line-height: 1.875rem;
}
.text-2\.5xl {
font-size: 1.75rem;
line-height: 2.25rem;
}
}
@layer base {
html {
font-family: "Inter", sans-serif;
}
:root {
color-scheme: light !important;
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
--color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06),
0px 1px 2px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.14);
--color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
--color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
--color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
--color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
--color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
--color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
--color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
0px 12px 24px 0px rgba(16, 24, 40, 0.12), 0px 1px 32px 0px rgba(16, 24, 40, 0.12);
--color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
--color-sidebar-shadow-xs: var(--color-shadow-xs);
--color-sidebar-shadow-sm: var(--color-shadow-sm);
--color-sidebar-shadow-rg: var(--color-shadow-rg);
--color-sidebar-shadow-md: var(--color-shadow-md);
--color-sidebar-shadow-lg: var(--color-shadow-lg);
--color-sidebar-shadow-xl: var(--color-shadow-xl);
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
}
[data-theme="light"],
[data-theme="light-contrast"] {
color-scheme: light !important;
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 250, 250, 250; /* secondary bg */
--color-background-80: 245, 245, 245; /* tertiary bg */
}
[data-theme="light"] {
--color-text-100: 23, 23, 23; /* primary text */
--color-text-200: 58, 58, 58; /* secondary text */
--color-text-300: 82, 82, 82; /* tertiary text */
--color-text-400: 163, 163, 163; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
}
[data-theme="light-contrast"] {
--color-text-100: 11, 11, 11; /* primary text */
--color-text-200: 38, 38, 38; /* secondary text */
--color-text-300: 58, 58, 58; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
}
[data-theme="dark"],
[data-theme="dark-contrast"] {
color-scheme: dark !important;
--color-background-100: 7, 7, 7; /* primary bg */
--color-background-90: 11, 11, 11; /* secondary bg */
--color-background-80: 23, 23, 23; /* tertiary bg */
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
}
[data-theme="dark"] {
--color-text-100: 229, 229, 229; /* primary text */
--color-text-200: 163, 163, 163; /* secondary text */
--color-text-300: 115, 115, 115; /* tertiary text */
--color-text-400: 82, 82, 82; /* placeholder text */
--color-border-100: 34, 34, 34; /* subtle border= 1 */
--color-border-200: 38, 38, 38; /* subtle border- 2 */
--color-border-300: 46, 46, 46; /* strong border- 1 */
--color-border-400: 58, 58, 58; /* strong border- 2 */
}
[data-theme="dark-contrast"] {
--color-text-100: 250, 250, 250; /* primary text */
--color-text-200: 241, 241, 241; /* secondary text */
--color-text-300: 212, 212, 212; /* tertiary text */
--color-text-400: 115, 115, 115; /* placeholder text */
--color-border-100: 245, 245, 245; /* subtle border= 1 */
--color-border-200: 229, 229, 229; /* subtle border- 2 */
--color-border-300: 212, 212, 212; /* strong border- 1 */
--color-border-400: 185, 185, 185; /* strong border- 2 */
}
[data-theme="light"],
[data-theme="dark"],
[data-theme="light-contrast"],
[data-theme="dark-contrast"] {
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
--color-sidebar-text-100: var(--color-text-100); /* primary sidebar text */
--color-sidebar-text-200: var(--color-text-200); /* secondary sidebar text */
--color-sidebar-text-300: var(--color-text-300); /* tertiary sidebar text */
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-variant-ligatures: none;
-webkit-font-variant-ligatures: none;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
body {
color: rgba(var(--color-text-100));
}
/* scrollbar style */
::-webkit-scrollbar {
display: none;
}
.horizontal-scroll-enable {
overflow-x: scroll;
}
.horizontal-scroll-enable::-webkit-scrollbar {
display: block;
height: 7px;
width: 0;
}
.horizontal-scroll-enable::-webkit-scrollbar-track {
height: 7px;
background-color: rgba(var(--color-background-100));
}
.horizontal-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-background-80));
}
.vertical-scroll-enable::-webkit-scrollbar {
display: block;
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-track {
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-background-90));
}
/* end scrollbar style */
.tags-input-container {
border: 2px solid #000;
padding: 0.5em;
border-radius: 3px;
width: min(80vw, 600px);
margin-top: 1em;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
.tag-item {
background-color: rgb(218, 216, 216);
display: inline-block;
padding: 0.5em 0.75em;
border-radius: 20px;
}
.tag-item .close {
height: 20px;
width: 20px;
background-color: rgb(48, 48, 48);
color: #fff;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin-left: 0.5em;
font-size: 18px;
cursor: pointer;
}
.tags-input {
flex-grow: 1;
padding: 0.5em 0;
border: none;
outline: none;
}
/* emoji icon picker */
.conical-gradient {
background: conic-gradient(
from 180deg at 50% 50%,
#ff6b00 0deg,
#f7ae59 70.5deg,
#3f76ff 151.12deg,
#05c3ff 213deg,
#18914f 289.87deg,
#f6f172 329.25deg,
#ff6b00 360deg
);
}
/* progress bar */
.progress-bar {
fill: currentColor;
color: rgba(var(--color-sidebar-background-100));
}
/* lineclamp */
.lineclamp {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
/* popover2 styling */
.bp4-popover2-transition-container {
z-index: 1 !important;
}
::-webkit-input-placeholder,
::placeholder,
:-ms-input-placeholder {
color: rgb(var(--color-text-400));
}
.bp4-overlay-content {
z-index: 555 !important;
}
.disable-scroll {
overflow: hidden !important;
}

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

@ -1,5 +1,9 @@
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// icons
import { Icon, Tooltip } from "components/ui";
import { CopyPlus } from "lucide-react";
@ -10,6 +14,8 @@ 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();
@ -52,6 +58,26 @@ 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: (
@ -325,14 +351,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 +368,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 && (

View File

@ -76,7 +76,7 @@ export const AllBoards: React.FC<Props> = ({
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-8 bg-custom-background-90">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
displayFilters?.group_by === "state"

View File

@ -35,6 +35,7 @@ type Props = {
data: IIssue | null;
user: ICurrentUserResponse | undefined;
onSubmit?: () => Promise<void>;
redirection?: boolean;
};
export const DeleteIssueModal: React.FC<Props> = ({
@ -43,6 +44,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
data,
user,
onSubmit,
redirection = true,
}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@ -132,7 +134,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
message: "Issue deleted successfully",
});
if (issueId) router.back();
if (issueId && redirection) router.back();
})
.catch((error) => {
console.log(error);

View File

@ -137,13 +137,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
<TiptapEditor
uploadFile={fileService.uploadFile}
deleteFile={fileService.deleteImage}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? "<p></p>"
: value
}
value={value}
workspaceSlug={workspaceSlug}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}

View File

@ -12,7 +12,6 @@ export * from "./main-content";
export * from "./modal";
export * from "./parent-issues-list-modal";
export * from "./sidebar";
export * from "./sub-issues-list";
export * from "./label";
export * from "./issue-reaction";
export * from "./peek-overview";

View File

@ -18,9 +18,9 @@ import {
IssueAttachmentUpload,
IssueAttachments,
IssueDescriptionForm,
SubIssuesList,
IssueReaction,
} from "components/issues";
import { SubIssuesRoot } from "./sub-issues";
// ui
import { CustomMenu } from "components/ui";
// icons
@ -206,7 +206,7 @@ export const IssueMainContent: React.FC<Props> = ({
<IssueReaction workspaceSlug={workspaceSlug} issueId={issueId} projectId={projectId} />
<div className="mt-2 space-y-2">
<SubIssuesList parentIssue={issueDetails} user={user} disabled={uneditable} />
<SubIssuesRoot parentIssue={issueDetails} user={user} editable={uneditable} />
</div>
</div>
<div className="flex flex-col gap-3 py-3">

View File

@ -1,251 +0,0 @@
import { FC, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// services
import issuesService from "services/issues.service";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
// components
import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal } from "components/issues";
// ui
import { CustomMenu } from "components/ui";
// icons
import { ChevronRightIcon, PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import { ICurrentUserResponse, IIssue, ISearchIssueResponse, ISubIssueResponse } from "types";
// fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys";
type Props = {
parentIssue: IIssue;
user: ICurrentUserResponse | undefined;
disabled?: boolean;
};
export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }) => {
// states
const [createIssueModal, setCreateIssueModal] = useState(false);
const [subIssuesListModal, setSubIssuesListModal] = useState(false);
const [preloadedData, setPreloadedData] = useState<Partial<IIssue> | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { memberRole } = useProjectMyMembership();
const { data: subIssuesResponse } = useSWR(
workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null,
workspaceSlug && parentIssue
? () => issuesService.subIssues(workspaceSlug as string, parentIssue.project, parentIssue.id)
: null
);
const addAsSubIssue = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !parentIssue) return;
const payload = {
sub_issue_ids: data.map((i) => i.id),
};
await issuesService
.addSubIssues(workspaceSlug as string, parentIssue.project, parentIssue.id, payload)
.finally(() => mutate(SUB_ISSUES(parentIssue.id)));
};
const handleSubIssueRemove = (issue: IIssue) => {
if (!workspaceSlug || !parentIssue) return;
mutate<ISubIssueResponse>(
SUB_ISSUES(parentIssue.id),
(prevData) => {
if (!prevData) return prevData;
const stateDistribution = { ...prevData.state_distribution };
const issueGroup = issue.state_detail.group;
stateDistribution[issueGroup] = stateDistribution[issueGroup] - 1;
return {
state_distribution: stateDistribution,
sub_issues: prevData.sub_issues.filter((i) => i.id !== issue.id),
};
},
false
);
issuesService
.patchIssue(workspaceSlug.toString(), issue.project, issue.id, { parent: null }, user)
.finally(() => mutate(SUB_ISSUES(parentIssue.id)));
};
const handleCreateIssueModal = () => {
setCreateIssueModal(true);
setPreloadedData({
parent: parentIssue.id,
});
};
const completedSubIssue = subIssuesResponse?.state_distribution.completed ?? 0;
const cancelledSubIssue = subIssuesResponse?.state_distribution.cancelled ?? 0;
const totalCompletedSubIssues = completedSubIssue + cancelledSubIssue;
const totalSubIssues = subIssuesResponse ? subIssuesResponse.sub_issues.length : 0;
const completionPercentage = (totalCompletedSubIssues / totalSubIssues) * 100;
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;
return (
<>
<CreateUpdateIssueModal
isOpen={createIssueModal}
prePopulateData={{ ...preloadedData }}
handleClose={() => setCreateIssueModal(false)}
/>
<ExistingIssuesListModal
isOpen={subIssuesListModal}
handleClose={() => setSubIssuesListModal(false)}
searchParams={{ sub_issue: true, issue_id: parentIssue?.id }}
handleOnSubmit={addAsSubIssue}
workspaceLevelToggle
/>
{subIssuesResponse && subIssuesResponse.sub_issues.length > 0 ? (
<Disclosure defaultOpen={true}>
{({ open }) => (
<>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-3 text-custom-text-200">
<Disclosure.Button className="flex items-center gap-1 rounded px-2 py-1 text-xs text-custom-text-100 hover:bg-custom-background-80">
<ChevronRightIcon className={`h-3 w-3 ${open ? "rotate-90" : ""}`} />
Sub-issues{" "}
<span className="ml-1 text-custom-text-200">
{subIssuesResponse.sub_issues.length}
</span>
</Disclosure.Button>
<div className="flex w-60 items-center gap-2">
<div className="bar relative h-1.5 w-full rounded bg-custom-background-80">
<div
className="absolute top-0 left-0 h-1.5 rounded bg-green-500 duration-300"
style={{
width: `${
isNaN(completionPercentage)
? 0
: completionPercentage > 100
? 100
: completionPercentage.toFixed(0)
}%`,
}}
/>
</div>
<span className="whitespace-nowrap text-xs">
{isNaN(completionPercentage)
? 0
: completionPercentage > 100
? 100
: completionPercentage.toFixed(0)}
% Done
</span>
</div>
</div>
{open && !isNotAllowed ? (
<div className="flex items-center">
<button
type="button"
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80"
onClick={handleCreateIssueModal}
>
<PlusIcon className="h-3 w-3" />
Create new
</button>
<CustomMenu ellipsis>
<CustomMenu.MenuItem onClick={() => setSubIssuesListModal(true)}>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</div>
) : null}
</div>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className="mt-3 flex flex-col gap-y-1">
{subIssuesResponse.sub_issues.map((issue) => (
<Link
key={issue.id}
href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}
>
<a className="group flex items-center justify-between gap-2 rounded p-2 hover:bg-custom-background-90">
<div className="flex items-center gap-2 rounded text-xs">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<span className="flex-shrink-0 text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</span>
<span className="max-w-sm break-words font-medium">{issue.name}</span>
</div>
{!isNotAllowed && (
<button
type="button"
className="cursor-pointer opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubIssueRemove(issue);
}}
>
<XMarkIcon className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
</button>
)}
</a>
</Link>
))}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
) : (
!isNotAllowed && (
<CustomMenu
label={
<>
<PlusIcon className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
position="left"
noBorder
noChevron
>
<CustomMenu.MenuItem onClick={handleCreateIssueModal}>Create new</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => setSubIssuesListModal(true)}>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
)
)}
</>
);
};

View File

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

View File

@ -0,0 +1,171 @@
import React from "react";
// next imports
import Link from "next/link";
// lucide icons
import {
ChevronDown,
ChevronRight,
X,
Pencil,
Trash,
Link as LinkIcon,
Loader,
} from "lucide-react";
// components
import { SubIssuesRootList } from "./issues-list";
import { IssueProperty } from "./properties";
// ui
import { Tooltip, CustomMenu } from "components/ui";
// types
import { ICurrentUserResponse, IIssue } from "types";
export interface ISubIssues {
workspaceSlug: string;
projectId: string;
parentIssue: IIssue;
issue: any;
spacingLeft?: number;
user: ICurrentUserResponse | undefined;
editable: boolean;
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
issuesVisibility: string[];
handleIssuesVisibility: (issueId: string) => void;
copyText: (text: string) => void;
handleIssueCrudOperation: (
key: "create" | "existing" | "edit" | "delete",
issueId: string,
issue?: IIssue | null
) => void;
}
export const SubIssues: React.FC<ISubIssues> = ({
workspaceSlug,
projectId,
parentIssue,
issue,
spacingLeft = 0,
user,
editable,
removeIssueFromSubIssues,
issuesVisibility,
handleIssuesVisibility,
copyText,
handleIssueCrudOperation,
}) => (
<div>
{issue && (
<div
className="relative flex items-center gap-2 py-1 px-2 w-full h-full hover:bg-custom-background-90 group transition-all border-b border-custom-border-100"
style={{ paddingLeft: `${spacingLeft}px` }}
>
<div className="flex-shrink-0 w-[22px] h-[22px]">
{issue?.sub_issues_count > 0 && (
<>
{true ? (
<div
className="w-full h-full flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer"
onClick={() => handleIssuesVisibility(issue?.id)}
>
{issuesVisibility && issuesVisibility.includes(issue?.id) ? (
<ChevronDown width={14} strokeWidth={2} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
</div>
) : (
<Loader width={14} strokeWidth={2} className="animate-spin" />
)}
</>
)}
</div>
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="w-full flex items-center gap-2">
<div
className="flex-shrink-0 w-[6px] h-[6px] rounded-full"
style={{
backgroundColor: issue.state_detail.color,
}}
/>
<div className="flex-shrink-0 text-xs text-custom-text-200">
{issue.project_detail.identifier}-{issue?.sequence_id}
</div>
<Tooltip tooltipHeading="Title" tooltipContent={`${issue?.name}`}>
<div className="line-clamp-1 text-xs text-custom-text-100">{issue?.name}</div>
</Tooltip>
</a>
</Link>
<div className="flex-shrink-0 text-sm">
<IssueProperty
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssue={parentIssue}
issue={issue}
user={user}
editable={editable}
/>
</div>
<div className="flex-shrink-0 text-sm">
<CustomMenu width="auto" ellipsis>
{editable && (
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("edit", parentIssue?.id, issue)}
>
<div className="flex items-center justify-start gap-2">
<Pencil width={14} strokeWidth={2} />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
)}
{editable && (
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("delete", parentIssue?.id, issue)}
>
<div className="flex items-center justify-start gap-2">
<Trash width={14} strokeWidth={2} />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={copyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon width={14} strokeWidth={2} />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
{editable && (
<div
className="flex-shrink-0 invisible group-hover:visible w-[22px] h-[22px] flex justify-center items-center rounded-sm hover:bg-custom-background-80 transition-all cursor-pointer overflow-hidden"
onClick={() => removeIssueFromSubIssues(parentIssue?.id, issue)}
>
<X width={14} strokeWidth={2} />
</div>
)}
</div>
)}
{issuesVisibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
<SubIssuesRootList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssue={issue}
spacingLeft={spacingLeft + 22}
user={user}
editable={editable}
removeIssueFromSubIssues={removeIssueFromSubIssues}
issuesVisibility={issuesVisibility}
handleIssuesVisibility={handleIssuesVisibility}
copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation}
/>
)}
</div>
);

View File

@ -0,0 +1,84 @@
import React from "react";
// swr
import useSWR from "swr";
// components
import { SubIssues } from "./issue";
// types
import { ICurrentUserResponse, IIssue } from "types";
// services
import issuesService from "services/issues.service";
// fetch keys
import { SUB_ISSUES } from "constants/fetch-keys";
export interface ISubIssuesRootList {
workspaceSlug: string;
projectId: string;
parentIssue: IIssue;
spacingLeft?: number;
user: ICurrentUserResponse | undefined;
editable: boolean;
removeIssueFromSubIssues: (parentIssueId: string, issue: IIssue) => void;
issuesVisibility: string[];
handleIssuesVisibility: (issueId: string) => void;
copyText: (text: string) => void;
handleIssueCrudOperation: (
key: "create" | "existing" | "edit" | "delete",
issueId: string,
issue?: IIssue | null
) => void;
}
export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
workspaceSlug,
projectId,
parentIssue,
spacingLeft = 10,
user,
editable,
removeIssueFromSubIssues,
issuesVisibility,
handleIssuesVisibility,
copyText,
handleIssueCrudOperation,
}) => {
const { data: issues, isLoading } = useSWR(
workspaceSlug && projectId && parentIssue && parentIssue?.id
? SUB_ISSUES(parentIssue?.id)
: null,
workspaceSlug && projectId && parentIssue && parentIssue?.id
? () => issuesService.subIssues(workspaceSlug, projectId, parentIssue.id)
: null
);
return (
<div className="relative">
{issues &&
issues.sub_issues &&
issues.sub_issues.length > 0 &&
issues.sub_issues.map((issue: IIssue) => (
<SubIssues
key={`${issue?.id}`}
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssue={parentIssue}
issue={issue}
spacingLeft={spacingLeft}
user={user}
editable={editable}
removeIssueFromSubIssues={removeIssueFromSubIssues}
issuesVisibility={issuesVisibility}
handleIssuesVisibility={handleIssuesVisibility}
copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation}
/>
))}
<div
className={`absolute top-0 bottom-0 ${
spacingLeft > 10 ? `border-l border-custom-border-100` : ``
}`}
style={{ left: `${spacingLeft - 12}px` }}
/>
</div>
);
};

View File

@ -0,0 +1,25 @@
export interface IProgressBar {
total: number;
done: number;
}
export const ProgressBar = ({ total = 0, done = 0 }: IProgressBar) => {
const calPercentage = (doneValue: number, totalValue: number): string => {
if (doneValue === 0 || totalValue === 0) return (0).toFixed(0);
return ((100 * doneValue) / totalValue).toFixed(0);
};
return (
<div className="relative flex items-center gap-2">
<div className="w-full">
<div className="w-full rounded-full bg-custom-background-80 overflow-hidden shadow">
<div
className="bg-green-500 h-[6px] rounded-full"
style={{ width: `${calPercentage(done, total)}%` }}
/>
</div>
</div>
<div className="flex-shrink-0 text-xs font-medium">{calPercentage(done, total)}% Done</div>
</div>
);
};

View File

@ -0,0 +1,204 @@
import React from "react";
// swr
import { mutate } from "swr";
// components
import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues";
import { MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// hooks
import useIssuesProperties from "hooks/use-issue-properties";
// types
import { ICurrentUserResponse, IIssue, IState } from "types";
// fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys";
// services
import issuesService from "services/issues.service";
import trackEventServices from "services/track-event.service";
export interface IIssueProperty {
workspaceSlug: string;
projectId: string;
parentIssue: IIssue;
issue: IIssue;
user: ICurrentUserResponse | undefined;
editable: boolean;
}
export const IssueProperty: React.FC<IIssueProperty> = ({
workspaceSlug,
projectId,
parentIssue,
issue,
user,
editable,
}) => {
const [properties] = useIssuesProperties(workspaceSlug, projectId);
const handlePriorityChange = (data: any) => {
partialUpdateIssue({ priority: data });
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 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,
});
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 });
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 partialUpdateIssue = async (data: Partial<IIssue>) => {
mutate(
workspaceSlug && parentIssue ? SUB_ISSUES(parentIssue.id) : null,
(elements: any) => {
const _elements = { ...elements };
const _issues = _elements.sub_issues.map((element: IIssue) =>
element.id === issue.id ? { ...element, ...data } : element
);
_elements["sub_issues"] = [..._issues];
return _elements;
},
false
);
const issueResponse = await issuesService.patchIssue(
workspaceSlug as string,
issue.project,
issue.id,
data,
user
);
mutate(
SUB_ISSUES(parentIssue.id),
(elements: any) => {
const _elements = elements.sub_issues.map((element: IIssue) =>
element.id === issue.id ? issueResponse : element
);
elements["sub_issues"] = _elements;
return elements;
},
true
);
};
return (
<div className="relative flex items-center gap-1">
{properties.priority && (
<div className="flex-shrink-0">
<PrioritySelect
value={issue.priority}
onChange={handlePriorityChange}
hideDropdownArrow
disabled={!editable}
/>
</div>
)}
{properties.state && (
<div className="flex-shrink-0">
<StateSelect
value={issue.state_detail}
onChange={handleStateChange}
hideDropdownArrow
disabled={!editable}
/>
</div>
)}
{properties.start_date && issue.start_date && (
<div className="flex-shrink-0 w-[104px]">
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={!editable}
/>
</div>
)}
{properties.due_date && issue.target_date && (
<div className="flex-shrink-0 w-[104px]">
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
user={user}
isNotAllowed={!editable}
/>
</div>
)}
{properties.assignee && (
<div className="flex-shrink-0">
<MembersSelect
value={issue.assignees}
onChange={handleAssigneeChange}
membersDetails={issue.assignee_details}
hideDropdownArrow
disabled={!editable}
/>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,283 @@
import React from "react";
// next imports
import { useRouter } from "next/router";
// swr
import useSWR, { mutate } from "swr";
// lucide icons
import { Plus, ChevronRight, ChevronDown } from "lucide-react";
// components
import { ExistingIssuesListModal } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { SubIssuesRootList } from "./issues-list";
import { ProgressBar } from "./progressbar";
// ui
import { CustomMenu } from "components/ui";
// hooks
import { useProjectMyMembership } from "contexts/project-member.context";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, ISearchIssueResponse } from "types";
// services
import issuesService from "services/issues.service";
// fetch keys
import { SUB_ISSUES } from "constants/fetch-keys";
export interface ISubIssuesRoot {
parentIssue: IIssue;
user: ICurrentUserResponse | undefined;
editable: boolean;
}
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = ({ parentIssue, user, editable }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
const { memberRole } = useProjectMyMembership();
const { data: issues } = useSWR(
workspaceSlug && projectId && parentIssue && parentIssue?.id
? SUB_ISSUES(parentIssue?.id)
: null,
workspaceSlug && projectId && parentIssue && parentIssue?.id
? () => issuesService.subIssues(workspaceSlug, projectId, parentIssue.id)
: null
);
const [issuesVisibility, setIssuesVisibility] = React.useState<string[]>([parentIssue?.id]);
const handleIssuesVisibility = (issueId: string) => {
if (issuesVisibility.includes(issueId)) {
setIssuesVisibility(issuesVisibility.filter((i: string) => i !== issueId));
} else {
setIssuesVisibility([...issuesVisibility, issueId]);
}
};
const [issueCrudOperation, setIssueCrudOperation] = React.useState<{
create: { toggle: boolean; issueId: string | null };
existing: { toggle: boolean; issueId: string | null };
edit: { toggle: boolean; issueId: string | null; issue: IIssue | null };
delete: { toggle: boolean; issueId: string | null; issue: IIssue | null };
}>({
create: {
toggle: false,
issueId: null,
},
existing: {
toggle: false,
issueId: null,
},
edit: {
toggle: false,
issueId: null, // parent issue id for mutation
issue: null,
},
delete: {
toggle: false,
issueId: null, // parent issue id for mutation
issue: null,
},
});
const handleIssueCrudOperation = (
key: "create" | "existing" | "edit" | "delete",
issueId: string | null,
issue: IIssue | null = null
) => {
setIssueCrudOperation({
...issueCrudOperation,
[key]: {
toggle: !issueCrudOperation[key].toggle,
issueId: issueId,
issue: issue,
},
});
};
const addAsSubIssueFromExistingIssues = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !parentIssue || issueCrudOperation?.existing?.issueId === null) return;
const issueId = issueCrudOperation?.existing?.issueId;
const payload = {
sub_issue_ids: data.map((i) => i.id),
};
await issuesService.addSubIssues(workspaceSlug, projectId, issueId, payload).finally(() => {
if (issueId) mutate(SUB_ISSUES(issueId));
});
};
const removeIssueFromSubIssues = async (parentIssueId: string, issue: IIssue) => {
if (!workspaceSlug || !parentIssue || !issue?.id) return;
issuesService
.patchIssue(workspaceSlug, projectId, issue.id, { parent: null }, user)
.finally(() => {
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
});
};
const copyText = (text: string) => {
const originURL =
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${text}`).then(() => {
// setToastAlert({
// type: "success",
// title: "Link Copied!",
// message: "Issue link copied to clipboard.",
// });
});
};
const isEditable = memberRole?.isGuest || memberRole?.isViewer ? false : true;
const mutateSubIssues = (parentIssueId: string | null) => {
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
};
return (
<div className="w-full h-full space-y-2">
{parentIssue && parentIssue?.sub_issues_count > 0 ? (
<>
{/* header */}
<div className="relative flex items-center gap-4 text-xs">
<div
className="rounded border border-custom-border-100 shadow p-1.5 px-2 flex items-center gap-1 hover:bg-custom-background-80 transition-all cursor-pointer select-none"
onClick={() => handleIssuesVisibility(parentIssue?.id)}
>
<div className="flex-shrink-0 w-[16px] h-[16px] flex justify-center items-center">
{issuesVisibility.includes(parentIssue?.id) ? (
<ChevronDown width={16} strokeWidth={2} />
) : (
<ChevronRight width={14} strokeWidth={2} />
)}
</div>
<div>Sub-issues</div>
<div>({parentIssue?.sub_issues_count})</div>
</div>
<div className="w-full max-w-[250px] select-none">
<ProgressBar
total={parentIssue?.sub_issues_count}
done={
(issues?.state_distribution?.cancelled || 0) +
(issues?.state_distribution?.completed || 0)
}
/>
</div>
{isEditable && issuesVisibility.includes(parentIssue?.id) && (
<div className="ml-auto flex-shrink-0 flex items-center gap-2 select-none">
<div
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
onClick={() => handleIssueCrudOperation("create", parentIssue?.id)}
>
Add sub-issue
</div>
<div
className="hover:bg-custom-background-80 transition-all cursor-pointer p-1.5 px-2 rounded border border-custom-border-100 shadow"
onClick={() => handleIssueCrudOperation("existing", parentIssue?.id)}
>
Add an existing issue
</div>
</div>
)}
</div>
{/* issues */}
{issuesVisibility.includes(parentIssue?.id) && (
<div className="border border-b-0 border-custom-border-100">
<SubIssuesRootList
workspaceSlug={workspaceSlug}
projectId={projectId}
parentIssue={parentIssue}
user={undefined}
editable={isEditable}
removeIssueFromSubIssues={removeIssueFromSubIssues}
issuesVisibility={issuesVisibility}
handleIssuesVisibility={handleIssuesVisibility}
copyText={copyText}
handleIssueCrudOperation={handleIssueCrudOperation}
/>
</div>
)}
</>
) : (
isEditable && (
<div className="text-xs py-2 text-custom-text-300 font-medium">
<div className="py-3 text-center">No sub issues are available</div>
<>
<CustomMenu
label={
<>
<Plus className="h-3 w-3" />
Add sub-issue
</>
}
buttonClassName="whitespace-nowrap"
position="left"
noBorder
noChevron
>
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("create", parentIssue?.id)}
>
Create new
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => handleIssueCrudOperation("existing", parentIssue?.id)}
>
Add an existing issue
</CustomMenu.MenuItem>
</CustomMenu>
</>
</div>
)
)}
{isEditable && issueCrudOperation?.create?.toggle && (
<CreateUpdateIssueModal
isOpen={issueCrudOperation?.create?.toggle}
prePopulateData={{
parent: issueCrudOperation?.create?.issueId,
}}
handleClose={() => handleIssueCrudOperation("create", null)}
/>
)}
{isEditable &&
issueCrudOperation?.existing?.toggle &&
issueCrudOperation?.existing?.issueId && (
<ExistingIssuesListModal
isOpen={issueCrudOperation?.existing?.toggle}
handleClose={() => handleIssueCrudOperation("existing", null)}
searchParams={{ sub_issue: true, issue_id: issueCrudOperation?.existing?.issueId }}
handleOnSubmit={addAsSubIssueFromExistingIssues}
workspaceLevelToggle
/>
)}
{isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && (
<CreateUpdateIssueModal
isOpen={issueCrudOperation?.edit?.toggle}
handleClose={() => {
mutateSubIssues(issueCrudOperation?.edit?.issueId);
handleIssueCrudOperation("edit", null, null);
}}
data={issueCrudOperation?.edit?.issue}
/>
)}
{isEditable && issueCrudOperation?.delete?.toggle && issueCrudOperation?.delete?.issueId && (
<DeleteIssueModal
isOpen={issueCrudOperation?.delete?.toggle}
handleClose={() => {
mutateSubIssues(issueCrudOperation?.delete?.issueId);
handleIssueCrudOperation("delete", null, null);
}}
data={issueCrudOperation?.delete?.issue}
user={user}
redirection={false}
/>
)}
</div>
);
};

View File

@ -51,7 +51,6 @@ export const ProfileIssuesView = () => {
groupedIssues,
mutateProfileIssues,
displayFilters,
setDisplayFilters,
isEmpty,
filters,
setFilters,

View File

@ -82,7 +82,7 @@ export const MembersSelect: React.FC<Props> = ({
<Tooltip
tooltipHeading="Assignee"
tooltipContent={
membersDetails.length > 0
membersDetails && membersDetails.length > 0
? membersDetails.map((assignee) => assignee?.display_name).join(", ")
: "No Assignee"
}

View File

@ -1,15 +1,16 @@
import React from "react";
import useSWR from "swr";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// component
import { MarkdownRenderer, Spinner } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
// services
import workspaceService from "services/workspace.service";
// helper
// components
import { Loader, MarkdownRenderer } from "components/ui";
// icons
import { XMarkIcon } from "@heroicons/react/20/solid";
// helpers
import { renderLongDateFormat } from "helpers/date-time.helper";
type Props = {
@ -34,8 +35,8 @@ export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="grid place-items-center min-h-full text-center p-4">
<div className="fixed inset-0 z-20 h-full w-full">
<div className="grid place-items-center h-full w-full p-4">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -45,49 +46,62 @@ export const ProductUpdatesModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-100 text-left shadow-xl transition-all grid place-items-center sm:w-full sm:max-w-2xl">
<div className="max-h-[90vh] overflow-y-auto p-5">
<div className="sm:flex sm:items-start">
<div className="flex w-full flex-col gap-y-4 text-center sm:text-left">
<Dialog.Title
as="h3"
className="flex justify-between text-lg font-medium leading-6 text-custom-text-100"
>
<span>Product Updates</span>
<span>
<button type="button" onClick={() => setIsOpen(false)}>
<XMarkIcon
className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
aria-hidden="true"
/>
</button>
</span>
</Dialog.Title>
{updates && updates.length > 0 ? (
updates.map((item, index) => (
<React.Fragment key={item.id}>
<div className="flex items-center gap-3 text-xs text-custom-text-200">
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-background-90 px-3 py-1.5 text-xs">
{item.tag_name}
<Dialog.Panel className="relative overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-100 shadow-custom-shadow-rg] min-w-[100%] sm:min-w-[50%] sm:max-w-[50%]">
<div className="flex flex-col p-4 max-h-[90vh] w-full">
<Dialog.Title
as="h3"
className="flex items-center justify-between text-lg font-semibold"
>
<span>Product Updates</span>
<span>
<button type="button" onClick={() => setIsOpen(false)}>
<XMarkIcon
className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100"
aria-hidden="true"
/>
</button>
</span>
</Dialog.Title>
{updates && updates.length > 0 ? (
<div className="h-full overflow-y-auto mt-4 space-y-4">
{updates.map((item, index) => (
<React.Fragment key={item.id}>
<div className="flex items-center gap-3 text-xs text-custom-text-200">
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-background-90 px-3 py-1.5 text-xs">
{item.tag_name}
</span>
<span>{renderLongDateFormat(item.published_at)}</span>
{index === 0 && (
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-primary px-3 py-1.5 text-xs text-white">
New
</span>
<span>{renderLongDateFormat(item.published_at)}</span>
{index === 0 && (
<span className="flex items-center rounded-full border border-custom-border-200 bg-custom-primary px-3 py-1.5 text-xs text-white">
New
</span>
)}
</div>
<MarkdownRenderer markdown={item.body} />
</React.Fragment>
))
) : (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
Loading...
</div>
)}
)}
</div>
<MarkdownRenderer markdown={item.body} />
</React.Fragment>
))}
</div>
</div>
) : (
<div className="grid place-items-center w-full mt-4">
<Loader className="space-y-6 w-full">
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
<div className="space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
</div>
</Loader>
</div>
)}
</div>
</Dialog.Panel>
</Transition.Child>

View File

@ -10,7 +10,7 @@ import { useForm, Controller } from "react-hook-form";
import useProjectDetails from "hooks/use-project-details";
// components
import { TipTapEditor } from "components/tiptap";
import { TiptapEditorWithRef } from "@plane/editor";
// icons
import { Send } from "lucide-react";
@ -20,6 +20,7 @@ import { Icon, SecondaryButton, Tooltip, PrimaryButton } from "components/ui";
// types
import type { IIssueComment } from "types";
import fileService from "@/services/file.service";
const defaultValues: Partial<IIssueComment> = {
access: "INTERNAL",
@ -85,15 +86,13 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
<button
type="button"
onClick={() => onChange(access.key)}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${
value === access.key ? "bg-custom-background-80" : ""
}`}
className={`grid place-items-center p-1 hover:bg-custom-background-80 ${value === access.key ? "bg-custom-background-80" : ""
}`}
>
<Icon
iconName={access.icon}
className={`w-4 h-4 -mt-1 ${
value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
}`}
className={`w-4 h-4 -mt-1 ${value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
}`}
/>
</button>
</Tooltip>
@ -107,7 +106,9 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
name="comment_html"
control={control}
render={({ field: { value, onChange } }) => (
<TipTapEditor
<TiptapEditorWithRef
uploadFile={fileService.uploadFile}
deleteFile={fileService.deleteImage}
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={!value || value === "" ? "<p></p>" : value}

View File

@ -71,9 +71,8 @@ export const reducer: ReducerFunctionType = (state, action) => {
...state,
display_filters: {
...state.display_filters,
...payload,
...payload?.display_filters,
},
issueView: payload?.display_filters?.layout || "list",
};
return {
@ -100,7 +99,7 @@ export const reducer: ReducerFunctionType = (state, action) => {
case "SET_PROPERTIES": {
const newState = {
...state,
properties: {
display_properties: {
...state.display_properties,
...payload?.display_properties,
},
@ -129,7 +128,6 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
type: "SET_DISPLAY_FILTERS",
payload: {
display_filters: {
...state.display_filters,
...displayFilter,
},
},
@ -179,7 +177,6 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
type: "SET_PROPERTIES",
payload: {
display_properties: {
...state.display_properties,
[key]: !state.display_properties[key],
},
},

View File

@ -38,10 +38,13 @@ export const orderArrayBy = (
export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length;
export const findStringWithMostCharacters = (strings: string[]) =>
strings.reduce((longestString, currentString) =>
export const findStringWithMostCharacters = (strings: string[]): string => {
if (!strings || strings.length === 0) return "";
return strings.reduce((longestString, currentString) =>
currentString.length > longestString.length ? currentString : longestString
);
};
export const checkIfArraysHaveSameElements = (arr1: any[] | null, arr2: any[] | null): boolean => {
if (!arr1 || !arr2) return false;

View File

@ -9,7 +9,8 @@ import userService from "services/user.service";
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
// components
import { ActivityIcon, ActivityMessage } from "components/core";
import { TipTapEditor } from "components/tiptap";
import { TiptapEditor } from "@plane/editor";
// import { TipTapEditor } from "components/tiptap";
// icons
import { ArrowTopRightOnSquareIcon, ChatBubbleLeftEllipsisIcon } from "@heroicons/react/24/outline";
// ui
@ -20,6 +21,7 @@ import { USER_ACTIVITY } from "constants/fetch-keys";
// helper
import { timeAgo } from "helpers/date-time.helper";
import { SettingsSidebar } from "components/project";
import fileService from "@/services/file.service";
const ProfileActivity = () => {
const router = useRouter();
@ -96,7 +98,9 @@ const ProfileActivity = () => {
</p>
</div>
<div className="issue-comments-section p-0">
<TipTapEditor
<TiptapEditor
uploadFile={fileService.uploadFile}
deleteFile={fileService.deleteImage}
workspaceSlug={workspaceSlug as string}
value={
activityItem?.new_value !== ""