Compare commits

..
Author SHA1 Message Date
deepsource-autofix[bot]GitHubdeepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
80e6d7e1ea refactor: remove true from boolean attribute (#2579)
When using a boolean attribute in JSX, you can set the attribute value to true or omit the value. This helps to keep consistency in code.

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2023-11-01 12:30:21 +05:30
sriram veeraghantaandGitHub b7d5a42d45 fix: added deepsource config file (#2578) 2023-10-31 19:52:13 +05:30
sriram veeraghantaandGitHub 2b1e1557ca fix: upgrading to turbo new version (#2576) 2023-10-31 19:27:56 +05:30
Aaryan KhandelwalandGitHub 705b33377c fix: members list endpoint authorization (#2571)
* fix: members list endpoint authorization

* chore: update user types
2023-10-31 13:11:13 +05:30
NikhilandGitHub 49fd4427c8 chore: user settings endpoint (#2557)
* chore: user settings endpoint

* dev: fix the user settings
2023-10-31 13:08:13 +05:30
Bavisetti NarayanandGitHub bdbb64f385 fix: changed assignees and labels in pages and modules (#2553) 2023-10-31 13:06:57 +05:30
Aaryan KhandelwalandGitHub 98716859d5 chore: reove unused files (#2567) 2023-10-31 12:43:08 +05:30
M. PalanikannanandGitHub 8072bbb559 fix: Debounce title and Editor initialization (#2530)
* fixed debounce logic and extracted the same

* fixed editor mounting with custom hook

* removed console logs and improved structure

* fixed comment editor behavior on Shift-Enter

* fixed editor initialization behaviour for new peek view

* fixed button type to avoid reload while editing comments

* fixed initialization of content in peek overview

* improved naming variables in updated title debounce logic

* added react-hook-form support to the issue detail in peek view with save states

* delete image plugin's ts support improved
2023-10-31 12:26:10 +05:30
Aaryan KhandelwalandGitHub 442c83eea2 style: spreadsheet columns (#2554)
* style: spreadsheet columns

* fix: build errors
2023-10-31 12:18:04 +05:30
Aaryan KhandelwalandGitHub cb533849e8 chore: update members endpoint (#2569) 2023-10-31 12:16:40 +05:30
Aaryan KhandelwalandGitHub 59c52023fb style: list layout (#2566) 2023-10-31 12:14:06 +05:30
Aaryan KhandelwalandGitHub 08ca016f65 fix: custom theme form validations (#2565) 2023-10-31 12:12:24 +05:30
Aaryan KhandelwalandGitHub 1c2ea6da5e fix: edit project button redirection (#2564)
* fix: redirect to project settings

* fix: 404 page button alignment
2023-10-31 12:06:55 +05:30
Aaryan KhandelwalandGitHub 8b7b5c54b9 fix: global views bugs (#2563) 2023-10-31 12:06:11 +05:30
guru_sainathandGitHub 52474715de chore: handled next_url redirection issue (#2562) 2023-10-31 12:04:36 +05:30
Aaryan KhandelwalandGitHub dcf81e28e4 dev: implemented MobX in workspace settings and create workspace form (#2561)
* dev: implement mobx store for workspace settings

* chore: workspace general settings mobx integration

* chore: workspace members settings mobx integration
2023-10-30 20:38:50 +05:30
Aaryan KhandelwalandGitHub 050406b8a4 chore: add empty state for list and spreadsheet layouts (#2531)
* chore: add empty state for list and spreadsheet layouts

* fix: build errors
2023-10-30 20:09:04 +05:30
Anmol Singh BhatiaandGitHub 8eaac60aa5 style: cycle ui revamp, and chore: code refactor (#2558)
* chore: cycle custom svg icon added and code refactor

* chore: module code refactor

* style: cycle ui revamp and code refactor

* chore: cycle card view layout fix

* chore: layout fix

* style: module and cycle title tooltip position
2023-10-30 19:22:27 +05:30
219 changed files with 3234 additions and 4496 deletions
+17
View File
@@ -0,0 +1,17 @@
version = 1
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[analyzers]]
name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
+8 -3
View File
@@ -19,7 +19,7 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
members_list = serializers.ListField(
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
required=False,
@@ -39,6 +39,11 @@ class ModuleWriteSerializer(BaseSerializer):
"created_at",
"updated_at",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = [str(member.id) for member in instance.members.all()]
return data
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
@@ -46,7 +51,7 @@ class ModuleWriteSerializer(BaseSerializer):
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
project = self.context["project"]
@@ -72,7 +77,7 @@ class ModuleWriteSerializer(BaseSerializer):
return module
def update(self, instance, validated_data):
members = validated_data.pop("members_list", None)
members = validated_data.pop("members", None)
if members is not None:
ModuleMember.objects.filter(module=instance).delete()
+7 -3
View File
@@ -33,7 +33,7 @@ class PageBlockLiteSerializer(BaseSerializer):
class PageSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
labels_list = serializers.ListField(
labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
@@ -50,9 +50,13 @@ class PageSerializer(BaseSerializer):
"project",
"owned_by",
]
def to_representation(self, instance):
data = super().to_representation(instance)
data['labels'] = [str(label.id) for label in instance.labels.all()]
return data
def create(self, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
page = Page.objects.create(
@@ -77,7 +81,7 @@ class PageSerializer(BaseSerializer):
return page
def update(self, instance, validated_data):
labels = validated_data.pop("labels_list", None)
labels = validated_data.pop("labels", None)
if labels is not None:
PageLabel.objects.filter(page=instance).delete()
PageLabel.objects.bulk_create(
+4 -4
View File
@@ -79,14 +79,14 @@ class UserMeSettingsSerializer(BaseSerializer):
email=obj.email
).count()
if obj.last_workspace_id is not None:
workspace = Workspace.objects.get(
workspace = Workspace.objects.filter(
pk=obj.last_workspace_id, workspace_member__member=obj.id
)
).first()
return {
"last_workspace_id": obj.last_workspace_id,
"last_workspace_slug": workspace.slug,
"last_workspace_slug": workspace.slug if workspace is not None else "",
"fallback_workspace_id": obj.last_workspace_id,
"fallback_workspace_slug": workspace.slug,
"fallback_workspace_slug": workspace.slug if workspace is not None else "",
"invites": workspace_invites,
}
else:
@@ -82,7 +82,7 @@ def track_description(
if (
last_activity is not None
and last_activity.field == "description"
and actor_id == last_activity.actor_id
and actor_id == str(last_activity.actor_id)
):
last_activity.created_at = timezone.now()
last_activity.save(update_fields=["created_at"])
@@ -276,7 +276,7 @@ def track_labels(
issue_activities,
epoch,
):
requested_labels = set([str(lab) for lab in requested_data.get("labels_list", [])])
requested_labels = set([str(lab) for lab in requested_data.get("labels", [])])
current_labels = set([str(lab) for lab in current_instance.get("labels", [])])
added_labels = requested_labels - current_labels
@@ -335,7 +335,7 @@ def track_assignees(
epoch,
):
requested_assignees = set(
[str(asg) for asg in requested_data.get("assignees_list", [])]
[str(asg) for asg in requested_data.get("assignees", [])]
)
current_assignees = set([str(asg) for asg in current_instance.get("assignees", [])])
@@ -523,8 +523,8 @@ def update_issue_activity(
"description_html": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"labels": track_labels,
"assignees": track_assignees,
"estimate_point": track_estimate_points,
"archived_at": track_archive_at,
"closed_to": track_closed_to,
+1 -1
View File
@@ -27,7 +27,7 @@
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3",
"turbo": "^1.10.14"
"turbo": "^1.10.16"
},
"resolutions": {
"@types/react": "18.2.0"
@@ -23,8 +23,8 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
origin={false}
edge={false}
throttleDrag={0}
keepRatio={true}
resizable={true}
keepRatio
resizable
throttleResize={0}
onResize={({ target, width, height, delta }: any) => {
delta[0] && (target!.style.width = `${width}px`);
@@ -33,7 +33,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
onResizeEnd={() => {
updateMediaSize();
}}
scalable={true}
scalable
renderDirections={["w", "e"]}
onScale={({ target, transform }: any) => {
target!.style.transform = transform;
+41 -30
View File
@@ -1,18 +1,23 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react";
import { useDebouncedCallback } from "use-debounce";
import { DeleteImage } from '../../types/delete-image';
import {
useImperativeHandle,
useRef,
MutableRefObject,
useEffect,
} from "react";
import { DeleteImage } from "../../types/delete-image";
import { CoreEditorProps } from "../props";
import { CoreEditorExtensions } from "../extensions";
import { EditorProps } from '@tiptap/pm/view';
import { EditorProps } from "@tiptap/pm/view";
import { getTrimmedHTML } from "../../lib/utils";
import { UploadImage } from "../../types/upload-image";
const DEBOUNCE_DELAY = 1500;
import { useInitializedContent } from "./useInitializedContent";
interface CustomEditorProps {
uploadFile: UploadImage;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void;
value: string;
deleteFile: DeleteImage;
@@ -23,25 +28,37 @@ interface CustomEditorProps {
forwardedRef?: any;
}
export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, extensions = [], onChange, setIsSubmitting, debouncedUpdatesEnabled, forwardedRef, setShouldShowAlert, }: CustomEditorProps) => {
const editor = useCustomEditor({
editorProps: {
...CoreEditorProps(uploadFile, setIsSubmitting),
...editorProps,
},
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange: onChange, editor });
} else {
export const useEditor = ({
uploadFile,
deleteFile,
editorProps = {},
value,
extensions = [],
onChange,
setIsSubmitting,
forwardedRef,
setShouldShowAlert,
}: CustomEditorProps) => {
const editor = useCustomEditor(
{
editorProps: {
...CoreEditorProps(uploadFile, setIsSubmitting),
...editorProps,
},
extensions: [...CoreEditorExtensions(deleteFile), ...extensions],
content:
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}
},
},
});
[],
);
useInitializedContent(editor, value);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor;
@@ -55,12 +72,6 @@ export const useEditor = ({ uploadFile, deleteFile, editorProps = {}, value, ext
},
}));
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
if (onChange) {
onChange(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}
}, DEBOUNCE_DELAY);
if (!editor) {
return null;
}
@@ -0,0 +1,19 @@
import { Editor } from "@tiptap/react";
import { useEffect, useRef } from "react";
export const useInitializedContent = (editor: Editor | null, value: string) => {
const hasInitializedContent = useRef(false);
useEffect(() => {
if (editor) {
const cleanedValue =
typeof value === "string" && value.trim() !== "" ? value : "<p></p>";
if (cleanedValue !== "<p></p>" && !hasInitializedContent.current) {
editor.commands.setContent(cleanedValue);
hasInitializedContent.current = true;
} else if (cleanedValue === "<p></p>" && hasInitializedContent.current) {
hasInitializedContent.current = false;
}
}
}, [value, editor]);
};
@@ -1,8 +1,13 @@
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useImperativeHandle, useRef, MutableRefObject } from "react";
import {
useImperativeHandle,
useRef,
MutableRefObject,
useEffect,
} from "react";
import { CoreReadOnlyEditorExtensions } from "../../ui/read-only/extensions";
import { CoreReadOnlyEditorProps } from "../../ui/read-only/props";
import { EditorProps } from '@tiptap/pm/view';
import { EditorProps } from "@tiptap/pm/view";
interface CustomReadOnlyEditorProps {
value: string;
@@ -11,10 +16,16 @@ interface CustomReadOnlyEditorProps {
editorProps?: EditorProps;
}
export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editorProps = {} }: CustomReadOnlyEditorProps) => {
export const useReadOnlyEditor = ({
value,
forwardedRef,
extensions = [],
editorProps = {},
}: CustomReadOnlyEditorProps) => {
const editor = useCustomEditor({
editable: false,
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
content:
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
editorProps: {
...CoreReadOnlyEditorProps,
...editorProps,
@@ -22,6 +33,14 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor
extensions: [...CoreReadOnlyEditorExtensions, ...extensions],
});
const hasIntiliazedContent = useRef(false);
useEffect(() => {
if (editor && !value && !hasIntiliazedContent.current) {
editor.commands.setContent(value);
hasIntiliazedContent.current = true;
}
}, [value]);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
editorRef.current = editor;
@@ -34,7 +53,6 @@ export const useReadOnlyEditor = ({ value, forwardedRef, extensions = [], editor
},
}));
if (!editor) {
return null;
}
@@ -16,7 +16,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
const newImageSources = new Set();
const newImageSources = new Set<string>();
newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) {
newImageSources.add(node.attrs.src);
@@ -1,9 +0,0 @@
import ListItem from '@tiptap/extension-list-item'
export const CustomListItem = ListItem.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.chain().focus().splitListItem('listItem').run(),
}
},
})
@@ -1,16 +1,25 @@
import { Extension } from '@tiptap/core';
import { Extension } from "@tiptap/core";
export const EnterKeyExtension = (onEnterKeyPress?: () => void) => Extension.create({
name: 'enterKey',
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: "enterKey",
addKeyboardShortcuts() {
return {
'Enter': () => {
if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
},
}
},
});
addKeyboardShortcuts() {
return {
Enter: () => {
if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
},
"Shift-Enter": ({ editor }) =>
editor.commands.first(({ commands }) => [
() => commands.newlineInCode(),
() => commands.splitListItem("listItem"),
() => commands.createParagraphNear(),
() => commands.liftEmptyBlock(),
() => commands.splitBlock(),
]),
};
},
});
@@ -1,7 +1,5 @@
import { CustomListItem } from "./custom-list-extension";
import { EnterKeyExtension } from "./enter-key-extension";
export const LiteTextEditorExtensions = (onEnterKeyPress?: () => void) => [
CustomListItem,
EnterKeyExtension(onEnterKeyPress),
];
+1
View File
@@ -1,3 +1,4 @@
// FIXME: fix this!!!
import { Placement } from "@blueprintjs/popover2";
export interface IDropdownProps {
@@ -11,12 +11,14 @@ export interface InputColorPickerProps {
value: string | undefined;
onChange: (value: string) => void;
name: string;
className: string;
className?: string;
style?: React.CSSProperties;
placeholder: string;
}
export const InputColorPicker: React.FC<InputColorPickerProps> = (props) => {
const { value, hasError, onChange, name, className, placeholder } = props;
const { value, hasError, onChange, name, className, style, placeholder } =
props;
const [referenceElement, setReferenceElement] =
React.useState<HTMLButtonElement | null>(null);
@@ -32,12 +34,12 @@ export const InputColorPicker: React.FC<InputColorPickerProps> = (props) => {
onChange(hex);
};
const handleInputChange = (value: any) => {
onChange(value);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return (
<div className="flex items-center justify-between rounded border border-custom-border-200 px-1">
<div className="relative">
<Input
id={name}
name={name}
@@ -46,10 +48,14 @@ export const InputColorPicker: React.FC<InputColorPickerProps> = (props) => {
onChange={handleInputChange}
hasError={hasError}
placeholder={placeholder}
className={`border-none ${className}`}
className={`border-[0.5px] border-custom-border-200 ${className}`}
style={style}
/>
<Popover as="div">
<Popover
as="div"
className="absolute top-1/2 -translate-y-1/2 right-1 z-10"
>
{({ open }) => {
if (open) {
}
@@ -60,26 +66,26 @@ export const InputColorPicker: React.FC<InputColorPickerProps> = (props) => {
ref={setReferenceElement}
variant="neutral-primary"
size="sm"
className="border-none !p-1.5"
className="border-none !bg-transparent"
>
{value && value !== "" ? (
<span
className="h-3.5 w-3.5 rounded"
style={{
backgroundColor: `${value}`,
}}
/>
) : (
<svg
width={14}
height={14}
viewBox="0 0 14 14"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0.8125 13.7508C0.65 13.7508 0.515625 13.6977 0.409375 13.5914C0.303125 13.4852 0.25 13.3508 0.25 13.1883V10.8258C0.25 10.7508 0.2625 10.682 0.2875 10.6195C0.3125 10.557 0.35625 10.4945 0.41875 10.432L7.31875 3.53203L6.34375 2.55703C6.24375 2.45703 6.19688 2.32891 6.20312 2.17266C6.20938 2.01641 6.2625 1.88828 6.3625 1.78828C6.4625 1.68828 6.59063 1.63828 6.74688 1.63828C6.90313 1.63828 7.03125 1.68828 7.13125 1.78828L8.4625 3.13828L11.125 0.475781C11.2625 0.338281 11.4094 0.269531 11.5656 0.269531C11.7219 0.269531 11.8688 0.338281 12.0063 0.475781L13.525 1.99453C13.6625 2.13203 13.7313 2.27891 13.7313 2.43516C13.7313 2.59141 13.6625 2.73828 13.525 2.87578L10.8625 5.53828L12.2125 6.88828C12.3125 6.98828 12.3625 7.11328 12.3625 7.26328C12.3625 7.41328 12.3125 7.53828 12.2125 7.63828C12.1125 7.73828 11.9844 7.78828 11.8281 7.78828C11.6719 7.78828 11.5438 7.73828 11.4438 7.63828L10.4688 6.68203L3.56875 13.582C3.50625 13.6445 3.44375 13.6883 3.38125 13.7133C3.31875 13.7383 3.25 13.7508 3.175 13.7508H0.8125ZM1.375 12.6258H3.00625L9.6625 5.96953L8.03125 4.33828L1.375 10.9945V12.6258ZM10.0563 4.75078L12.3813 2.42578L11.575 1.61953L9.25 3.94453L10.0563 4.75078Z" />
</svg>
)}
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-palette"
>
<circle cx="13.5" cy="6.5" r=".5" />
<circle cx="17.5" cy="10.5" r=".5" />
<circle cx="8.5" cy="7.5" r=".5" />
<circle cx="6.5" cy="12.5" r=".5" />
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" />
</svg>
</Button>
</Popover.Button>
<Transition
@@ -0,0 +1,20 @@
import * as React from "react";
import { ISvgIcons } from "../type";
export const CircleDotFullIcon: React.FC<ISvgIcons> = ({
className = "text-current",
...rest
}) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-2`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<circle cx="8.33333" cy="8.33333" r="5.33333" stroke-linecap="round" />
<circle cx="8.33333" cy="8.33333" r="4.33333" fill="currentColor" />
</svg>
);
@@ -1,6 +1,6 @@
import * as React from "react";
import { ISvgIcons } from "./type";
import { ISvgIcons } from "../type";
export const ContrastIcon: React.FC<ISvgIcons> = ({
className = "text-current",
@@ -0,0 +1,33 @@
import * as React from "react";
import { ContrastIcon } from "./contrast-icon";
import { CircleDotFullIcon } from "./circle-dot-full-icon";
import { CircleDotDashed, Circle } from "lucide-react";
import { CYCLE_GROUP_COLORS, ICycleGroupIcon } from "./helper";
const iconComponents = {
current: ContrastIcon,
upcoming: CircleDotDashed,
completed: CircleDotFullIcon,
draft: Circle,
};
export const CycleGroupIcon: React.FC<ICycleGroupIcon> = ({
className = "",
color,
cycleGroup,
height = "12px",
width = "12px",
}) => {
const CycleIconComponent = iconComponents[cycleGroup] || ContrastIcon;
return (
<CycleIconComponent
height={height}
width={width}
color={color ?? CYCLE_GROUP_COLORS[cycleGroup]}
className={`flex-shrink-0 ${className}`}
/>
);
};
@@ -1,6 +1,6 @@
import * as React from "react";
import { ISvgIcons } from "./type";
import { ISvgIcons } from "../type";
export const DoubleCircleIcon: React.FC<ISvgIcons> = ({
className = "text-current",
+18
View File
@@ -0,0 +1,18 @@
export interface ICycleGroupIcon {
className?: string;
color?: string;
cycleGroup: TCycleGroups;
height?: string;
width?: string;
}
export type TCycleGroups = "current" | "upcoming" | "completed" | "draft";
export const CYCLE_GROUP_COLORS: {
[key in TCycleGroups]: string;
} = {
current: "#F59E0B",
upcoming: "#3F76FF",
completed: "#16A34A",
draft: "#525252",
};
+5
View File
@@ -0,0 +1,5 @@
export * from "./double-circle-icon";
export * from "./circle-dot-full-icon";
export * from "./contrast-icon";
export * from "./circle-dot-full-icon";
export * from "./cycle-group-icon";
+1 -2
View File
@@ -1,5 +1,4 @@
export * from "./user-group-icon";
export * from "./contrast-icon";
export * from "./dice-icon";
export * from "./layers-icon";
export * from "./photo-filter-icon";
@@ -7,7 +6,6 @@ export * from "./archive-icon";
export * from "./admin-profile-icon";
export * from "./create-icon";
export * from "./subscribe-icon";
export * from "./double-circle-icon";
export * from "./external-link-icon";
export * from "./copy-icon";
export * from "./layer-stack";
@@ -20,6 +18,7 @@ export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./related-icon";
export * from "./module";
export * from "./cycle";
export * from "./github-icon";
export * from "./discord-icon";
export * from "./transfer-icon";
+20 -34
View File
@@ -12,20 +12,10 @@ export interface EmailPasswordFormValues {
export interface IEmailPasswordForm {
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
buttonText?: string;
submittingButtonText?: string;
withForgetPassword?: boolean;
withSignUpLink?: boolean;
}
export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
const {
onSubmit,
buttonText = "Sign in",
submittingButtonText = "Signing in...",
withForgetPassword = false,
withSignUpLink = false,
} = props;
const { onSubmit } = props;
// router
const router = useRouter();
// form info
@@ -92,17 +82,15 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
)}
/>
</div>
{withForgetPassword && (
<div className="text-right text-xs">
<button
type="button"
onClick={() => router.push("/accounts/forgot-password")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
Forgot your password?
</button>
</div>
)}
<div className="text-right text-xs">
<button
type="button"
onClick={() => router.push("/accounts/forgot-password")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
Forgot your password?
</button>
</div>
<div>
<Button
variant="primary"
@@ -112,20 +100,18 @@ export const EmailPasswordForm: React.FC<IEmailPasswordForm> = (props) => {
disabled={!isValid && isDirty}
loading={isSubmitting}
>
{isSubmitting ? submittingButtonText : buttonText}
{isSubmitting ? "Signing in..." : "Sign in"}
</Button>
</div>
{withSignUpLink && (
<div className="text-xs">
<button
type="button"
onClick={() => router.push("/accounts/sign-up")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
{"Don't have an account? Sign Up"}
</button>
</div>
)}
<div className="text-xs">
<button
type="button"
onClick={() => router.push("/accounts/sign-up")}
className="text-custom-text-200 hover:text-custom-primary-100"
>
{"Don't have an account? Sign Up"}
</button>
</div>
</form>
</>
);
+3 -3
View File
@@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form";
// ui
import { Button, Input } from "@plane/ui";
// types
export type EmailPasswordSignUpFormValues = {
type EmailPasswordFormValues = {
email: string;
password?: string;
confirm_password: string;
@@ -12,7 +12,7 @@ export type EmailPasswordSignUpFormValues = {
};
type Props = {
onSubmit: (formData: EmailPasswordSignUpFormValues) => Promise<void>;
onSubmit: (formData: EmailPasswordFormValues) => Promise<void>;
};
export const EmailSignUpForm: React.FC<Props> = (props) => {
@@ -23,7 +23,7 @@ export const EmailSignUpForm: React.FC<Props> = (props) => {
control,
watch,
formState: { errors, isSubmitting, isValid, isDirty },
} = useForm<EmailPasswordSignUpFormValues>({
} = useForm<EmailPasswordFormValues>({
defaultValues: {
email: "",
password: "",
@@ -144,7 +144,7 @@ export const CommandModal: React.FC<Props> = (props) => {
} else {
updatedAssignees.push(assignee);
}
updateIssue({ assignees_list: updatedAssignees });
updateIssue({ assignees: updatedAssignees });
};
const redirect = (path: string) => {
@@ -79,7 +79,7 @@ export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }
);
const handleIssueAssignees = (assignee: string) => {
const updatedAssignees = issue.assignees_list ?? [];
const updatedAssignees = issue.assignees ?? [];
if (updatedAssignees.includes(assignee)) {
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
@@ -87,7 +87,7 @@ export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }
updatedAssignees.push(assignee);
}
updateIssue({ assignees_list: updatedAssignees });
updateIssue({ assignees: updatedAssignees });
setIsPaletteOpen(false);
};
@@ -36,7 +36,7 @@ type Props = {
module?: IModule;
roundedTab?: boolean;
noBackground?: boolean;
isPeekModuleDetails?: boolean;
isPeekView?: boolean;
};
export const SidebarProgressStats: React.FC<Props> = ({
@@ -46,7 +46,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
module,
roundedTab,
noBackground,
isPeekModuleDetails = false,
isPeekView = false,
}) => {
const { filters, setFilters } = useIssuesView();
@@ -154,7 +154,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
}
completed={assignee.completed_issues}
total={assignee.total_issues}
{...(!isPeekModuleDetails && {
{...(!isPeekView && {
onClick: () => {
if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
setFilters({
@@ -213,7 +213,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
}
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekModuleDetails && {
{...(!isPeekView && {
onClick: () => {
if (filters.labels?.includes(label.label_id ?? ""))
setFilters({
@@ -1,27 +1,40 @@
import { FC } from "react";
import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { useTheme } from "next-themes";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Button, InputColorPicker } from "@plane/ui";
// types
import { IUserTheme } from "types";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {};
const inputRules = {
required: "Background color is required",
minLength: {
value: 7,
message: "Enter a valid hex code of 6 characters",
},
maxLength: {
value: 7,
message: "Enter a valid hex code of 6 characters",
},
pattern: {
value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
message: "Enter a valid hex code of 6 characters",
},
};
export const CustomThemeSelector: FC<Props> = observer(() => {
export const CustomThemeSelector: React.FC = observer(() => {
const { user: userStore } = useMobxStore();
const userTheme = userStore?.currentUser?.theme;
// hooks
const { setTheme } = useTheme();
const {
control,
formState: { errors, isSubmitting },
handleSubmit,
control,
watch,
} = useForm<IUserTheme>({
defaultValues: {
background: userTheme?.background !== "" ? userTheme?.background : "#0d101b",
@@ -51,100 +64,151 @@ export const CustomThemeSelector: FC<Props> = observer(() => {
return userStore.updateCurrentUser({ theme: payload });
};
const handleValueChange = (val: string | undefined, onChange: any) => {
let hex = val;
// prepend a hashtag if it doesn't exist
if (val && val[0] !== "#") hex = `#${val}`;
onChange(hex);
};
return (
<form onSubmit={handleSubmit(handleUpdateTheme)}>
<div className="space-y-5">
<h3 className="text-lg font-semibold text-custom-text-100">Customize your theme</h3>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
<div className="grid grid-cols-1 gap-x-8 gap-y-4 sm:grid-cols-2 md:grid-cols-3">
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Background color</h3>
<Controller
control={control}
name="background"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="background"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.background)}
/>
)}
/>
<div className="w-full">
<Controller
control={control}
name="background"
rules={inputRules}
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="background"
value={value}
onChange={(val) => handleValueChange(val, onChange)}
placeholder="#0d101b"
className="w-full"
style={{
backgroundColor: value,
color: watch("text"),
}}
hasError={Boolean(errors?.background)}
/>
)}
/>
{errors.background && <p className="text-xs text-red-500 mt-1">{errors.background.message}</p>}
</div>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Text color</h3>
<Controller
control={control}
name="text"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="text"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.text)}
/>
)}
/>
<div className="w-full">
<Controller
control={control}
name="text"
rules={inputRules}
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="text"
value={value}
onChange={(val) => handleValueChange(val, onChange)}
placeholder="#c5c5c5"
className="w-full"
style={{
backgroundColor: watch("background"),
color: value,
}}
hasError={Boolean(errors?.text)}
/>
)}
/>
{errors.text && <p className="text-xs text-red-500 mt-1">{errors.text.message}</p>}
</div>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Primary(Theme) color</h3>
<Controller
control={control}
name="primary"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="primary"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.primary)}
/>
)}
/>
<div className="w-full">
<Controller
control={control}
name="primary"
rules={inputRules}
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="primary"
value={value}
onChange={(val) => handleValueChange(val, onChange)}
placeholder="#3f76ff"
className="w-full"
style={{
backgroundColor: value,
color: watch("text"),
}}
hasError={Boolean(errors?.primary)}
/>
)}
/>
{errors.primary && <p className="text-xs text-red-500 mt-1">{errors.primary.message}</p>}
</div>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar background color</h3>
<Controller
control={control}
name="sidebarBackground"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarBackground"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarBackground)}
/>
<div className="w-full">
<Controller
control={control}
name="sidebarBackground"
rules={inputRules}
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarBackground"
value={value}
onChange={(val) => handleValueChange(val, onChange)}
placeholder="#0d101b"
className="w-full"
style={{
backgroundColor: value,
color: watch("sidebarText"),
}}
hasError={Boolean(errors?.sidebarBackground)}
/>
)}
/>
{errors.sidebarBackground && (
<p className="text-xs text-red-500 mt-1">{errors.sidebarBackground.message}</p>
)}
/>
</div>
</div>
<div className="flex flex-col items-start gap-2">
<h3 className="text-left text-sm font-medium text-custom-text-200">Sidebar text color</h3>
<Controller
control={control}
name="sidebarText"
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarText"
value={value}
onChange={onChange}
className=""
placeholder="#ffffff"
hasError={Boolean(errors?.sidebarText)}
/>
)}
/>
<div className="w-full">
<Controller
control={control}
name="sidebarText"
rules={inputRules}
render={({ field: { value, onChange } }) => (
<InputColorPicker
name="sidebarText"
value={value}
onChange={(val) => handleValueChange(val, onChange)}
placeholder="#c5c5c5"
className="w-full"
style={{
backgroundColor: watch("sidebarBackground"),
color: value,
}}
hasError={Boolean(errors?.sidebarText)}
/>
)}
/>
{errors.sidebarText && <p className="text-xs text-red-500 mt-1">{errors.sidebarText.message}</p>}
</div>
</div>
</div>
</div>
@@ -198,8 +198,7 @@ export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
if (onSuccess) await onSuccess(res);
if (formData.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (formData.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
})
@@ -0,0 +1,55 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CycleDetailsSidebar } from "./sidebar";
type Props = {
projectId: string;
workspaceSlug: string;
};
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug }) => {
const router = useRouter();
const { peekCycle } = router.query;
const ref = React.useRef(null);
const { cycle: cycleStore } = useMobxStore();
const { fetchCycleWithId } = cycleStore;
const handleClose = () => {
delete router.query.peekCycle;
router.push({
pathname: router.pathname,
query: { ...router.query },
});
};
useEffect(() => {
if (!peekCycle) return;
fetchCycleWithId(workspaceSlug, projectId, peekCycle.toString());
}, [fetchCycleWithId, peekCycle, projectId, workspaceSlug]);
return (
<>
{peekCycle && (
<div
ref={ref}
className="flex flex-col gap-3.5 h-full w-[24rem] z-10 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-6 py-3.5 duration-300 flex-shrink-0"
style={{
boxShadow:
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<CycleDetailsSidebar cycleId={peekCycle?.toString() ?? ""} handleClose={handleClose} />
</div>
)}
</>
);
});
+169 -314
View File
@@ -1,64 +1,32 @@
import { FC, MouseEvent, useState } from "react";
import { useRouter } from "next/router";
// next imports
import Link from "next/link";
// headless ui
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// components
import { SingleProgressStats } from "components/core";
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
// ui
import { AssigneesList } from "components/ui/avatar";
import { CustomMenu, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
import { CustomMenu, Tooltip, LayersIcon, CycleGroupIcon } from "@plane/ui";
// icons
import {
AlarmClock,
AlertTriangle,
ArrowRight,
CalendarDays,
ChevronDown,
LinkIcon,
Pencil,
Star,
Target,
Trash2,
} from "lucide-react";
import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
// helpers
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import {
getDateRangeStatus,
findHowManyDaysLeft,
renderShortDate,
renderShortMonthDate,
} from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
// store
import { useMobxStore } from "lib/mobx/store-provider";
const stateGroups = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];
// constants
import { CYCLE_STATUS } from "constants/cycle";
export interface ICyclesBoardCard {
workspaceSlug: string;
@@ -81,7 +49,34 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const handleCopyText = () => {
const router = useRouter();
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const cycleTotalIssues =
cycle.backlog_issues +
cycle.unstarted_issues +
cycle.started_issues +
cycle.completed_issues +
cycle.cancelled_issues;
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const issueCount = cycle
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycle.completed_issues
? cycleTotalIssues > 1
? `${cycleTotalIssues} Issues`
: `${cycleTotalIssues} Issue`
: `${cycle.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
@@ -93,21 +88,6 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
});
};
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const groupedIssues: any = {
backlog: cycle.backlog_issues,
unstarted: cycle.unstarted_issues,
started: cycle.started_issues,
completed: cycle.completed_issues,
cancelled: cycle.cancelled_issues,
};
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
@@ -134,6 +114,29 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
});
};
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setUpdateModal(true);
};
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setDeleteModal(true);
};
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
const { query } = router;
e.preventDefault();
e.stopPropagation();
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycle.id },
});
};
return (
<div>
<CycleCreateUpdateModal
@@ -152,267 +155,119 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = (props) => {
projectId={projectId}
/>
<div className="flex flex-col rounded-[10px] bg-custom-background-100 border border-custom-border-200 text-xs shadow">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full">
<div className="flex h-full flex-col gap-4 rounded-b-[10px] p-4">
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-1">
<span className="h-5 w-5">
<ContrastIcon
className="h-5 w-5"
color={`${
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
</span>
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
<h3 className="break-words text-lg font-semibold">{truncateText(cycle.name, 15)}</h3>
</Tooltip>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="flex flex-col justify-between p-4 h-44 w-full min-w-[250px] text-sm rounded bg-custom-background-100 border border-custom-border-100 hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3">
<span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span>
<span className="flex items-center gap-1 capitalize">
<Tooltip tooltipContent={cycle.name} position="top">
<span className="text-base font-medium truncate">{cycle.name}</span>
</Tooltip>
</div>
<div className="flex items-center gap-2">
{currentCycle && (
<span
className={`rounded-full px-1.5 py-0.5
${
cycleStatus === "current"
? "bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/5 text-neutral-400"
: ""
}`}
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
<RunningIcon className="h-4 w-4" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} Days Left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1 whitespace-nowrap">
<AlarmClock className="h-4 w-4" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} Days Left
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues - cycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
<AlertTriangle className="h-3.5 w-3.5" />
</span>
</Tooltip>
)}{" "}
Completed
</span>
) : (
cycleStatus
)}
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
{cycle.is_favorite ? (
<button onClick={handleRemoveFromFavorites}>
<Star className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button onClick={handleAddToFavorites}>
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
</span>
</div>
<div className="flex h-4 items-center justify-start gap-5 text-custom-text-200">
{cycleStatus !== "draft" && (
<>
<div className="flex items-start gap-1">
<CalendarDays className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<ArrowRight className="h-4 w-4" />
<div className="flex items-start gap-1">
<Target className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</>
)}
</div>
<div className="flex justify-between items-end">
<div className="flex flex-col gap-2 text-xs text-custom-text-200">
<div className="flex items-center gap-2">
<div className="w-16">Creator:</div>
<div className="flex items-center gap-2.5 text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycle.owned_by.display_name}</span>
</div>
</div>
<div className="flex h-5 items-center gap-2">
<div className="w-16">Members:</div>
{cycle.assignees.length > 0 ? (
<div className="flex items-center gap-1 text-custom-text-200">
<AssigneesList users={cycle.assignees} length={4} />
</div>
) : (
"No members"
)}
</div>
</div>
<div className="flex items-center">
{!isCompleted && (
<button
onClick={(e) => {
e.preventDefault();
setUpdateModal(true);
}}
className="cursor-pointer rounded p-1 text-custom-text-200 duration-300 hover:bg-custom-background-80"
>
<Pencil className="h-4 w-4" />
</button>
)}
<CustomMenu width="auto" verticalEllipsis>
{!isCompleted && (
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
setDeleteModal(true);
}}
>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
handleCopyText();
}}
>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<button onClick={openCycleOverview}>
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
</div>
</a>
</Link>
</div>
<div className="flex h-full flex-col rounded-b-[10px]">
<Disclosure>
{({ open }) => (
<div
className={`flex h-full w-full flex-col rounded-b-[10px] border-t border-custom-border-200 bg-custom-background-80 text-custom-text-200 ${
open ? "" : "flex-row"
}`}
>
<div className="flex w-full items-center gap-2 px-4 py-1">
<span>Progress</span>
<Tooltip
tooltipContent={
<div className="flex w-56 flex-col">
{Object.keys(groupedIssues).map((group, index) => (
<SingleProgressStats
key={index}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: stateGroups[index].color,
}}
/>
<span className="text-xs capitalize">{group}</span>
</div>
}
completed={groupedIssues[group]}
total={cycle.total_issues}
/>
))}
</div>
}
position="bottom"
>
<div className="flex w-full items-center">
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
</div>
</Tooltip>
<Disclosure.Button>
<span className="p-1">
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</span>
</Disclosure.Button>
</div>
<Transition show={open}>
<Disclosure.Panel>
<div className="overflow-hidden rounded-b-md bg-custom-background-80 py-3 shadow">
<div className="col-span-2 space-y-3 px-4">
<div className="space-y-3 text-xs">
{stateGroups.map((group) => (
<div key={group.key} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="block h-2 w-2 rounded-full"
style={{
backgroundColor: group.color,
}}
/>
<h6 className="text-xs">{group.title}</h6>
</div>
<div>
<span>
{cycle[group.key as keyof ICycle] as number}{" "}
<span className="text-custom-text-200">
-{" "}
{cycle.total_issues > 0
? `${Math.round(
((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100
)}%`
: "0%"}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</div>
</Disclosure.Panel>
</Transition>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-custom-text-200">
<LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount}</span>
</div>
)}
</Disclosure>
</div>
</div>
{cycle.assignees.length > 0 && (
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
<div className="flex items-center gap-1 cursor-default">
<AssigneesList users={cycle.assignees} length={3} />
</div>
</Tooltip>
)}
</div>
<Tooltip
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
position="top-left"
>
<div className="flex items-center w-full">
<div
className="bar relative h-1.5 w-full rounded bg-custom-background-90"
style={{
boxShadow: "1px 1px 4px 0px rgba(161, 169, 191, 0.35) inset",
}}
>
<div
className="absolute top-0 left-0 h-1.5 rounded bg-blue-600 duration-300"
style={{
width: `${isNaN(completionPercentage) ? 0 : completionPercentage.toFixed(0)}%`,
}}
/>
</div>
</div>
</Tooltip>
<div className="flex items-center justify-between">
<span className="text-xs text-custom-text-300">
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")} -{" "}
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</span>
<div className="flex items-center gap-1.5 z-10">
{cycle.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites}>
<Star className="h-3.5 w-3.5 text-custom-text-200" />
</button>
)}
<CustomMenu width="auto" ellipsis className="z-10">
{!isCompleted && (
<>
<CustomMenu.MenuItem onClick={handleEditCycle}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" />
<span>Edit cycle</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
</div>
</a>
</Link>
</div>
);
};
+24 -9
View File
@@ -2,26 +2,41 @@ import { FC } from "react";
// types
import { ICycle } from "types";
// components
import { CyclesBoardCard } from "components/cycles";
import { CyclePeekOverview, CyclesBoardCard } from "components/cycles";
export interface ICyclesBoard {
cycles: ICycle[];
filter: string;
workspaceSlug: string;
projectId: string;
peekCycle: string;
}
export const CyclesBoard: FC<ICyclesBoard> = (props) => {
const { cycles, filter, workspaceSlug, projectId } = props;
const { cycles, filter, workspaceSlug, projectId, peekCycle } = props;
return (
<div className="grid grid-cols-1 gap-9 lg:grid-cols-2 xl:grid-cols-3">
<>
{cycles.length > 0 ? (
<>
{cycles.map((cycle) => (
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
))}
</>
<div className="h-full w-full">
<div className="flex justify-between h-full w-full">
<div
className={`grid grid-cols-1 gap-6 p-8 h-full w-full overflow-y-auto ${
peekCycle
? "lg:grid-cols-1 xl:grid-cols-2 3xl:grid-cols-3"
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
} auto-rows-max transition-all `}
>
{cycles.map((cycle) => (
<CyclesBoardCard key={cycle.id} workspaceSlug={workspaceSlug} projectId={projectId} cycle={cycle} />
))}
</div>
<CyclePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
</div>
</div>
) : (
<div className="h-full grid place-items-center text-center">
<div className="space-y-2">
@@ -50,6 +65,6 @@ export const CyclesBoard: FC<ICyclesBoard> = (props) => {
</div>
</div>
)}
</div>
</>
);
};
+164 -267
View File
@@ -1,29 +1,30 @@
import { FC, MouseEvent, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
// stores
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// components
import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles";
import { AssigneesList } from "components/ui";
// ui
import { CustomMenu, RadialProgressBar, Tooltip, LinearProgressIndicator, ContrastIcon, RunningIcon } from "@plane/ui";
import { CustomMenu, Tooltip, CircularProgressIndicator, CycleGroupIcon } from "@plane/ui";
// icons
import {
AlarmClock,
AlertTriangle,
ArrowRight,
CalendarDays,
LinkIcon,
Pencil,
Star,
Target,
Trash2,
} from "lucide-react";
import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
// helpers
import { getDateRangeStatus, renderShortDateWithYearFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
import {
getDateRangeStatus,
findHowManyDaysLeft,
renderShortDate,
renderShortMonthDate,
} from "helpers/date-time.helper";
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { ICycle } from "types";
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { CYCLE_STATUS } from "constants/cycle";
type TCyclesListItem = {
cycle: ICycle;
@@ -35,34 +36,6 @@ type TCyclesListItem = {
projectId: string;
};
const stateGroups = [
{
key: "backlog_issues",
title: "Backlog",
color: "#dee2e6",
},
{
key: "unstarted_issues",
title: "Unstarted",
color: "#26b5ce",
},
{
key: "started_issues",
title: "Started",
color: "#f7ae59",
},
{
key: "cancelled_issues",
title: "Cancelled",
color: "#d687ff",
},
{
key: "completed_issues",
title: "Completed",
color: "#09a953",
},
];
export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const { cycle, workspaceSlug, projectId } = props;
// store
@@ -78,7 +51,28 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
const handleCopyText = () => {
const router = useRouter();
const cycleTotalIssues =
cycle.backlog_issues +
cycle.unstarted_issues +
cycle.started_issues +
cycle.completed_issues +
cycle.cancelled_issues;
const renderDate = cycle.start_date || cycle.end_date;
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycle.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const handleCopyText = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`).then(() => {
@@ -90,13 +84,6 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
});
};
const progressIndicatorData = stateGroups.map((group, index) => ({
id: index,
name: group.title,
value: cycle.total_issues > 0 ? ((cycle[group.key as keyof ICycle] as number) / cycle.total_issues) * 100 : 0,
color: group.color,
}));
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
@@ -123,224 +110,31 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
});
};
const handleEditCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setUpdateModal(true);
};
const handleDeleteCycle = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setDeleteModal(true);
};
const openCycleOverview = (e: MouseEvent<HTMLButtonElement>) => {
const { query } = router;
e.preventDefault();
e.stopPropagation();
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycle.id },
});
};
return (
<>
<div className="relative flex items-center gap-1 hover:bg-custom-background-80 transition-all rounded px-2 pl-3">
<div className="w-full text-xs py-3">
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="w-full h-full relative overflow-hidden flex items-center gap-2">
{/* left content */}
<div className="relative flex items-center gap-2 overflow-hidden">
{/* cycle state */}
<div className="flex-shrink-0">
<ContrastIcon
className="h-5 w-5"
color={`${
cycleStatus === "current"
? "#09A953"
: cycleStatus === "upcoming"
? "#F7AE59"
: cycleStatus === "completed"
? "#3F76FF"
: cycleStatus === "draft"
? "rgb(var(--color-text-200))"
: ""
}`}
/>
</div>
{/* cycle title and description */}
<div className="max-w-xl">
<Tooltip tooltipContent={cycle.name} className="break-words" position="top-left">
<div className="text-base font-semibold line-clamp-1 pr-5 overflow-hidden break-words">
{cycle.name}
</div>
</Tooltip>
{cycle.description && (
<div className="mt-1 text-custom-text-200 break-words w-full line-clamp-2">{cycle.description}</div>
)}
</div>
</div>
{/* right content */}
<div className="ml-auto flex-shrink-0 relative flex items-center gap-3 p-2">
{/* cycle status */}
<div
className={`rounded-full px-2 py-1
${
cycleStatus === "current"
? "bg-green-600/10 text-green-600"
: cycleStatus === "upcoming"
? "bg-orange-300/10 text-orange-300"
: cycleStatus === "completed"
? "bg-blue-500/10 text-blue-500"
: cycleStatus === "draft"
? "bg-neutral-400/10 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex items-center gap-1 whitespace-nowrap">
<RunningIcon className="h-3.5 w-3.5" />
{findHowManyDaysLeft(cycle.end_date ?? new Date())} days left
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex items-center gap-1">
<AlarmClock className="h-3.5 w-3.5" />
{findHowManyDaysLeft(cycle.start_date ?? new Date())} days left
</span>
) : cycleStatus === "completed" ? (
<span className="flex items-center gap-1">
{cycle.total_issues - cycle.completed_issues > 0 && (
<Tooltip
tooltipContent={`${cycle.total_issues - cycle.completed_issues} more pending ${
cycle.total_issues - cycle.completed_issues === 1 ? "issue" : "issues"
}`}
>
<span>
<AlertTriangle className="h-3.5 w-3.5" />
</span>
</Tooltip>
)}{" "}
Completed
</span>
) : (
cycleStatus
)}
</div>
{/* cycle start_date and target_date */}
{cycleStatus !== "draft" && (
<div className="flex items-center justify-start gap-2 text-custom-text-200">
<div className="flex items-start gap-1 whitespace-nowrap">
<CalendarDays className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(startDate)}</span>
</div>
<ArrowRight className="h-4 w-4" />
<div className="flex items-start gap-1 whitespace-nowrap">
<Target className="h-4 w-4" />
<span>{renderShortDateWithYearFormat(endDate)}</span>
</div>
</div>
)}
{/* cycle created by */}
<div className="flex items-center text-custom-text-200">
{cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? (
<img
src={cycle.owned_by.avatar}
height={16}
width={16}
className="rounded-full"
alt={cycle.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-300 capitalize text-white">
{cycle.owned_by.display_name.charAt(0)}
</span>
)}
</div>
{/* cycle progress */}
<Tooltip
position="top-right"
tooltipContent={
<div className="flex w-80 items-center gap-2 px-4 py-1">
<span>Progress</span>
<LinearProgressIndicator data={progressIndicatorData} />
</div>
}
>
<span
className={`rounded-md px-1.5 py-1
${
cycleStatus === "current"
? "border border-green-600 bg-green-600/5 text-green-600"
: cycleStatus === "upcoming"
? "border border-orange-300 bg-orange-300/5 text-orange-300"
: cycleStatus === "completed"
? "border border-blue-500 bg-blue-500/5 text-blue-500"
: cycleStatus === "draft"
? "border border-neutral-400 bg-neutral-400/5 text-neutral-400"
: ""
}`}
>
{cycleStatus === "current" ? (
<span className="flex gap-1 whitespace-nowrap">
{cycle.total_issues > 0 ? (
<>
<RadialProgressBar progress={(cycle.completed_issues / cycle.total_issues) * 100} />
<span>{Math.floor((cycle.completed_issues / cycle.total_issues) * 100)} %</span>
</>
) : (
<span className="normal-case">No issues present</span>
)}
</span>
) : cycleStatus === "upcoming" ? (
<span className="flex gap-1">
<RadialProgressBar progress={100} /> Yet to start
</span>
) : cycleStatus === "completed" ? (
<span className="flex gap-1">
<RadialProgressBar progress={100} />
<span>{100} %</span>
</span>
) : (
<span className="flex gap-1">
<RadialProgressBar progress={(cycle.total_issues / cycle.completed_issues) * 100} />
{cycleStatus}
</span>
)}
</span>
</Tooltip>
{/* cycle favorite */}
{cycle.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-4 w-4 text-orange-400" fill="#f6ad55" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites}>
<Star className="h-4 w-4 " color="rgb(var(--color-text-200))" />
</button>
)}
</div>
</a>
</Link>
</div>
<div className="flex-shrink-0">
<CustomMenu width="auto" verticalEllipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={() => setUpdateModal(true)}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit Cycle</span>
</span>
</CustomMenu.MenuItem>
)}
{!isCompleted && (
<CustomMenu.MenuItem onClick={() => setDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</div>
<CycleCreateUpdateModal
data={cycle}
isOpen={updateModal}
@@ -348,7 +142,6 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
<CycleDeleteModal
cycle={cycle}
isOpen={deleteModal}
@@ -356,6 +149,110 @@ export const CyclesListItem: FC<TCyclesListItem> = (props) => {
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle.id}`}>
<a className="group flex items-center justify-between gap-5 px-10 py-6 h-16 w-full text-sm bg-custom-background-100 border-b border-custom-border-100 hover:bg-custom-background-90">
<div className="flex items-center gap-3 w-full truncate">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{isCompleted ? (
<span className="text-sm text-custom-primary-100">{`!`}</span>
) : progress === 100 ? (
<Check className="h-3 w-3 text-custom-primary-100 stroke-[2]" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<div className="flex items-center gap-2.5">
<span className="flex-shrink-0">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />
</span>
<Tooltip tooltipContent={cycle.name} position="top">
<span className="text-base font-medium truncate">{cycle.name}</span>
</Tooltip>
</div>
</div>
<button onClick={openCycleOverview} className="flex-shrink-0 hidden group-hover:flex z-10">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="flex items-center gap-2.5 justify-end w-full md:w-auto md:flex-shrink-0 ">
<div className="flex items-center justify-center">
{currentCycle && (
<span
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycle.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
)}
</div>
{renderDate && (
<span className="flex items-center justify-center gap-2 w-28 text-xs text-custom-text-300">
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
{" - "}
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</span>
)}
<Tooltip tooltipContent={`${cycle.assignees.length} Members`}>
<div className="flex items-center justify-center gap-1 cursor-default w-16">
{cycle.assignees.length > 0 ? (
<AssigneesList users={cycle.assignees} length={2} />
) : (
<span className="flex items-end justify-center h-5 w-5 bg-custom-background-80 rounded-full border border-dashed border-custom-text-400">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{cycle.is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 text-amber-500 fill-current" />
</button>
) : (
<button type="button" onClick={handleAddToFavorites}>
<Star className="h-3.5 w-3.5 text-custom-text-200" />
</button>
)}
<CustomMenu width="auto" ellipsis className="z-10">
{!isCompleted && (
<>
<CustomMenu.MenuItem onClick={handleEditCycle}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" />
<span>Edit cycle</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete module</span>
</span>
</CustomMenu.MenuItem>
</>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
</a>
</Link>
</>
);
};
+14 -9
View File
@@ -1,6 +1,7 @@
import { FC } from "react";
// components
import { CyclesListItem } from "./cycles-list-item";
import { CyclePeekOverview, CyclesListItem } from "components/cycles";
// ui
import { Loader } from "@plane/ui";
// types
@@ -17,18 +18,22 @@ export const CyclesList: FC<ICyclesList> = (props) => {
const { cycles, filter, workspaceSlug, projectId } = props;
return (
<div>
<>
{cycles ? (
<>
{cycles.length > 0 ? (
<div className="divide-y divide-custom-border-200">
{cycles.map((cycle) => (
<div className="hover:bg-custom-background-80" key={cycle.id}>
<div className="flex flex-col border-custom-border-200">
<div className="h-full overflow-y-auto">
<div className="flex justify-between h-full w-full">
<div className="flex flex-col h-full w-full overflow-y-auto">
{cycles.map((cycle) => (
<CyclesListItem cycle={cycle} workspaceSlug={workspaceSlug} projectId={projectId} />
</div>
))}
</div>
))}
<CyclePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
</div>
</div>
) : (
<div className="h-full grid place-items-center text-center">
@@ -68,6 +73,6 @@ export const CyclesList: FC<ICyclesList> = (props) => {
<Loader.Item height="50px" />
</Loader>
)}
</div>
</>
);
};
+9 -2
View File
@@ -15,10 +15,11 @@ export interface ICyclesView {
layout: TCycleLayout;
workspaceSlug: string;
projectId: string;
peekCycle: string;
}
export const CyclesView: FC<ICyclesView> = observer((props) => {
const { filter, layout, workspaceSlug, projectId } = props;
const { filter, layout, workspaceSlug, projectId, peekCycle } = props;
// store
const { cycle: cycleStore } = useMobxStore();
@@ -50,7 +51,13 @@ export const CyclesView: FC<ICyclesView> = observer((props) => {
{layout === "board" && (
<>
{!isLoading ? (
<CyclesBoard cycles={cyclesList} filter={filter} workspaceSlug={workspaceSlug} projectId={projectId} />
<CyclesBoard
cycles={cyclesList}
filter={filter}
workspaceSlug={workspaceSlug}
projectId={projectId}
peekCycle={peekCycle}
/>
) : (
<Loader className="grid grid-cols-1 gap-9 md:grid-cols-2 lg:grid-cols-3">
<Loader.Item height="200px" />
+2
View File
@@ -17,3 +17,5 @@ export * from "./cycles-board";
export * from "./cycles-board-card";
export * from "./cycles-gantt";
export * from "./delete-modal";
export * from "./cycle-peek-overview";
export * from "./cycles-list-item";
+252 -304
View File
@@ -15,36 +15,29 @@ import { SidebarProgressStats } from "components/core";
import ProgressChart from "components/core/sidebar/progress-chart";
import { CycleDeleteModal } from "components/cycles/delete-modal";
// ui
import { CustomRangeDatePicker } from "components/ui";
import { CustomMenu, Loader, ProgressBar } from "@plane/ui";
import { Avatar, CustomRangeDatePicker } from "components/ui";
import { CustomMenu, Loader, LayersIcon } from "@plane/ui";
// icons
import {
CalendarDays,
ChevronDown,
File,
MoveRight,
LinkIcon,
PieChart,
Trash2,
UserCircle2,
AlertCircle,
} from "lucide-react";
import { ChevronDown, LinkIcon, Trash2, UserCircle2, AlertCircle, ChevronRight, MoveRight } from "lucide-react";
// helpers
import { capitalizeFirstLetter, copyUrlToClipboard } from "helpers/string.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import {
findHowManyDaysLeft,
getDateRangeStatus,
isDateGreaterThanToday,
renderDateFormat,
renderShortDateWithYearFormat,
renderShortDate,
renderShortMonthDate,
} from "helpers/date-time.helper";
// types
import { ICycle } from "types";
// fetch-keys
import { CYCLE_DETAILS } from "constants/fetch-keys";
import { CYCLE_STATUS } from "constants/cycle";
type Props = {
isOpen: boolean;
cycleId: string;
handleClose: () => void;
};
// services
@@ -52,12 +45,12 @@ const cycleService = new CycleService();
// TODO: refactor the whole component
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { isOpen, cycleId } = props;
const { cycleId, handleClose } = props;
const [cycleDeleteModal, setCycleDeleteModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug, projectId, peekCycle } = router.query;
const { user: userStore, cycle: cycleDetailsStore } = useMobxStore();
@@ -280,6 +273,22 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
if (!cycleDetails) return null;
const endDate = new Date(cycleDetails.end_date ?? "");
const startDate = new Date(cycleDetails.start_date ?? "");
const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const issueCount =
cycleDetails.total_issues === 0
? "0 Issue"
: cycleDetails.total_issues === cycleDetails.completed_issues
? cycleDetails.total_issues > 1
? `${cycleDetails.total_issues}`
: `${cycleDetails.total_issues}`
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
return (
<>
{cycleDetails && workspaceSlug && projectId && (
@@ -291,327 +300,266 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
projectId={projectId.toString()}
/>
)}
<div
className={`fixed top-[66px] ${
isOpen ? "right-0" : "-right-[24rem]"
} h-full w-[24rem] overflow-y-auto border-l border-custom-border-200 bg-custom-sidebar-background-100 pt-5 pb-10 duration-300`}
>
{cycleDetails ? (
<>
<div className="flex flex-col items-start justify-center">
<div className="flex gap-2.5 px-5 text-sm">
<div className="flex items-center">
<span className="flex items-center rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-center text-xs capitalize">
{capitalizeFirstLetter(cycleStatus)}
</span>
</div>
<div className="relative flex h-full w-52 items-center gap-2">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({}) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex h-full items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycleDetails.start_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3" />
<span>
{renderShortDateWithYearFormat(
new Date(`${watch("start_date") ? watch("start_date") : cycleDetails?.start_date}`),
"Start date"
)}
</span>
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<span>
<MoveRight className="h-3 w-3 text-custom-text-200" />
</span>
<Popover className="flex h-full items-center justify-center rounded-lg">
{({}) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className={`group flex items-center gap-2 whitespace-nowrap rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-2 py-1 text-xs ${
cycleDetails.end_date ? "" : "text-custom-text-200"
}`}
>
<CalendarDays className="h-3 w-3" />
{cycleDetails ? (
<>
<div className="flex items-center justify-between w-full">
<div>
{peekCycle && (
<button
className="flex items-center justify-center h-5 w-5 rounded-full bg-custom-border-300"
onClick={() => handleClose()}
>
<ChevronRight className="h-3 w-3 text-white stroke-2" />
</button>
)}
</div>
<div className="flex items-center gap-3.5">
<button onClick={handleCopyText}>
<LinkIcon className="h-3 w-3 text-custom-text-300" />
</button>
{!isCompleted && (
<CustomMenu width="lg" ellipsis>
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</div>
<span>
{renderShortDateWithYearFormat(
new Date(`${watch("end_date") ? watch("end_date") : cycleDetails?.end_date}`),
"End date"
)}
</span>
</Popover.Button>
<div className="flex flex-col gap-3">
<h4 className="text-xl font-semibold break-words w-full text-custom-text-100">{cycleDetails.name}</h4>
<div className="flex items-center gap-5">
{currentCycle && (
<span
className="flex items-center justify-center text-xs text-center h-6 w-20 rounded-sm"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current"
? `${findHowManyDaysLeft(cycleDetails.end_date ?? new Date())} ${currentCycle.label}`
: `${currentCycle.label}`}
</span>
)}
<div className="relative flex h-full w-52 items-center gap-2.5">
<Popover className="flex h-full items-center justify-center rounded-lg">
{({}) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className="text-sm text-custom-text-300 font-medium cursor-default"
>
{areYearsEqual ? renderShortDate(startDate, "_ _") : renderShortMonthDate(startDate, "_ _")}
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("start_date") ? watch("start_date") : cycleDetails?.start_date}
onChange={(val) => {
if (val) {
handleStartDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
maxDate={new Date(`${watch("end_date")}`)}
selectsStart
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<MoveRight className="h-4 w-4 text-custom-text-300" />
<Popover className="flex h-full items-center justify-center rounded-lg">
{({}) => (
<>
<Popover.Button
disabled={isCompleted ?? false}
className="text-sm text-custom-text-300 font-medium cursor-default"
>
{areYearsEqual ? renderShortDate(endDate, "_ _") : renderShortMonthDate(endDate, "_ _")}
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute top-10 -right-5 z-20 transform overflow-hidden">
<CustomRangeDatePicker
value={watch("end_date") ? watch("end_date") : cycleDetails?.end_date}
onChange={(val) => {
if (val) {
handleEndDateChange(val);
}
}}
startDate={watch("start_date") ? `${watch("start_date")}` : null}
endDate={watch("end_date") ? `${watch("end_date")}` : null}
minDate={new Date(`${watch("start_date")}`)}
selectsEnd
/>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
</div>
</div>
<div className="flex w-full flex-col gap-6 px-6 py-6">
<div className="flex w-full flex-col items-start justify-start gap-2">
<div className="flex w-full items-start justify-between gap-2">
<div className="max-w-[300px]">
<h4 className="text-xl font-semibold text-custom-text-100 break-words w-full">
{cycleDetails.name}
</h4>
</div>
<CustomMenu width="lg" ellipsis>
{!isCompleted && (
<CustomMenu.MenuItem onClick={() => setCycleDeleteModal(true)}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy link</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
{cycleDetails.description && (
<span className="whitespace-normal text-sm leading-5 py-2.5 text-custom-text-200 break-words w-full">
{cycleDetails.description}
</span>
)}
<span className="whitespace-normal text-sm leading-5 text-custom-text-200 break-words w-full">
{cycleDetails.description}
</span>
</div>
<div className="flex flex-col gap-4 text-sm">
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2 text-custom-text-200">
<UserCircle2 className="h-5 w-5" />
<span>Lead</span>
</div>
<div className="flex items-center gap-2.5">
{cycleDetails.owned_by.avatar && cycleDetails.owned_by.avatar !== "" ? (
<img
src={cycleDetails.owned_by.avatar}
height={12}
width={12}
className="rounded-full"
alt={cycleDetails.owned_by.display_name}
/>
) : (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-800 capitalize text-white">
{cycleDetails.owned_by.display_name.charAt(0)}
</span>
)}
<span className="text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
</div>
</div>
<div className="flex items-center justify-start gap-1">
<div className="flex w-40 items-center justify-start gap-2 text-custom-text-200">
<PieChart className="h-5 w-5" />
<span>Progress</span>
</div>
<div className="flex items-center gap-2.5 text-custom-text-200">
<span className="h-4 w-4">
<ProgressBar value={cycleDetails.completed_issues} maxValue={cycleDetails.total_issues} />
</span>
{cycleDetails.completed_issues}/{cycleDetails.total_issues}
</div>
</div>
<div className="flex flex-col gap-5 pt-2.5 pb-6">
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<UserCircle2 className="h-4 w-4" />
<span className="text-base">Lead</span>
</div>
<div className="flex items-center w-1/2 rounded-sm">
<div className="flex items-center gap-2.5">
<Avatar user={cycleDetails.owned_by} />
<span className="text-sm text-custom-text-200">{cycleDetails.owned_by.display_name}</span>
</div>
</div>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
<Disclosure defaultOpen>
<div className="flex items-center justify-start gap-1">
<div className="flex w-1/2 items-center justify-start gap-2 text-custom-text-300">
<LayersIcon className="h-4 w-4" />
<span className="text-base">Issues</span>
</div>
<div className="flex items-center w-1/2">
<span className="text-sm text-custom-text-300 px-1.5">{issueCount}</span>
</div>
</div>
</div>
<div className="flex flex-col">
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 py-5 px-1.5">
<Disclosure>
{({ open }) => (
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
<div className="flex w-full items-center justify-between gap-2 ">
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-custom-text-200">Progress</span>
{!open && progressPercentage ? (
<span className="rounded bg-[#09A953]/10 px-1.5 py-0.5 text-xs text-[#09A953]">
</div>
<div className="flex items-center gap-2.5">
{progressPercentage ? (
<span className="flex items-center justify-center h-5 w-9 rounded text-xs font-medium text-amber-500 bg-amber-50">
{progressPercentage ? `${progressPercentage}%` : ""}
</span>
) : (
""
)}
{isStartValid && isEndValid ? (
<Disclosure.Button className="p-1.5">
<ChevronDown
className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`}
aria-hidden="true"
/>
</Disclosure.Button>
) : (
<div className="flex items-center gap-1">
<AlertCircle height={14} width={14} className="text-custom-text-200" />
<span className="text-xs italic text-custom-text-200">
Invalid date. Please enter valid date.
</span>
</div>
)}
</div>
{isStartValid && isEndValid ? (
<Disclosure.Button>
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button>
) : (
<div className="flex items-center gap-1">
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs italic text-custom-text-200">
{cycleStatus === "upcoming"
? "Cycle is yet to start."
: "Invalid date. Please enter valid date."}
</span>
</div>
)}
</div>
<Transition show={open}>
<Disclosure.Panel>
{isStartValid && isEndValid ? (
<div className=" h-full w-full py-4">
<div className="flex items-start justify-between gap-4 py-2 text-xs">
<div className="flex items-center gap-1">
<span>
<File className="h-3 w-3 text-custom-text-200" />
</span>
<span>
Pending Issues -{" "}
{cycleDetails.total_issues -
(cycleDetails.completed_issues + cycleDetails.cancelled_issues)}
</span>
<div className="flex flex-col gap-3">
{isStartValid && isEndValid ? (
<div className=" h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-custom-text-100">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="flex items-center gap-3 text-custom-text-100">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution.completion_chart}
startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""}
totalIssues={cycleDetails.total_issues}
/>
</div>
</div>
<div className="relative">
<ProgressChart
distribution={cycleDetails.distribution.completion_chart}
startDate={cycleDetails.start_date ?? ""}
endDate={cycleDetails.end_date ?? ""}
) : (
""
)}
{cycleDetails.total_issues > 0 && (
<div className="h-full w-full pt-5 border-t border-custom-border-200">
<SidebarProgressStats
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
/>
</div>
</div>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-custom-border-200 p-6">
<Disclosure defaultOpen>
{({ open }) => (
<div className={`relative flex h-full w-full flex-col ${open ? "" : "flex-row"}`}>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center justify-start gap-2 text-sm">
<span className="font-medium text-custom-text-200">Other Information</span>
</div>
{cycleDetails.total_issues > 0 ? (
<Disclosure.Button>
<ChevronDown className={`h-3 w-3 ${open ? "rotate-180 transform" : ""}`} aria-hidden="true" />
</Disclosure.Button>
) : (
<div className="flex items-center gap-1">
<AlertCircle className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs italic text-custom-text-200">
No issues found. Please add issue.
</span>
)}
</div>
)}
</div>
<Transition show={open}>
<Disclosure.Panel>
{cycleDetails.total_issues > 0 ? (
<div className="h-full w-full py-4">
<SidebarProgressStats
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycleDetails.total_issues}
/>
</div>
) : (
""
)}
</Disclosure.Panel>
</Transition>
</div>
)}
</Disclosure>
</div>
</>
) : (
<Loader className="px-5">
<div className="space-y-2">
<Loader.Item height="15px" width="50%" />
<Loader.Item height="15px" width="30%" />
</div>
<div className="mt-8 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
)}
</div>
</div>
</>
) : (
<Loader className="px-5">
<div className="space-y-2">
<Loader.Item height="15px" width="50%" />
<Loader.Item height="15px" width="30%" />
</div>
<div className="mt-8 space-y-3">
<Loader.Item height="30px" />
<Loader.Item height="30px" />
<Loader.Item height="30px" />
</div>
</Loader>
)}
</>
);
});
+1 -1
View File
@@ -334,7 +334,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
position="bottom"
>
<div className="flex w-full items-center">
<LinearProgressIndicator data={progressIndicatorData} noTooltip={true} />
<LinearProgressIndicator data={progressIndicatorData} noTooltip />
</div>
</Tooltip>
<Disclosure.Button>
+1 -1
View File
@@ -128,7 +128,7 @@ export const Exporter: React.FC<Props> = observer((props) => {
value={value ?? []}
onChange={(val: string[]) => onChange(val)}
options={options}
input={true}
input
label={
value && value.length > 0
? projects &&
+1 -1
View File
@@ -150,7 +150,7 @@ const IntegrationGuide = () => {
</>
{provider && (
<Exporter
isOpen={true}
isOpen
handleClose={() => handleCsvClose()}
data={null}
user={user}
+2 -2
View File
@@ -147,7 +147,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={cycleIssueFilterStore.cycleFilters}
handleFiltersUpdate={handleFiltersUpdate}
@@ -159,7 +159,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
+3 -2
View File
@@ -128,18 +128,19 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
{activeLayout === "spreadsheet" && (
<>
{!STATIC_VIEW_TYPES.some((word) => router.pathname.includes(word)) && (
<FiltersDropdown title="Filters">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={storedFilters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
labels={workspaceStore.workspaceLabels ?? undefined}
members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined}
projects={workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined}
/>
</FiltersDropdown>
)}
<FiltersDropdown title="Display">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
displayProperties={workspaceFilterStore.workspaceDisplayProperties}
+2 -2
View File
@@ -150,7 +150,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={moduleFilterStore.moduleFilters}
handleFiltersUpdate={handleFiltersUpdate}
@@ -162,7 +162,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
+2 -2
View File
@@ -146,7 +146,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={issueFilterStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
@@ -158,7 +158,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
@@ -135,7 +135,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={storedFilters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
@@ -147,7 +147,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display">
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
+4 -4
View File
@@ -22,10 +22,10 @@ import { IInboxIssue, IIssue } from "types";
const defaultValues: Partial<IInboxIssue> = {
name: "",
description_html: "",
assignees_list: [],
assignees: [],
priority: "low",
target_date: new Date().toString(),
labels_list: [],
labels: [],
};
export const InboxMainContent: React.FC = observer(() => {
@@ -122,8 +122,8 @@ export const InboxMainContent: React.FC = observer(() => {
reset({
...issueDetails,
assignees_list: issueDetails.assignees_list ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels,
assignees: issueDetails.assignees ?? (issueDetails.assignee_details ?? []).map((user) => user.id),
labels: issueDetails.labels ?? issueDetails.labels,
});
}, [issueDetails, reset, inboxIssueId]);
@@ -44,7 +44,7 @@ export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users,
const { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug.toString()) : null
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null
);
const options = members?.map((member) => ({
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import useSWR from "swr";
import { useFormContext, useFieldArray, Controller } from "react-hook-form";
// fetch keys
import { WORKSPACE_MEMBERS_WITH_EMAIL } from "constants/fetch-keys";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
// services
import { WorkspaceService } from "services/workspace.service";
// components
@@ -30,8 +30,8 @@ export const JiraImportUsers: FC = () => {
});
const { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_WITH_EMAIL(workspaceSlug?.toString() ?? "") : null,
workspaceSlug ? () => workspaceService.workspaceMembersWithEmail(workspaceSlug?.toString() ?? "") : null
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug?.toString() ?? "") : null,
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug?.toString() ?? "") : null
);
const options = members?.map((member) => ({
@@ -44,7 +44,7 @@ export const IssueAttachments = () => {
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
? () => projectService.fetchProjectMembers(workspaceSlug as string, projectId as string)
: null
);
+1 -30
View File
@@ -7,7 +7,7 @@ import { FileService } from "services/file.service";
// components
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
// ui
import { Button, Tooltip } from "@plane/ui";
import { Button } from "@plane/ui";
import { Globe2, Lock } from "lucide-react";
// types
@@ -72,35 +72,6 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
<form onSubmit={handleSubmit(handleAddComment)}>
<div>
<div className="relative">
{showAccessSpecifier && (
<div className="absolute bottom-2 left-3 z-[1]">
<Controller
control={control}
name="access"
render={({ field: { onChange, value } }) => (
<div className="flex border border-custom-border-300 divide-x divide-custom-border-300 rounded overflow-hidden">
{commentAccess.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}>
<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" : ""
}`}
>
<access.icon
className={`w-4 h-4 -mt-1 ${
value === access.key ? "!text-custom-text-100" : "!text-custom-text-400"
}`}
/>
</button>
</Tooltip>
))}
</div>
)}
/>
</div>
)}
<Controller
name="access"
control={control}
@@ -116,7 +116,8 @@ export const CommentCard: React.FC<Props> = ({
</div>
<div className="flex gap-1 self-end">
<button
type="submit"
type="button"
onClick={handleSubmit(onEnter)}
disabled={isSubmitting}
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
>
+15 -7
View File
@@ -49,6 +49,14 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
},
});
const [localTitleValue, setLocalTitleValue] = useState("");
const issueTitleCurrentValue = watch("name");
useEffect(() => {
if (localTitleValue === "" && issueTitleCurrentValue !== "") {
setLocalTitleValue(issueTitleCurrentValue);
}
}, [issueTitleCurrentValue, localTitleValue]);
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
@@ -81,7 +89,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
});
}, [issue, reset]);
const debouncedTitleSave = useDebouncedCallback(async () => {
const debouncedFormSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500);
@@ -92,20 +100,21 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
<Controller
name="name"
control={control}
render={({ field: { value, onChange } }) => (
render={({ field: { onChange } }) => (
<TextArea
value={localTitleValue}
id="name"
name="name"
value={value}
placeholder="Enter issue name"
onFocus={() => setCharacterLimit(true)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setCharacterLimit(false);
setIsSubmitting("submitting");
debouncedTitleSave();
setLocalTitleValue(e.target.value);
onChange(e.target.value);
debouncedFormSave();
}}
required={true}
required
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
hasError={Boolean(errors?.description)}
role="textbox"
@@ -135,7 +144,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
value={value}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
@@ -144,7 +152,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
debouncedFormSave();
}}
/>
)}
+1 -6
View File
@@ -51,9 +51,7 @@ const defaultValues: Partial<IIssue> = {
parent: null,
priority: "none",
assignees: [],
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
@@ -310,10 +308,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])}
/>
</>
)}
+2 -6
View File
@@ -208,8 +208,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
message: "Issue created successfully.",
});
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
})
.catch(() => {
setToastAlert({
@@ -325,8 +324,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
if (!createMore) onClose();
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
@@ -347,8 +345,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const payload: Partial<IIssue> = {
...formData,
assignees_list: formData.assignees ?? [],
labels_list: formData.labels ?? [],
description: formData.description ?? "",
description_html: formData.description_html ?? "<p></p>",
};
+1 -6
View File
@@ -41,9 +41,7 @@ const defaultValues: Partial<IIssue> = {
parent: null,
priority: "none",
assignees: [],
assignees_list: [],
labels: [],
labels_list: [],
start_date: null,
target_date: null,
};
@@ -262,10 +260,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
handleClose={() => setLabelModal(false)}
projectId={projectId}
user={user ?? undefined}
onSuccess={(response) => {
setValue("labels", [...watch("labels"), response.id]);
setValue("labels_list", [...watch("labels_list"), response.id]);
}}
onSuccess={(response) => setValue("labels", [...watch("labels"), response.id])}
/>
</>
)}
-1
View File
@@ -1,6 +1,5 @@
export * from "./attachment";
export * from "./comment";
export * from "./my-issues";
export * from "./sidebar-select";
export * from "./view-select";
export * from "./activity";
@@ -147,18 +147,6 @@ export const CalendarInlineCreateIssueForm: React.FC<Props> = observer((props) =
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
...(prePopulatedData ?? {}),
...formData,
labels_list:
formData.labels_list?.length !== 0
? formData.labels_list
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
? [prePopulatedData.labels as any]
: [],
assignees_list:
formData.assignees_list?.length !== 0
? formData.assignees_list
: prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none"
? [prePopulatedData.assignees as any]
: [],
});
try {
@@ -0,0 +1,25 @@
import { PlusIcon } from "lucide-react";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
export const CycleEmptyState: React.FC = () => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
/>
</div>
);
@@ -0,0 +1,25 @@
import { PlusIcon } from "lucide-react";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
export const GlobalViewEmptyState: React.FC = () => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="View issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
/>
</div>
);
@@ -0,0 +1,5 @@
export * from "./cycle";
export * from "./global-view";
export * from "./module";
export * from "./project-view";
export * from "./project";
@@ -0,0 +1,25 @@
import { PlusIcon } from "lucide-react";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
export const ModuleEmptyState: React.FC = () => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
/>
</div>
);
@@ -0,0 +1,25 @@
import { PlusIcon } from "lucide-react";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
export const ProjectViewEmptyState: React.FC = () => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="View issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
/>
</div>
);
@@ -0,0 +1,25 @@
import { PlusIcon } from "lucide-react";
// components
import { EmptyState } from "components/common";
// assets
import emptyIssue from "public/empty-state/issue.svg";
export const ProjectEmptyState: React.FC = () => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Project issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
/>
</div>
);
@@ -1,7 +1,7 @@
import React, { Fragment, useState } from "react";
import { usePopper } from "react-popper";
import { Popover, Transition } from "@headlessui/react";
import { Placement } from "@popperjs/core";
// ui
import { Button } from "@plane/ui";
// icons
@@ -10,16 +10,17 @@ import { ChevronUp } from "lucide-react";
type Props = {
children: React.ReactNode;
title?: string;
placement?: Placement;
};
export const FiltersDropdown: React.FC<Props> = (props) => {
const { children, title = "Dropdown" } = props;
const { children, title = "Dropdown", placement } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "auto",
placement: placement ?? "auto",
});
return (
@@ -113,12 +113,6 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
...(prePopulatedData ?? {}),
...formData,
labels_list:
formData.labels_list?.length !== 0
? formData.labels_list
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
? [prePopulatedData.labels as any]
: [],
start_date: renderDateFormat(new Date()),
target_date: renderDateFormat(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)),
});
@@ -1,5 +1,6 @@
// filters
export * from "./filters";
export * from "./empty-states";
export * from "./quick-action-dropdowns";
// layouts
@@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd";
// components
import { KanBanProperties } from "./properties";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
interface IssueBlockProps {
sub_group_id: string;
@@ -17,7 +17,7 @@ interface IssueBlockProps {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: any;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
@@ -65,9 +65,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
)}
</div>
<div
className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 hover:cursor-grab ${
snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`
}`}
className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>
{displayProperties && displayProperties?.key && (
<div className="text-xs line-clamp-1 text-custom-text-300">
@@ -81,7 +81,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
columnId={columnId}
issue={issue}
handleIssues={updateIssue}
display_properties={displayProperties}
displayProperties={displayProperties}
states={states}
labels={labels}
members={members}
@@ -1,6 +1,6 @@
// components
import { KanbanIssueBlock } from "components/issues";
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
interface IssueBlocksListProps {
sub_group_id: string;
@@ -14,7 +14,7 @@ interface IssueBlocksListProps {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
@@ -29,7 +29,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
isDragDisabled,
handleIssues,
quickActions,
display_properties,
displayProperties,
states,
labels,
members,
@@ -47,7 +47,7 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
issue={issue}
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={display_properties}
displayProperties={displayProperties}
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
@@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
@@ -26,7 +26,7 @@ export interface IGroupByKanBan {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
enableQuickIssueCreate?: boolean;
@@ -48,7 +48,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
isDragDisabled,
handleIssues,
quickActions,
display_properties,
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
@@ -104,7 +104,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
isDragDisabled={isDragDisabled}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
states={states}
labels={labels}
members={members}
@@ -151,7 +151,7 @@ export interface IKanBan {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
states: IState[] | null;
@@ -172,7 +172,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
sub_group_id = "null",
handleIssues,
quickActions,
display_properties,
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
@@ -200,7 +200,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -223,7 +223,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -246,7 +246,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -269,7 +269,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -292,7 +292,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -315,7 +315,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
isDragDisabled={!issueKanBanViewStore?.canUserDragDrop}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
@@ -117,18 +117,6 @@ export const BoardInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
...(prePopulatedData ?? {}),
...formData,
labels_list:
formData.labels_list && formData.labels_list.length !== 0
? formData.labels_list
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
? [prePopulatedData.labels as any]
: [],
assignees_list:
formData.assignees_list && formData.assignees_list.length !== 0
? formData.assignees_list
: prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none"
? [prePopulatedData.assignees as any]
: [],
});
try {
@@ -10,14 +10,22 @@ import { IssuePropertyAssignee } from "../properties/assignee";
import { IssuePropertyEstimates } from "../properties/estimates";
import { IssuePropertyDate } from "../properties/date";
import { Tooltip } from "@plane/ui";
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types";
import {
IEstimatePoint,
IIssue,
IIssueDisplayProperties,
IIssueLabels,
IState,
IUserLite,
TIssuePriorities,
} from "types";
export interface IKanBanProperties {
sub_group_id: string;
columnId: string;
issue: IIssue;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
display_properties: any;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
@@ -30,7 +38,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
columnId: group_id,
issue,
handleIssues,
display_properties,
displayProperties,
states,
labels,
members,
@@ -57,7 +65,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, labels_list: ids }
{ ...issue, labels: ids }
);
};
@@ -65,7 +73,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id,
{ ...issue, assignees_list: ids }
{ ...issue, assignees: ids }
);
};
@@ -97,42 +105,42 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
<div className="flex items-center gap-2 flex-wrap whitespace-nowrap">
{/* basic properties */}
{/* state */}
{display_properties && display_properties?.state && (
{displayProperties && displayProperties?.state && (
<IssuePropertyState
value={issue?.state_detail || null}
onChange={handleState}
states={states}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
{/* priority */}
{display_properties && display_properties?.priority && (
{displayProperties && displayProperties?.priority && (
<IssuePropertyPriority
value={issue?.priority || null}
onChange={handlePriority}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
{/* label */}
{display_properties && display_properties?.labels && (
{displayProperties && displayProperties?.labels && (
<IssuePropertyLabels
value={issue?.labels || null}
onChange={handleLabel}
labels={labels}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
{/* assignee */}
{display_properties && display_properties?.assignee && (
{displayProperties && displayProperties?.assignee && (
<IssuePropertyAssignee
value={issue?.assignees || null}
hideDropdownArrow={true}
hideDropdownArrow
onChange={handleAssignee}
members={members}
disabled={false}
@@ -140,7 +148,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)}
{/* start date */}
{display_properties && display_properties?.start_date && (
{displayProperties && displayProperties?.start_date && (
<IssuePropertyDate
value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)}
@@ -150,7 +158,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)}
{/* target/due date */}
{display_properties && display_properties?.due_date && (
{displayProperties && displayProperties?.due_date && (
<IssuePropertyDate
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)}
@@ -160,19 +168,19 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)}
{/* estimates */}
{display_properties && display_properties?.estimate && (
{displayProperties && displayProperties?.estimate && (
<IssuePropertyEstimates
value={issue?.estimate_point || null}
onChange={handleEstimate}
estimatePoints={estimates}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
{/* extra render properties */}
{/* sub-issues */}
{display_properties && display_properties?.sub_issue_count && (
{displayProperties && displayProperties?.sub_issue_count && (
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@@ -182,7 +190,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)}
{/* attachments */}
{display_properties && display_properties?.attachment_count && (
{displayProperties && displayProperties?.attachment_count && (
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@@ -192,7 +200,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
)}
{/* link */}
{display_properties && display_properties?.link && (
{displayProperties && displayProperties?.link && (
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@@ -35,7 +35,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
? "swimlanes"
@@ -113,7 +113,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -138,7 +138,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -35,7 +35,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
? "swimlanes"
@@ -113,7 +113,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -138,7 +138,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -34,7 +34,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
const display_properties = profileIssueFiltersStore?.userDisplayProperties || null;
const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null;
const currentKanBanView: "swimlanes" | "default" = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by
? "swimlanes"
@@ -96,7 +96,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -120,7 +120,7 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -35,7 +35,7 @@ export const KanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
? "swimlanes"
@@ -103,7 +103,7 @@ export const KanBanLayout: React.FC = observer(() => {
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -128,7 +128,7 @@ export const KanBanLayout: React.FC = observer(() => {
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -7,7 +7,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
@@ -73,7 +73,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
states: IState[] | null;
@@ -93,7 +93,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
listKey,
handleIssues,
quickActions,
display_properties,
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
@@ -143,7 +143,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
sub_group_id={getValueFromObject(_list, listKey) as string}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -174,7 +174,7 @@ export interface IKanBanSwimLanes {
action: "update" | "delete"
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
states: IState[] | null;
@@ -193,7 +193,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
group_by,
handleIssues,
quickActions,
display_properties,
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
@@ -322,7 +322,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -344,7 +344,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -366,7 +366,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -388,7 +388,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -410,7 +410,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`member.id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -432,7 +432,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
listKey={`member.id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
@@ -27,7 +27,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
return (
<>
<div className="text-sm p-3 relative shadow-custom-shadow-2xs bg-custom-background-100 flex items-center gap-3 border-b border-custom-border-200 hover:bg-custom-background-80">
<div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3">
{display_properties && display_properties?.key && (
<div className="flex-shrink-0 text-xs text-custom-text-300">
{issue?.project_detail?.identifier}-{issue.sequence_id}
@@ -21,9 +21,8 @@ export const IssueBlocksList: FC<Props> = (props) => {
props;
return (
<>
{issues &&
issues?.length > 0 &&
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
{issues && issues.length > 0 ? (
issues.map((issue) => (
<IssueBlock
key={issue.id}
@@ -37,7 +36,10 @@ export const IssueBlocksList: FC<Props> = (props) => {
members={members}
estimates={estimates}
/>
))}
</>
))
) : (
<div className="bg-custom-background-100 text-custom-text-400 text-sm p-3">No issues</div>
)}
</div>
);
};
@@ -53,7 +53,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
list.length > 0 &&
list.map((_list: any) => (
<div key={getValueFromObject(_list, listKey) as string} className={`flex-shrink-0 flex flex-col`}>
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2] px-3 border-b border-custom-border-100">
<div className="flex-shrink-0 w-full py-1 sticky top-0 z-[2] px-3">
<ListGroupByHeaderRoot
column_id={getValueFromObject(_list, listKey) as string}
column_value={_list}
@@ -63,21 +63,19 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
}
/>
</div>
<div className={`w-full h-full relative transition-all`}>
{issues && (
<IssueBlocksList
columnId={getValueFromObject(_list, listKey) as string}
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
)}
</div>
{issues && (
<IssueBlocksList
columnId={getValueFromObject(_list, listKey) as string}
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
)}
{enableQuickIssueCreate && (
<ListInlineCreateIssueForm
groupId={getValueFromObject(_list, listKey) as string}
@@ -138,7 +136,7 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
is_list={true}
is_list
states={states}
labels={labels}
members={members}
@@ -1,4 +1,5 @@
export * from "./roots";
export * from "./block";
export * from "./roots";
export * from "./blocks-list";
export * from "./inline-create-issue-form";
@@ -116,18 +116,6 @@ export const ListInlineCreateIssueForm: React.FC<Props> = observer((props) => {
const payload = createIssuePayload(workspaceDetail!, projectDetails!, {
...(prePopulatedData ?? {}),
...formData,
labels_list:
formData.labels_list?.length !== 0
? formData.labels_list
: prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none"
? [prePopulatedData.labels as any]
: [],
assignees_list:
formData.assignees_list?.length !== 0
? formData.assignees_list
: prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none"
? [prePopulatedData.assignees as any]
: [],
});
try {
@@ -161,7 +149,7 @@ export const ListInlineCreateIssueForm: React.FC<Props> = observer((props) => {
};
return (
<div>
<div className="bg-custom-background-100">
<Transition
show={isOpen}
enter="transition ease-in-out duration-200 transform"
@@ -189,10 +177,10 @@ export const ListInlineCreateIssueForm: React.FC<Props> = observer((props) => {
{!isOpen && (
<button
type="button"
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
className="flex items-center gap-x-[6px] text-custom-primary-100 px-3 py-1 rounded-md"
onClick={() => setIsOpen(true)}
>
<PlusIcon className="h-4 w-4" />
<PlusIcon className="h-3 w-3" />
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
</button>
)}
@@ -36,11 +36,11 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
};
const handleLabel = (ids: string[]) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels_list: ids });
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids });
};
const handleAssignee = (ids: string[]) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees_list: ids });
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids });
};
const handleStartDate = (date: string) => {
@@ -62,7 +62,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
{display_properties && display_properties?.state && states && (
<IssuePropertyState
value={issue?.state_detail || null}
hideDropdownArrow={true}
hideDropdownArrow
onChange={handleState}
disabled={false}
states={states}
@@ -75,7 +75,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
value={issue?.priority || null}
onChange={handlePriority}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
@@ -86,7 +86,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
onChange={handleLabel}
labels={labels}
disabled={false}
hideDropdownArrow={true}
hideDropdownArrow
/>
)}
@@ -94,7 +94,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
{display_properties && display_properties?.assignee && members && (
<IssuePropertyAssignee
value={issue?.assignees || null}
hideDropdownArrow={true}
hideDropdownArrow
onChange={handleAssignee}
disabled={false}
members={members}
@@ -126,7 +126,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
<IssuePropertyEstimates
value={issue?.estimate_point || null}
estimatePoints={estimates}
hideDropdownArrow={true}
hideDropdownArrow
onChange={handleEstimate}
disabled={false}
/>
@@ -68,7 +68,7 @@ export const ModuleListLayout: React.FC = observer(() => {
: null;
return (
<div className={`relative w-full h-full bg-custom-background-90`}>
<div className="relative w-full h-full bg-custom-background-90">
<List
issues={issues}
group_by={group_by}
@@ -1,175 +0,0 @@
import React from "react";
// headless ui
import { Combobox } from "@headlessui/react";
// lucide icons
import { ChevronDown, Search, X, Check } from "lucide-react";
// hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
interface IFiltersOption {
id: string;
title: string;
}
export interface IIssuePropertyState {
options: IFiltersOption[];
value?: any;
onChange?: (id: any, data: IFiltersOption) => void;
disabled?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
dropdownArrow?: boolean;
children?: any;
}
export const IssuePropertyState: React.FC<IIssuePropertyState> = ({
options,
value,
onChange,
disabled,
className,
buttonClassName,
optionsClassName,
dropdownArrow = true,
children,
}) => {
const dropdownBtn = React.useRef<any>(null);
const dropdownOptions = React.useRef<any>(null);
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [search, setSearch] = React.useState<string>("");
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
const selectedOption: IFiltersOption | null | undefined =
(value && options.find((person: IFiltersOption) => person.id === value)) || null;
const filteredOptions: IFiltersOption[] =
search === ""
? options && options.length > 0
? options
: []
: options && options.length > 0
? options.filter((person: IFiltersOption) =>
person.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, ""))
)
: [];
return (
<Combobox
as="div"
className={`${className}`}
value={selectedOption && selectedOption.id}
onChange={(data: string) => {
if (onChange && selectedOption) onChange(data, selectedOption);
}}
disabled={disabled}
>
{({ open }: { open: boolean }) => {
if (open) {
if (!isOpen) setIsOpen(true);
} else if (isOpen) setIsOpen(false);
return (
<>
<Combobox.Button
ref={dropdownBtn}
type="button"
className={`flex items-center justify-between gap-1 w-full px-1 py-1 rounded-sm shadow-sm border border-custom-border-300 duration-300 outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{children ? (
children
) : (
<div className="text-xs">{(selectedOption && selectedOption?.title) || "Select option"}</div>
)}
{dropdownArrow && !disabled && (
<div className="flex-shrink-0 w-[14px] h-[14px] flex justify-center items-center">
<ChevronDown width={14} strokeWidth={2} />
</div>
)}
</Combobox.Button>
{options && options.length > 0 ? (
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
<Combobox.Options
ref={dropdownOptions}
className={`absolute z-10 border border-custom-border-300 p-2 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none min-w-48 max-w-60 whitespace-nowrap mt-1 space-y-1 ${optionsClassName}`}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-1">
<div className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm">
<Search width={12} strokeWidth={2} />
</div>
<div>
<Combobox.Input
className="w-full bg-transparent p-1 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
{search && search.length > 0 && (
<div
onClick={() => setSearch("")}
className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80"
>
<X width={12} strokeWidth={2} />
</div>
)}
</div>
<div className={`space-y-0.5 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.id}
value={option.id}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<div className="flex justify-between items-center gap-1 w-full px-1">
<div className="line-clamp-1">{option.title}</div>
{selected && (
<div className="flex-shrink-0 w-[13px] h-[13px] flex justify-center items-center">
<Check width={13} strokeWidth={2} />
</div>
)}
</div>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</Combobox.Options>
</div>
) : (
<p className="text-center text-custom-text-200">No options available.</p>
)}
</>
);
}}
</Combobox>
);
};
@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite";
// components
import { LabelSelect } from "components/project";
import { LabelSelect } from "components/labels";
// types
import { IIssueLabels } from "types";
@@ -62,7 +62,7 @@ export const CycleIssueQuickActions: React.FC<Props> = (props) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu placement="bottom-start" ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
@@ -62,7 +62,7 @@ export const ModuleIssueQuickActions: React.FC<Props> = (props) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu placement="bottom-start" ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
@@ -61,7 +61,7 @@ export const ProjectIssueQuickActions: React.FC<Props> = (props) => {
if (issueToEdit) handleUpdate({ ...issueToEdit, ...data });
}}
/>
<CustomMenu ellipsis>
<CustomMenu placement="bottom-start" ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
@@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import {
CycleAppliedFiltersRoot,
CycleCalendarLayout,
CycleEmptyState,
CycleGanttLayout,
CycleKanBanLayout,
CycleListLayout,
@@ -50,25 +51,31 @@ export const CycleLayoutRoot: React.FC = observer(() => {
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft";
const issueCount = cycleIssueStore.getIssuesCount;
return (
<>
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
<div className="relative w-full h-full flex flex-col overflow-hidden">
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot />
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<CycleEmptyState />
) : (
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
)}
</div>
</>
);
@@ -5,7 +5,7 @@ import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues";
import { GlobalViewEmptyState, GlobalViewsAppliedFiltersRoot, SpreadsheetView } from "components/issues";
// types
import { IIssue, IIssueDisplayFilterOptions, TStaticViewTypes } from "types";
@@ -25,6 +25,7 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
globalViewFilters: globalViewFiltersStore,
workspaceFilter: workspaceFilterStore,
workspace: workspaceStore,
issueDetail: issueDetailStore,
} = useMobxStore();
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
@@ -62,14 +63,17 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
const handleUpdateIssue = useCallback(
(issue: IIssue, data: Partial<IIssue>) => {
if (!workspaceSlug) return;
if (!workspaceSlug || !globalViewId) return;
console.log("issue", issue);
console.log("data", data);
const payload = {
...issue,
...data,
};
// TODO: add update issue logic here
globalViewIssuesStore.updateIssueStructure(globalViewId.toString(), payload);
issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, data);
},
[workspaceSlug]
[globalViewId, globalViewIssuesStore, workspaceSlug, issueDetailStore]
);
const issues = type
@@ -81,19 +85,23 @@ export const GlobalViewLayoutRoot: React.FC<Props> = observer((props) => {
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
<GlobalViewsAppliedFiltersRoot />
<div className="h-full w-full overflow-auto">
<SpreadsheetView
displayProperties={workspaceFilterStore.workspaceDisplayProperties}
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues}
members={workspaceStore.workspaceMembers ? workspaceStore.workspaceMembers.map((m) => m.member) : undefined}
labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined}
handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue}
disableUserActions={false}
/>
</div>
{issues?.length === 0 ? (
<GlobalViewEmptyState />
) : (
<div className="h-full w-full overflow-auto">
<SpreadsheetView
displayProperties={workspaceFilterStore.workspaceDisplayProperties}
displayFilters={workspaceFilterStore.workspaceDisplayFilters}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues}
members={workspaceStore.workspaceMembers ? workspaceStore.workspaceMembers.map((m) => m.member) : undefined}
labels={workspaceStore.workspaceLabels ? workspaceStore.workspaceLabels : undefined}
handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue}
disableUserActions={false}
/>
</div>
)}
</div>
);
});
@@ -9,6 +9,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import {
ModuleAppliedFiltersRoot,
ModuleCalendarLayout,
ModuleEmptyState,
ModuleGanttLayout,
ModuleKanBanLayout,
ModuleListLayout,
@@ -46,22 +47,28 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const issueCount = moduleIssueStore.getIssuesCount;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ModuleEmptyState />
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
)}
</div>
);
});
@@ -12,6 +12,7 @@ import {
KanBanLayout,
ProjectAppliedFiltersRoot,
ProjectSpreadsheetLayout,
ProjectEmptyState,
} from "components/issues";
export const ProjectLayoutRoot: React.FC = observer(() => {
@@ -30,22 +31,30 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const issueCount = issueStore.getIssuesCount;
console.log("issueCount", issueCount);
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ProjectEmptyState />
) : (
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<ListLayout />
) : activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<CalendarLayout />
) : activeLayout === "gantt_chart" ? (
<GanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectSpreadsheetLayout />
) : null}
</div>
)}
</div>
);
});
@@ -11,6 +11,7 @@ import {
ModuleListLayout,
ProjectViewAppliedFiltersRoot,
ProjectViewCalendarLayout,
ProjectViewEmptyState,
ProjectViewGanttLayout,
ProjectViewSpreadsheetLayout,
} from "components/issues";
@@ -48,22 +49,28 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const issueCount = projectViewIssuesStore.getIssuesCount;
return (
<div className="relative h-full w-full flex flex-col overflow-hidden">
<ProjectViewAppliedFiltersRoot />
<div className="h-full w-full overflow-y-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout />
) : null}
</div>
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ProjectViewEmptyState />
) : (
<div className="h-full w-full overflow-y-auto">
{activeLayout === "list" ? (
<ModuleListLayout />
) : activeLayout === "kanban" ? (
<ModuleKanBanLayout />
) : activeLayout === "calendar" ? (
<ProjectViewCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ProjectViewGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ProjectViewSpreadsheetLayout />
) : null}
</div>
)}
</div>
);
});
@@ -21,12 +21,12 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<>
<div className="flex items-center h-full px-4">
<MembersSelect
value={issue.assignees}
onChange={(data) => onChange({ assignees_list: data })}
onChange={(data) => onChange({ assignees: data })}
members={members ?? []}
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
buttonClassName="!p-0 !rounded-none !border-0"
hideDropdownArrow
disabled={disabled}
multiple
@@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
disabled={disabled}
/>
))}
</>
</div>
);
};
@@ -19,7 +19,7 @@ export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({ issue, expandedIss
return (
<>
{renderLongDetailDateFormat(issue.created_at)}
<div className="text-xs">{renderLongDetailDateFormat(issue.created_at)}</div>
{isExpanded &&
!isLoading &&
@@ -1,5 +1,5 @@
// components
import { ViewEstimateSelect } from "components/issues";
import { EstimateSelect } from "components/estimates";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@@ -21,7 +21,15 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
return (
<>
<ViewEstimateSelect issue={issue} onChange={(data) => onChange({ estimate_point: data })} disabled={disabled} />
<EstimateSelect
value={issue.estimate_point}
onChange={(data) => onChange({ estimate_point: data })}
className="h-full"
buttonClassName="!border-0 !h-full !w-full !rounded-none px-4"
estimatePoints={undefined}
disabled={disabled}
hideDropdownArrow
/>
{isExpanded &&
!isLoading &&
@@ -5,13 +5,12 @@ import { MoreHorizontal, Pencil, Trash2, ChevronRight, Link } from "lucide-react
// hooks
import useToast from "hooks/use-toast";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IIssue, IIssueDisplayProperties } from "types";
type Props = {
issue: IIssue;
projectId: string;
expanded: boolean;
handleToggleExpand: (issueId: string) => void;
properties: IIssueDisplayProperties;
@@ -23,7 +22,6 @@ type Props = {
export const IssueColumn: React.FC<Props> = ({
issue,
projectId,
expanded,
handleToggleExpand,
properties,
@@ -50,8 +48,7 @@ export const IssueColumn: React.FC<Props> = ({
};
const handleCopyText = () => {
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
@@ -9,7 +9,6 @@ import { IIssue, IIssueDisplayProperties } from "types";
type Props = {
issue: IIssue;
projectId: string;
expandedIssues: string[];
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
properties: IIssueDisplayProperties;
@@ -20,7 +19,6 @@ type Props = {
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
issue,
projectId,
expandedIssues,
setExpandedIssues,
properties,
@@ -48,7 +46,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
<>
<IssueColumn
issue={issue}
projectId={projectId}
expanded={isExpanded}
handleToggleExpand={handleToggleExpand}
properties={properties}
@@ -66,7 +63,6 @@ export const SpreadsheetIssuesColumn: React.FC<Props> = ({
<SpreadsheetIssuesColumn
key={subIssue.id}
issue={subIssue}
projectId={subIssue.project_detail.id}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
properties={properties}
@@ -1,7 +1,7 @@
import React from "react";
// components
import { LabelSelect } from "components/project";
import { LabelSelect } from "components/labels";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@@ -26,8 +26,10 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
<>
<LabelSelect
value={issue.labels}
onChange={(data) => onChange({ labels_list: data })}
onChange={(data) => onChange({ labels: data })}
labels={labels ?? []}
className="h-full"
buttonClassName="!border-0 !h-full !w-full !rounded-none"
hideDropdownArrow
maxRender={1}
disabled={disabled}

Some files were not shown because too many files have changed in this diff Show More