mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
reverted back types and fixed tailwind
This commit is contained in:
parent
448d433876
commit
f0e246ccfd
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 && (
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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";
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
1
web/components/issues/sub-issues/index.ts
Normal file
1
web/components/issues/sub-issues/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./root";
|
171
web/components/issues/sub-issues/issue.tsx
Normal file
171
web/components/issues/sub-issues/issue.tsx
Normal 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>
|
||||
);
|
84
web/components/issues/sub-issues/issues-list.tsx
Normal file
84
web/components/issues/sub-issues/issues-list.tsx
Normal 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>
|
||||
);
|
||||
};
|
25
web/components/issues/sub-issues/progressbar.tsx
Normal file
25
web/components/issues/sub-issues/progressbar.tsx
Normal 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>
|
||||
);
|
||||
};
|
204
web/components/issues/sub-issues/properties.tsx
Normal file
204
web/components/issues/sub-issues/properties.tsx
Normal 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>
|
||||
);
|
||||
};
|
283
web/components/issues/sub-issues/root.tsx
Normal file
283
web/components/issues/sub-issues/root.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -51,7 +51,6 @@ export const ProfileIssuesView = () => {
|
||||
groupedIssues,
|
||||
mutateProfileIssues,
|
||||
displayFilters,
|
||||
setDisplayFilters,
|
||||
isEmpty,
|
||||
filters,
|
||||
setFilters,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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],
|
||||
},
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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 !== ""
|
||||
|
Loading…
Reference in New Issue
Block a user