mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'preview' of https://github.com/makeplane/plane into chore/project-settings-events
This commit is contained in:
commit
be60b25dad
@ -7,12 +7,12 @@ import { useTheme } from "next-themes";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Mails, KeyRound } from "lucide-react";
|
import { Mails, KeyRound } from "lucide-react";
|
||||||
import { TInstanceConfigurationKeys } from "@plane/types";
|
import { TInstanceConfigurationKeys } from "@plane/types";
|
||||||
import { Loader, setPromiseToast } from "@plane/ui";
|
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { PageHeader } from "@/components/core";
|
import { PageHeader } from "@/components/core";
|
||||||
// hooks
|
// hooks
|
||||||
// helpers
|
// helpers
|
||||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
import { cn, resolveGeneralTheme } from "@/helpers/common.helper";
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// images
|
// images
|
||||||
import githubLightModeImage from "@/public/logos/github-black.png";
|
import githubLightModeImage from "@/public/logos/github-black.png";
|
||||||
@ -45,6 +45,8 @@ const InstanceAuthenticationPage = observer(() => {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
// derived values
|
||||||
|
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
||||||
|
|
||||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@ -129,7 +131,34 @@ const InstanceAuthenticationPage = observer(() => {
|
|||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-lg font-medium">Authentication modes</div>
|
<div className="text-lg font-medium pb-1">Sign-up configuration</div>
|
||||||
|
<div className={cn("w-full flex items-center gap-14 rounded")}>
|
||||||
|
<div className="flex grow items-center gap-4">
|
||||||
|
<div className="grow">
|
||||||
|
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
|
||||||
|
Allow anyone to sign up without invite
|
||||||
|
</div>
|
||||||
|
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||||
|
Toggling this off will disable self sign ups.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<ToggleSwitch
|
||||||
|
value={Boolean(parseInt(enableSignUpConfig))}
|
||||||
|
onChange={() => {
|
||||||
|
Boolean(parseInt(enableSignUpConfig)) === true
|
||||||
|
? updateConfig("ENABLE_SIGNUP", "0")
|
||||||
|
: updateConfig("ENABLE_SIGNUP", "1");
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-medium pt-6">Authentication modes</div>
|
||||||
{authenticationMethodsCard.map((method) => (
|
{authenticationMethodsCard.map((method) => (
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
key={method.key}
|
key={method.key}
|
||||||
|
@ -5,9 +5,11 @@ import { observer } from "mobx-react-lite";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
|
// ui
|
||||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { WEB_BASE_URL, cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { WEB_BASE_URL } from "@/helpers/common.helper";
|
|
||||||
import { useTheme } from "@/hooks/store";
|
import { useTheme } from "@/hooks/store";
|
||||||
// assets
|
// assets
|
||||||
import packageJson from "package.json";
|
import packageJson from "package.json";
|
||||||
@ -42,9 +44,12 @@ export const HelpSection: FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
|
className={cn(
|
||||||
isSidebarCollapsed ? "flex-col" : ""
|
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
||||||
}`}
|
{
|
||||||
|
"flex-col h-auto py-1.5": isSidebarCollapsed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||||
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||||
|
@ -12,6 +12,7 @@ from .base import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
def get_upload_path(instance, filename):
|
def get_upload_path(instance, filename):
|
||||||
|
filename = filename[:50]
|
||||||
if instance.workspace_id is not None:
|
if instance.workspace_id is not None:
|
||||||
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
|
return f"{instance.workspace.id}/{uuid4().hex}-{filename}"
|
||||||
return f"user-{uuid4().hex}-{filename}"
|
return f"user-{uuid4().hex}-{filename}"
|
||||||
|
@ -112,7 +112,7 @@ export const useEditor = ({
|
|||||||
if (value === null || value === undefined) return;
|
if (value === null || value === undefined) return;
|
||||||
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
|
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
|
||||||
try {
|
try {
|
||||||
editor.commands.setContent(value);
|
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||||
const currentSavedSelection = savedSelectionRef.current;
|
const currentSavedSelection = savedSelectionRef.current;
|
||||||
if (currentSavedSelection) {
|
if (currentSavedSelection) {
|
||||||
const docLength = editor.state.doc.content.size;
|
const docLength = editor.state.doc.content.size;
|
||||||
|
@ -50,9 +50,25 @@ export async function startImageUpload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const fileNameTrimmed = trimFileName(file.name);
|
||||||
|
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
|
||||||
|
|
||||||
|
const resolvedPos = view.state.doc.resolve(pos ?? 0);
|
||||||
|
const nodeBefore = resolvedPos.nodeBefore;
|
||||||
|
|
||||||
|
// if the image is at the start of the line i.e. when nodeBefore is null
|
||||||
|
if (nodeBefore === null) {
|
||||||
|
if (pos) {
|
||||||
|
// so that the image is not inserted at the next line, else incase the
|
||||||
|
// image is inserted at any line where there's some content, the
|
||||||
|
// position is kept as it is to be inserted at the next line
|
||||||
|
pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view.focus();
|
view.focus();
|
||||||
|
|
||||||
const src = await uploadAndValidateImage(file, uploadFile);
|
const src = await uploadAndValidateImage(fileWithTrimmedName, uploadFile);
|
||||||
|
|
||||||
if (src == null) {
|
if (src == null) {
|
||||||
throw new Error("Resolved image URL is undefined.");
|
throw new Error("Resolved image URL is undefined.");
|
||||||
@ -112,3 +128,14 @@ async function uploadAndValidateImage(file: File, uploadFile: UploadImage): Prom
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimFileName(fileName: string, maxLength = 100) {
|
||||||
|
if (fileName.length > maxLength) {
|
||||||
|
const extension = fileName.split(".").pop();
|
||||||
|
const nameWithoutExtension = fileName.slice(0, -(extension?.length ?? 0 + 1));
|
||||||
|
const allowedNameLength = maxLength - (extension?.length ?? 0) - 1; // -1 for the dot
|
||||||
|
return `${nameWithoutExtension.slice(0, allowedNameLength)}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
@ -20,12 +20,15 @@
|
|||||||
"postcss": "postcss styles/globals.css -o styles/output.css --watch"
|
"postcss": "postcss styles/globals.css -o styles/output.css --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.1.10",
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||||
"@blueprintjs/core": "^4.16.3",
|
"@blueprintjs/core": "^4.16.3",
|
||||||
"@blueprintjs/popover2": "^1.13.3",
|
"@blueprintjs/popover2": "^1.13.3",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"emoji-picker-react": "^4.5.16",
|
"emoji-picker-react": "^4.5.16",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.379.0",
|
"lucide-react": "^0.379.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -13,4 +13,5 @@ export * from "./loader";
|
|||||||
export * from "./control-link";
|
export * from "./control-link";
|
||||||
export * from "./toast";
|
export * from "./toast";
|
||||||
export * from "./drag-handle";
|
export * from "./drag-handle";
|
||||||
export * from "./drop-indicator";
|
export * from "./drop-indicator";
|
||||||
|
export * from "./sortable";
|
||||||
|
62
packages/ui/src/sortable/draggable.tsx
Normal file
62
packages/ui/src/sortable/draggable.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { isEqual } from "lodash";
|
||||||
|
import { cn } from "../../helpers";
|
||||||
|
import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
import { DropIndicator } from "../drop-indicator";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
data: any; //@todo make this generic
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
const Draggable = ({ children, data, className }: Props) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [dragging, setDragging] = useState<boolean>(false); // NEW
|
||||||
|
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
||||||
|
|
||||||
|
const [closestEdge, setClosestEdge] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
combine(
|
||||||
|
draggable({
|
||||||
|
element: el,
|
||||||
|
onDragStart: () => setDragging(true), // NEW
|
||||||
|
onDrop: () => setDragging(false), // NEW
|
||||||
|
getInitialData: () => data,
|
||||||
|
}),
|
||||||
|
dropTargetForElements({
|
||||||
|
element: el,
|
||||||
|
onDragEnter: (args) => {
|
||||||
|
setIsDraggedOver(true);
|
||||||
|
setClosestEdge(extractClosestEdge(args.self.data));
|
||||||
|
},
|
||||||
|
onDragLeave: () => setIsDraggedOver(false),
|
||||||
|
onDrop: () => {
|
||||||
|
setIsDraggedOver(false);
|
||||||
|
},
|
||||||
|
canDrop: ({ source }) => !isEqual(source.data, data) && source.data.__uuid__ === data.__uuid__,
|
||||||
|
getData: ({ input, element }) =>
|
||||||
|
attachClosestEdge(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
allowedEdges: ["top", "bottom"],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={cn(dragging && "opacity-25", className)}>
|
||||||
|
{<DropIndicator isVisible={isDraggedOver && closestEdge === "top"} />}
|
||||||
|
{children}
|
||||||
|
{<DropIndicator isVisible={isDraggedOver && closestEdge === "bottom"} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Draggable };
|
2
packages/ui/src/sortable/index.ts
Normal file
2
packages/ui/src/sortable/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./sortable";
|
||||||
|
export * from "./draggable";
|
32
packages/ui/src/sortable/sortable.stories.tsx
Normal file
32
packages/ui/src/sortable/sortable.stories.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import React from "react";
|
||||||
|
import { Draggable } from "./draggable";
|
||||||
|
import { Sortable } from "./sortable";
|
||||||
|
|
||||||
|
const meta: Meta<typeof Sortable> = {
|
||||||
|
title: "Sortable",
|
||||||
|
component: Sortable,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Sortable>;
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ id: "1", name: "John Doe" },
|
||||||
|
{ id: "2", name: "Jane Doe 2" },
|
||||||
|
{ id: "3", name: "Alice" },
|
||||||
|
{ id: "4", name: "Bob" },
|
||||||
|
{ id: "5", name: "Charlie" },
|
||||||
|
];
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
data,
|
||||||
|
render: (item: any) => (
|
||||||
|
// <Draggable data={item} className="rounded-lg">
|
||||||
|
<div className="border ">{item.name}</div>
|
||||||
|
// </Draggable>
|
||||||
|
),
|
||||||
|
onChange: (data) => console.log(data.map(({ id }) => id)),
|
||||||
|
keyExtractor: (item: any) => item.id,
|
||||||
|
},
|
||||||
|
};
|
79
packages/ui/src/sortable/sortable.tsx
Normal file
79
packages/ui/src/sortable/sortable.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { Fragment, useEffect, useMemo } from "react";
|
||||||
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import { Draggable } from "./draggable";
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
data: T[];
|
||||||
|
render: (item: T, index: number) => React.ReactNode;
|
||||||
|
onChange: (data: T[]) => void;
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
containerClassName?: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveItem = <T,>(
|
||||||
|
data: T[],
|
||||||
|
source: T,
|
||||||
|
destination: T & Record<symbol, string>,
|
||||||
|
keyExtractor: (item: T, index: number) => string
|
||||||
|
) => {
|
||||||
|
const sourceIndex = data.indexOf(source);
|
||||||
|
if (sourceIndex === -1) return data;
|
||||||
|
|
||||||
|
const destinationIndex = data.findIndex((item, index) => keyExtractor(item, index) === keyExtractor(destination, 0));
|
||||||
|
|
||||||
|
if (destinationIndex === -1) return data;
|
||||||
|
|
||||||
|
const symbolKey = Reflect.ownKeys(destination).find((key) => key.toString() === "Symbol(closestEdge)");
|
||||||
|
const position = symbolKey ? destination[symbolKey as symbol] : "bottom"; // Add 'as symbol' to cast symbolKey to symbol
|
||||||
|
const newData = [...data];
|
||||||
|
const [movedItem] = newData.splice(sourceIndex, 1);
|
||||||
|
|
||||||
|
let adjustedDestinationIndex = destinationIndex;
|
||||||
|
if (position === "bottom") {
|
||||||
|
adjustedDestinationIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent moving item out of bounds
|
||||||
|
if (adjustedDestinationIndex > newData.length) {
|
||||||
|
adjustedDestinationIndex = newData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
newData.splice(adjustedDestinationIndex, 0, movedItem);
|
||||||
|
|
||||||
|
return newData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Sortable = <T,>({ data, render, onChange, keyExtractor, containerClassName, id }: Props<T>) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = monitorForElements({
|
||||||
|
onDrop({ source, location }) {
|
||||||
|
const destination = location?.current?.dropTargets[0];
|
||||||
|
if (!destination) return;
|
||||||
|
onChange(moveItem(data, source.data as T, destination.data as T & { closestEdge: string }, keyExtractor));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up the subscription on unmount
|
||||||
|
return () => {
|
||||||
|
if (unsubscribe) unsubscribe();
|
||||||
|
};
|
||||||
|
}, [data, keyExtractor, onChange]);
|
||||||
|
|
||||||
|
const enhancedData = useMemo(() => {
|
||||||
|
const uuid = id ? id : Math.random().toString(36).substring(7);
|
||||||
|
return data.map((item) => ({ ...item, __uuid__: uuid }));
|
||||||
|
}, [data, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{enhancedData.map((item, index) => (
|
||||||
|
<Draggable key={keyExtractor(item, index)} data={item} className={containerClassName}>
|
||||||
|
<Fragment>{render(item, index)} </Fragment>
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sortable;
|
1
packages/ui/src/typography/index.tsx
Normal file
1
packages/ui/src/typography/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./sub-heading";
|
15
packages/ui/src/typography/sub-heading.tsx
Normal file
15
packages/ui/src/typography/sub-heading.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cn } from "../../helpers";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
noMargin?: boolean;
|
||||||
|
};
|
||||||
|
const SubHeading = ({ children, className, noMargin }: Props) => (
|
||||||
|
<h3 className={cn("text-xl font-medium text-custom-text-200 block leading-7", !noMargin && "mb-2", className)}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { SubHeading };
|
@ -210,11 +210,7 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||||||
{response !== "" && (
|
{response !== "" && (
|
||||||
<div className="page-block-section max-h-[8rem] text-sm">
|
<div className="page-block-section max-h-[8rem] text-sm">
|
||||||
Response:
|
Response:
|
||||||
<RichTextReadOnlyEditor
|
<RichTextReadOnlyEditor initialValue={`<p>${response}</p>`} ref={responseRef} />
|
||||||
initialValue={`<p>${response}</p>`}
|
|
||||||
containerClassName={response ? "-mx-3 -my-3" : ""}
|
|
||||||
ref={responseRef}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{invalidResponse && (
|
{invalidResponse && (
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
|
||||||
// components
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store";
|
import { useIssueDetail } from "@/hooks/store";
|
||||||
// types
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
// constants
|
// constants
|
||||||
import { BLOCK_HEIGHT } from "../constants";
|
import { BLOCK_HEIGHT } from "../constants";
|
||||||
|
// components
|
||||||
import { ChartAddBlock, ChartDraggable } from "../helpers";
|
import { ChartAddBlock, ChartDraggable } from "../helpers";
|
||||||
import { useGanttChart } from "../hooks";
|
import { useGanttChart } from "../hooks";
|
||||||
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -21,6 +22,7 @@ type Props = {
|
|||||||
enableBlockMove: boolean;
|
enableBlockMove: boolean;
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean;
|
||||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
||||||
@ -33,6 +35,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
|||||||
enableBlockMove,
|
enableBlockMove,
|
||||||
enableAddBlock,
|
enableAddBlock,
|
||||||
ganttContainerRef,
|
ganttContainerRef,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||||
@ -70,6 +73,10 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||||
|
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||||
|
const isBlockHoveredOn = isBlockActive(block.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`block-${block.id}`}
|
key={`block-${block.id}`}
|
||||||
@ -80,10 +87,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("relative h-full", {
|
className={cn("relative h-full", {
|
||||||
"bg-custom-background-80": isBlockActive(block.id),
|
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
|
||||||
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
|
"bg-custom-background-90": isBlockHoveredOn,
|
||||||
block.data.id
|
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isBlockSelected,
|
||||||
),
|
"bg-custom-primary-100/10": isBlockSelected && isBlockHoveredOn,
|
||||||
|
"border border-r-0 border-custom-border-400": isBlockFocused,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// components
|
// hooks
|
||||||
import { HEADER_HEIGHT } from "../constants";
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
|
||||||
import { GanttChartBlock } from "./block";
|
|
||||||
// types
|
|
||||||
// constants
|
// constants
|
||||||
|
import { HEADER_HEIGHT } from "../constants";
|
||||||
|
// types
|
||||||
|
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||||
|
// components
|
||||||
|
import { GanttChartBlock } from "./block";
|
||||||
|
|
||||||
export type GanttChartBlocksProps = {
|
export type GanttChartBlocksProps = {
|
||||||
itemsContainerWidth: number;
|
itemsContainerWidth: number;
|
||||||
@ -17,6 +19,7 @@ export type GanttChartBlocksProps = {
|
|||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean;
|
||||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
||||||
@ -31,6 +34,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
enableAddBlock,
|
enableAddBlock,
|
||||||
ganttContainerRef,
|
ganttContainerRef,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,6 +60,7 @@ export const GanttChartBlocksList: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
enableBlockMove={enableBlockMove}
|
enableBlockMove={enableBlockMove}
|
||||||
enableAddBlock={enableAddBlock}
|
enableAddBlock={enableAddBlock}
|
||||||
ganttContainerRef={ganttContainerRef}
|
ganttContainerRef={ganttContainerRef}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -2,8 +2,8 @@ import { useEffect, useRef } from "react";
|
|||||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectGroup } from "@/components/core";
|
||||||
import {
|
import {
|
||||||
BiWeekChartView,
|
BiWeekChartView,
|
||||||
DayChartView,
|
DayChartView,
|
||||||
@ -18,8 +18,12 @@ import {
|
|||||||
WeekChartView,
|
WeekChartView,
|
||||||
YearChartView,
|
YearChartView,
|
||||||
} from "@/components/gantt-chart";
|
} from "@/components/gantt-chart";
|
||||||
|
import { IssueBulkOperationsRoot } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// constants
|
||||||
|
import { GANTT_SELECT_GROUP } from "../constants";
|
||||||
|
// hooks
|
||||||
import { useGanttChart } from "../hooks/use-gantt-chart";
|
import { useGanttChart } from "../hooks/use-gantt-chart";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -33,6 +37,7 @@ type Props = {
|
|||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
itemsContainerWidth: number;
|
itemsContainerWidth: number;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
@ -53,6 +58,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
|||||||
enableBlockRightResize,
|
enableBlockRightResize,
|
||||||
enableReorder,
|
enableReorder,
|
||||||
enableAddBlock,
|
enableAddBlock,
|
||||||
|
enableSelection,
|
||||||
itemsContainerWidth,
|
itemsContainerWidth,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
sidebarToRender,
|
sidebarToRender,
|
||||||
@ -107,43 +113,58 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
|||||||
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
const ActiveChartView = CHART_VIEW_COMPONENTS[currentView];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<MultipleSelectGroup
|
||||||
// DO NOT REMOVE THE ID
|
containerRef={ganttContainerRef}
|
||||||
id="gantt-container"
|
entities={{
|
||||||
className={cn(
|
[GANTT_SELECT_GROUP]: chartBlocks?.map((block) => block.id) ?? [],
|
||||||
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
|
}}
|
||||||
{
|
|
||||||
"mb-8": bottomSpacing,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
ref={ganttContainerRef}
|
|
||||||
onScroll={onScroll}
|
|
||||||
>
|
>
|
||||||
<GanttChartSidebar
|
{(helpers) => (
|
||||||
blocks={blocks}
|
<>
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
<div
|
||||||
enableReorder={enableReorder}
|
// DO NOT REMOVE THE ID
|
||||||
sidebarToRender={sidebarToRender}
|
id="gantt-container"
|
||||||
title={title}
|
className={cn(
|
||||||
quickAdd={quickAdd}
|
"h-full w-full overflow-auto vertical-scrollbar horizontal-scrollbar scrollbar-lg flex border-t-[0.5px] border-custom-border-200",
|
||||||
/>
|
{
|
||||||
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
"mb-8": bottomSpacing,
|
||||||
<ActiveChartView />
|
}
|
||||||
{currentViewData && (
|
)}
|
||||||
<GanttChartBlocksList
|
ref={ganttContainerRef}
|
||||||
itemsContainerWidth={itemsContainerWidth}
|
onScroll={onScroll}
|
||||||
blocks={chartBlocks}
|
>
|
||||||
blockToRender={blockToRender}
|
<GanttChartSidebar
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
blocks={blocks}
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
enableReorder={enableReorder}
|
||||||
enableBlockMove={enableBlockMove}
|
enableSelection={enableSelection}
|
||||||
enableAddBlock={enableAddBlock}
|
sidebarToRender={sidebarToRender}
|
||||||
ganttContainerRef={ganttContainerRef}
|
title={title}
|
||||||
showAllBlocks={showAllBlocks}
|
quickAdd={quickAdd}
|
||||||
/>
|
selectionHelpers={helpers}
|
||||||
)}
|
/>
|
||||||
</div>
|
<div className="relative min-h-full h-max flex-shrink-0 flex-grow">
|
||||||
</div>
|
<ActiveChartView />
|
||||||
|
{currentViewData && (
|
||||||
|
<GanttChartBlocksList
|
||||||
|
itemsContainerWidth={itemsContainerWidth}
|
||||||
|
blocks={chartBlocks}
|
||||||
|
blockToRender={blockToRender}
|
||||||
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
|
enableBlockMove={enableBlockMove}
|
||||||
|
enableAddBlock={enableAddBlock}
|
||||||
|
ganttContainerRef={ganttContainerRef}
|
||||||
|
showAllBlocks={showAllBlocks}
|
||||||
|
selectionHelpers={helpers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MultipleSelectGroup>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -32,6 +32,7 @@ type ChartViewRootProps = {
|
|||||||
enableBlockMove: boolean;
|
enableBlockMove: boolean;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
enableAddBlock: boolean;
|
enableAddBlock: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
bottomSpacing: boolean;
|
bottomSpacing: boolean;
|
||||||
showAllBlocks: boolean;
|
showAllBlocks: boolean;
|
||||||
quickAdd?: React.JSX.Element | undefined;
|
quickAdd?: React.JSX.Element | undefined;
|
||||||
@ -51,6 +52,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||||||
enableBlockMove,
|
enableBlockMove,
|
||||||
enableReorder,
|
enableReorder,
|
||||||
enableAddBlock,
|
enableAddBlock,
|
||||||
|
enableSelection,
|
||||||
bottomSpacing,
|
bottomSpacing,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
quickAdd,
|
quickAdd,
|
||||||
@ -184,6 +186,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||||||
enableBlockRightResize={enableBlockRightResize}
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
enableReorder={enableReorder}
|
enableReorder={enableReorder}
|
||||||
enableAddBlock={enableAddBlock}
|
enableAddBlock={enableAddBlock}
|
||||||
|
enableSelection={enableSelection}
|
||||||
itemsContainerWidth={itemsContainerWidth}
|
itemsContainerWidth={itemsContainerWidth}
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
sidebarToRender={sidebarToRender}
|
sidebarToRender={sidebarToRender}
|
||||||
|
@ -3,3 +3,5 @@ export const BLOCK_HEIGHT = 44;
|
|||||||
export const HEADER_HEIGHT = 60;
|
export const HEADER_HEIGHT = 60;
|
||||||
|
|
||||||
export const SIDEBAR_WIDTH = 360;
|
export const SIDEBAR_WIDTH = 360;
|
||||||
|
|
||||||
|
export const GANTT_SELECT_GROUP = "gantt-issues";
|
||||||
|
@ -18,6 +18,7 @@ type GanttChartRootProps = {
|
|||||||
enableBlockMove?: boolean;
|
enableBlockMove?: boolean;
|
||||||
enableReorder?: boolean;
|
enableReorder?: boolean;
|
||||||
enableAddBlock?: boolean;
|
enableAddBlock?: boolean;
|
||||||
|
enableSelection?: boolean;
|
||||||
bottomSpacing?: boolean;
|
bottomSpacing?: boolean;
|
||||||
showAllBlocks?: boolean;
|
showAllBlocks?: boolean;
|
||||||
};
|
};
|
||||||
@ -36,6 +37,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
|||||||
enableBlockMove = false,
|
enableBlockMove = false,
|
||||||
enableReorder = false,
|
enableReorder = false,
|
||||||
enableAddBlock = false,
|
enableAddBlock = false,
|
||||||
|
enableSelection = false,
|
||||||
bottomSpacing = false,
|
bottomSpacing = false,
|
||||||
showAllBlocks = false,
|
showAllBlocks = false,
|
||||||
quickAdd,
|
quickAdd,
|
||||||
@ -56,6 +58,7 @@ export const GanttChartRoot: FC<GanttChartRootProps> = (props) => {
|
|||||||
enableBlockMove={enableBlockMove}
|
enableBlockMove={enableBlockMove}
|
||||||
enableReorder={enableReorder}
|
enableReorder={enableReorder}
|
||||||
enableAddBlock={enableAddBlock}
|
enableAddBlock={enableAddBlock}
|
||||||
|
enableSelection={enableSelection}
|
||||||
bottomSpacing={bottomSpacing}
|
bottomSpacing={bottomSpacing}
|
||||||
showAllBlocks={showAllBlocks}
|
showAllBlocks={showAllBlocks}
|
||||||
quickAdd={quickAdd}
|
quickAdd={quickAdd}
|
||||||
|
@ -38,7 +38,7 @@ export const CyclesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
"bg-custom-background-80": isBlockActive(block.id),
|
"bg-custom-background-90": isBlockActive(block.id),
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
height: `${BLOCK_HEIGHT}px`,
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
@ -1,63 +1,87 @@
|
|||||||
import React, { MutableRefObject } from "react";
|
import React, { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
|
||||||
import { useGanttChart } from "@/components/gantt-chart/hooks";
|
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectEntityAction } from "@/components/core";
|
||||||
|
import { useGanttChart } from "@/components/gantt-chart/hooks";
|
||||||
import { IssueGanttSidebarBlock } from "@/components/issues";
|
import { IssueGanttSidebarBlock } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
import { findTotalDaysInRange } from "@/helpers/date-time.helper";
|
||||||
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store";
|
import { useIssueDetail } from "@/hooks/store";
|
||||||
// types
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
// constants
|
// constants
|
||||||
import { BLOCK_HEIGHT } from "../../constants";
|
import { BLOCK_HEIGHT, GANTT_SELECT_GROUP } from "../../constants";
|
||||||
|
// types
|
||||||
import { IGanttBlock } from "../../types";
|
import { IGanttBlock } from "../../types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
dragHandleRef: MutableRefObject<HTMLButtonElement | null>;
|
||||||
|
selectionHelpers?: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesSidebarBlock = observer((props: Props) => {
|
export const IssuesSidebarBlock = observer((props: Props) => {
|
||||||
const { block, enableReorder, isDragging, dragHandleRef } = props;
|
const { block, enableReorder, enableSelection, isDragging, dragHandleRef, selectionHelpers } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
const { updateActiveBlockId, isBlockActive } = useGanttChart();
|
||||||
const { getIsIssuePeeked } = useIssueDetail();
|
const { getIsIssuePeeked } = useIssueDetail();
|
||||||
|
|
||||||
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
const duration = findTotalDaysInRange(block.start_date, block.target_date);
|
||||||
|
|
||||||
|
const isIssueSelected = selectionHelpers?.getIsEntitySelected(block.id);
|
||||||
|
const isIssueFocused = selectionHelpers?.getIsEntityActive(block.id);
|
||||||
|
const isBlockHoveredOn = isBlockActive(block.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn({
|
className={cn("group/list-block", {
|
||||||
"rounded bg-custom-background-80": isDragging,
|
"rounded bg-custom-background-80": isDragging,
|
||||||
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
|
"rounded-l border border-r-0 border-custom-primary-70": getIsIssuePeeked(block.data.id),
|
||||||
block.data.id
|
"border border-r-0 border-custom-border-400": isIssueFocused,
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
onMouseEnter={() => updateActiveBlockId(block.id)}
|
onMouseEnter={() => updateActiveBlockId(block.id)}
|
||||||
onMouseLeave={() => updateActiveBlockId(null)}
|
onMouseLeave={() => updateActiveBlockId(null)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
"bg-custom-background-80": isBlockActive(block.id),
|
"bg-custom-background-90": isBlockHoveredOn,
|
||||||
|
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isIssueSelected,
|
||||||
|
"bg-custom-primary-100/10": isIssueSelected && isBlockHoveredOn,
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
height: `${BLOCK_HEIGHT}px`,
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{enableReorder && (
|
<div className="flex items-center gap-2">
|
||||||
<button
|
{enableReorder && (
|
||||||
type="button"
|
<button
|
||||||
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
type="button"
|
||||||
ref={dragHandleRef}
|
className="flex flex-shrink-0 rounded p-0.5 text-custom-sidebar-text-200 opacity-0 group-hover:opacity-100"
|
||||||
>
|
ref={dragHandleRef}
|
||||||
<MoreVertical className="h-3.5 w-3.5" />
|
>
|
||||||
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
<MoreVertical className="h-3.5 w-3.5" />
|
||||||
</button>
|
<MoreVertical className="-ml-5 h-3.5 w-3.5" />
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
{enableSelection && selectionHelpers && (
|
||||||
|
<MultipleSelectEntityAction
|
||||||
|
className={cn(
|
||||||
|
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": isIssueSelected,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
groupId={GANTT_SELECT_GROUP}
|
||||||
|
id={block.id}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
<div className="flex h-full flex-grow items-center justify-between gap-2 truncate">
|
||||||
<div className="flex-grow truncate">
|
<div className="flex-grow truncate">
|
||||||
<IssueGanttSidebarBlock issueId={block.data.id} />
|
<IssueGanttSidebarBlock issueId={block.data.id} />
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
import { MutableRefObject } from "react";
|
import { MutableRefObject } from "react";
|
||||||
// components
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// types
|
// components
|
||||||
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
import { IGanttBlock, IBlockUpdateData } from "@/components/gantt-chart/types";
|
||||||
|
// hooks
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
import { GanttDnDHOC } from "../gantt-dnd-HOC";
|
||||||
import { handleOrderChange } from "../utils";
|
import { handleOrderChange } from "../utils";
|
||||||
|
// types
|
||||||
import { IssuesSidebarBlock } from "./block";
|
import { IssuesSidebarBlock } from "./block";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
blocks: IGanttBlock[] | null;
|
blocks: IGanttBlock[] | null;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
showAllBlocks?: boolean;
|
showAllBlocks?: boolean;
|
||||||
|
selectionHelpers?: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
||||||
const { blockUpdateHandler, blocks, enableReorder, showAllBlocks = false } = props;
|
const { blockUpdateHandler, blocks, enableReorder, enableSelection, showAllBlocks = false, selectionHelpers } = props;
|
||||||
|
|
||||||
const handleOnDrop = (
|
const handleOnDrop = (
|
||||||
draggingBlockId: string | undefined,
|
draggingBlockId: string | undefined,
|
||||||
@ -47,8 +51,10 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<IssuesSidebarBlock
|
<IssuesSidebarBlock
|
||||||
block={block}
|
block={block}
|
||||||
enableReorder={enableReorder}
|
enableReorder={enableReorder}
|
||||||
|
enableSelection={enableSelection}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
dragHandleRef={dragHandleRef}
|
dragHandleRef={dragHandleRef}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</GanttDnDHOC>
|
</GanttDnDHOC>
|
||||||
|
@ -38,7 +38,7 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
|
|||||||
<div
|
<div
|
||||||
id={`sidebar-block-${block.id}`}
|
id={`sidebar-block-${block.id}`}
|
||||||
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
className={cn("group w-full flex items-center gap-2 pl-2 pr-4", {
|
||||||
"bg-custom-background-80": isBlockActive(block.id),
|
"bg-custom-background-90": isBlockActive(block.id),
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
height: `${BLOCK_HEIGHT}px`,
|
height: `${BLOCK_HEIGHT}px`,
|
||||||
|
@ -1,19 +1,38 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectGroupAction } from "@/components/core";
|
||||||
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
import { IBlockUpdateData, IGanttBlock } from "@/components/gantt-chart";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
// constants
|
// constants
|
||||||
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
import { GANTT_SELECT_GROUP, HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
blocks: IGanttBlock[] | null;
|
blocks: IGanttBlock[] | null;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
quickAdd?: React.JSX.Element | undefined;
|
quickAdd?: React.JSX.Element | undefined;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttChartSidebar: React.FC<Props> = (props) => {
|
export const GanttChartSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { blocks, blockUpdateHandler, enableReorder, sidebarToRender, title, quickAdd } = props;
|
const {
|
||||||
|
blocks,
|
||||||
|
blockUpdateHandler,
|
||||||
|
enableReorder,
|
||||||
|
enableSelection,
|
||||||
|
sidebarToRender,
|
||||||
|
title,
|
||||||
|
quickAdd,
|
||||||
|
selectionHelpers,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(GANTT_SELECT_GROUP) === "empty";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -25,19 +44,39 @@ export const GanttChartSidebar: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-8 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
|
className="group/list-header box-border flex-shrink-0 flex items-end justify-between gap-2 border-b-[0.5px] border-custom-border-200 pb-2 pl-2 pr-4 text-sm font-medium text-custom-text-300 sticky top-0 z-10 bg-custom-background-100"
|
||||||
style={{
|
style={{
|
||||||
height: `${HEADER_HEIGHT}px`,
|
height: `${HEADER_HEIGHT}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h6>{title}</h6>
|
<div
|
||||||
|
className={cn("flex items-center gap-2", {
|
||||||
|
"pl-2": !enableSelection,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{enableSelection && (
|
||||||
|
<div className="flex-shrink-0 flex items-center w-3.5">
|
||||||
|
<MultipleSelectGroupAction
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
groupID={GANTT_SELECT_GROUP}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h6>{title}</h6>
|
||||||
|
</div>
|
||||||
<h6>Duration</h6>
|
<h6>Duration</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
|
<div className="min-h-full h-max bg-custom-background-100 overflow-hidden">
|
||||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
{sidebarToRender?.({ title, blockUpdateHandler, blocks, enableReorder, enableSelection, selectionHelpers })}
|
||||||
</div>
|
</div>
|
||||||
{quickAdd ? quickAdd : null}
|
{quickAdd ? quickAdd : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { FC, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
|
||||||
import { Inbox, PanelLeft } from "lucide-react";
|
import { Inbox, PanelLeft } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
@ -10,6 +9,7 @@ import { InboxLayoutLoader } from "@/components/ui";
|
|||||||
import { EmptyStateType } from "@/constants/empty-state";
|
import { EmptyStateType } from "@/constants/empty-state";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectInbox } from "@/hooks/store";
|
import { useProjectInbox } from "@/hooks/store";
|
||||||
|
|
||||||
@ -18,25 +18,25 @@ type TInboxIssueRoot = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
inboxIssueId: string | undefined;
|
inboxIssueId: string | undefined;
|
||||||
inboxAccessible: boolean;
|
inboxAccessible: boolean;
|
||||||
|
navigationTab?: EInboxIssueCurrentTab | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
|
export const InboxIssueRoot: FC<TInboxIssueRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible } = props;
|
const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props;
|
||||||
// states
|
// states
|
||||||
const [isMobileSidebar, setIsMobileSidebar] = useState(true);
|
const [isMobileSidebar, setIsMobileSidebar] = useState(true);
|
||||||
// hooks
|
// hooks
|
||||||
const { loader, error, fetchInboxIssues } = useProjectInbox();
|
const { loader, error, currentTab, handleCurrentTab, fetchInboxIssues } = useProjectInbox();
|
||||||
|
|
||||||
useSWR(
|
useEffect(() => {
|
||||||
inboxAccessible && workspaceSlug && projectId ? `PROJECT_INBOX_ISSUES_${workspaceSlug}_${projectId}` : null,
|
if (!inboxAccessible || !workspaceSlug || !projectId) return;
|
||||||
async () => {
|
if (navigationTab && navigationTab !== currentTab) {
|
||||||
inboxAccessible &&
|
handleCurrentTab(navigationTab);
|
||||||
workspaceSlug &&
|
} else {
|
||||||
projectId &&
|
fetchInboxIssues(workspaceSlug.toString(), projectId.toString());
|
||||||
(await fetchInboxIssues(workspaceSlug.toString(), projectId.toString()));
|
}
|
||||||
},
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
{ revalidateOnFocus: false, revalidateIfStale: false }
|
}, [inboxAccessible, workspaceSlug, projectId]);
|
||||||
);
|
|
||||||
|
|
||||||
// loader
|
// loader
|
||||||
if (loader === "init-loading")
|
if (loader === "init-loading")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useCallback, useRef } from "react";
|
import { FC, useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TInboxIssueCurrentTab } from "@plane/types";
|
import { TInboxIssueCurrentTab } from "@plane/types";
|
||||||
@ -37,7 +37,7 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
|||||||
const { workspaceSlug, projectId, setIsMobileSidebar } = props;
|
const { workspaceSlug, projectId, setIsMobileSidebar } = props;
|
||||||
// ref
|
// ref
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const [elementRef, setElementRef] = useState<HTMLDivElement | null>(null);
|
||||||
// store
|
// store
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const {
|
const {
|
||||||
@ -72,8 +72,10 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
|||||||
currentTab === option?.key ? `text-custom-primary-100` : `hover:text-custom-text-200`
|
currentTab === option?.key ? `text-custom-primary-100` : `hover:text-custom-text-200`
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (currentTab != option?.key) handleCurrentTab(option?.key);
|
if (currentTab != option?.key) {
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
|
handleCurrentTab(option?.key);
|
||||||
|
router.push(`/${workspaceSlug}/projects/${projectId}/inbox?currentTab=${option?.key}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>{option?.label}</div>
|
<div>{option?.label}</div>
|
||||||
@ -126,14 +128,14 @@ export const InboxSidebar: FC<IInboxSidebarProps> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{inboxIssuePaginationInfo?.next_page_results && (
|
<div ref={setElementRef}>
|
||||||
<div ref={elementRef}>
|
{inboxIssuePaginationInfo?.next_page_results && (
|
||||||
<Loader className="mx-auto w-full space-y-4 py-4 px-2">
|
<Loader className="mx-auto w-full space-y-4 py-4 px-2">
|
||||||
<Loader.Item height="64px" width="w-100" />
|
<Loader.Item height="64px" width="w-100" />
|
||||||
<Loader.Item height="64px" width="w-100" />
|
<Loader.Item height="64px" width="w-100" />
|
||||||
</Loader>
|
</Loader>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
2
web/components/issues/bulk-operations/index.ts
Normal file
2
web/components/issues/bulk-operations/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./root";
|
||||||
|
export * from "./upgrade-banner";
|
21
web/components/issues/bulk-operations/root.tsx
Normal file
21
web/components/issues/bulk-operations/root.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
// components
|
||||||
|
import { BulkOperationsUpgradeBanner } from "@/components/issues";
|
||||||
|
// hooks
|
||||||
|
import { useMultipleSelectStore } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueBulkOperationsRoot: React.FC<Props> = observer((props) => {
|
||||||
|
const { className } = props;
|
||||||
|
// store hooks
|
||||||
|
const { isSelectionActive } = useMultipleSelectStore();
|
||||||
|
|
||||||
|
if (!isSelectionActive) return null;
|
||||||
|
|
||||||
|
return <BulkOperationsUpgradeBanner className={className} />;
|
||||||
|
});
|
32
web/components/issues/bulk-operations/upgrade-banner.tsx
Normal file
32
web/components/issues/bulk-operations/upgrade-banner.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// ui
|
||||||
|
import { getButtonStyling } from "@plane/ui";
|
||||||
|
// constants
|
||||||
|
import { MARKETING_PLANE_ONE_PAGE_LINK } from "@/constants/common";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BulkOperationsUpgradeBanner: React.FC<Props> = (props) => {
|
||||||
|
const { className } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("sticky bottom-0 left-0 h-20 z-[2] px-3.5 grid place-items-center", className)}>
|
||||||
|
<div className="h-14 w-full bg-custom-primary-100/10 border-[0.5px] border-custom-primary-100/50 py-4 px-3.5 flex items-center justify-between gap-2 rounded-md">
|
||||||
|
<p className="text-custom-primary-100 font-medium">
|
||||||
|
Change state, priority, and more for several issues at once. Save three minutes on an average per operation.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={MARKETING_PLANE_ONE_PAGE_LINK}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={cn(getButtonStyling("primary", "sm"), "flex-shrink-0")}
|
||||||
|
>
|
||||||
|
Upgrade to One
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./attachment";
|
export * from "./attachment";
|
||||||
|
export * from "./bulk-operations";
|
||||||
export * from "./issue-modal";
|
export * from "./issue-modal";
|
||||||
export * from "./delete-issue-modal";
|
export * from "./delete-issue-modal";
|
||||||
export * from "./issue-layouts";
|
export * from "./issue-layouts";
|
||||||
|
@ -44,11 +44,11 @@ export const ReactionSelector: React.FC<Props> = (props) => {
|
|||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
className={`absolute -left-2 z-10 bg-custom-sidebar-background-100 ${
|
className={`absolute z-10 bg-custom-sidebar-background-100 ${
|
||||||
position === "top" ? "-top-12" : "-bottom-12"
|
position === "top" ? "-top-12" : "-bottom-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1 shadow-custom-shadow-sm">
|
<div className="rounded-md border border-custom-border-200 bg-custom-sidebar-background-100 p-1">
|
||||||
<div className="flex gap-x-1">
|
<div className="flex gap-x-1">
|
||||||
{reactionEmojis.map((emoji) => (
|
{reactionEmojis.map((emoji) => (
|
||||||
<button
|
<button
|
||||||
|
@ -72,6 +72,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
enableBlockMove={isAllowed}
|
enableBlockMove={isAllowed}
|
||||||
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
enableReorder={appliedDisplayFilters?.order_by === "sort_order" && isAllowed}
|
||||||
enableAddBlock={isAllowed}
|
enableAddBlock={isAllowed}
|
||||||
|
enableSelection={isAllowed}
|
||||||
quickAdd={
|
quickAdd={
|
||||||
enableIssueCreation && isAllowed ? (
|
enableIssueCreation && isAllowed ? (
|
||||||
<GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} />
|
<GanttQuickAddIssueForm quickAddCallback={issues.quickAddIssue} viewId={viewId} />
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { FC, useCallback } from "react";
|
import { FC, useCallback } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// types
|
// constants
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
// hooks
|
||||||
import { useIssues, useUser } from "@/hooks/store";
|
import { useIssues, useUser } from "@/hooks/store";
|
||||||
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
|
import { useGroupIssuesDragNDrop } from "@/hooks/use-group-dragndrop";
|
||||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||||
// components
|
// components
|
||||||
import { List } from "./default";
|
import { List } from "./default";
|
||||||
|
// types
|
||||||
import { IQuickActionProps, TRenderQuickActions } from "./list-view-types";
|
import { IQuickActionProps, TRenderQuickActions } from "./list-view-types";
|
||||||
// constants
|
|
||||||
// hooks
|
|
||||||
|
|
||||||
type ListStoreType =
|
type ListStoreType =
|
||||||
| EIssuesStoreType.PROJECT
|
| EIssuesStoreType.PROJECT
|
||||||
@ -37,22 +37,19 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
canEditPropertiesBasedOnProject,
|
canEditPropertiesBasedOnProject,
|
||||||
isCompletedCycle = false,
|
isCompletedCycle = false,
|
||||||
} = props;
|
} = props;
|
||||||
// router
|
// store hooks
|
||||||
//stores
|
|
||||||
const { issuesFilter, issues } = useIssues(storeType);
|
const { issuesFilter, issues } = useIssues(storeType);
|
||||||
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType);
|
const { updateIssue, removeIssue, removeIssueFromView, archiveIssue, restoreIssue } = useIssuesActions(storeType);
|
||||||
// mobx store
|
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
const { issueMap } = useIssues();
|
const { issueMap } = useIssues();
|
||||||
|
// derived values
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
const issueIds = issues?.groupedIssueIds || [];
|
const issueIds = issues?.groupedIssueIds || [];
|
||||||
|
// auth
|
||||||
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
||||||
|
|
||||||
const canEditProperties = useCallback(
|
const canEditProperties = useCallback(
|
||||||
(projectId: string | undefined) => {
|
(projectId: string | undefined) => {
|
||||||
const isEditingAllowedBasedOnProject =
|
const isEditingAllowedBasedOnProject =
|
||||||
@ -90,7 +87,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative h-full w-full bg-custom-background-90`}>
|
<div className="relative size-full bg-custom-background-90">
|
||||||
<List
|
<List
|
||||||
issuesMap={issueMap}
|
issuesMap={issueMap}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
|
@ -10,6 +10,7 @@ import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
|||||||
import { IssueBlock } from "@/components/issues/issue-layouts/list";
|
import { IssueBlock } from "@/components/issues/issue-layouts/list";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store";
|
import { useIssueDetail } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
// types
|
// types
|
||||||
import { HIGHLIGHT_CLASS, getIssueBlockId } from "../utils";
|
import { HIGHLIGHT_CLASS, getIssueBlockId } from "../utils";
|
||||||
@ -26,6 +27,7 @@ type Props = {
|
|||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
spacingLeft?: number;
|
spacingLeft?: number;
|
||||||
containerRef: MutableRefObject<HTMLDivElement | null>;
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
isDragAllowed: boolean;
|
isDragAllowed: boolean;
|
||||||
canDropOverIssue: boolean;
|
canDropOverIssue: boolean;
|
||||||
@ -50,6 +52,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
|||||||
canDropOverIssue,
|
canDropOverIssue,
|
||||||
isParentIssueBeingDragged = false,
|
isParentIssueBeingDragged = false,
|
||||||
isLastChild = false,
|
isLastChild = false,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
@ -132,6 +135,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
|||||||
setExpanded={setExpanded}
|
setExpanded={setExpanded}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
spacingLeft={spacingLeft}
|
spacingLeft={spacingLeft}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
canDrag={!isSubIssue && isDragAllowed}
|
canDrag={!isSubIssue && isDragAllowed}
|
||||||
isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging}
|
isCurrentBlockDragging={isParentIssueBeingDragged || isCurrentBlockDragging}
|
||||||
setIsCurrentBlockDragging={setIsCurrentBlockDragging}
|
setIsCurrentBlockDragging={setIsCurrentBlockDragging}
|
||||||
@ -139,9 +143,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
|||||||
</RenderIfVisible>
|
</RenderIfVisible>
|
||||||
|
|
||||||
{isExpanded &&
|
{isExpanded &&
|
||||||
subIssues &&
|
subIssues?.map((subIssueId: string) => (
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<IssueBlockRoot
|
<IssueBlockRoot
|
||||||
key={`${subIssueId}`}
|
key={`${subIssueId}`}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
@ -154,6 +156,7 @@ export const IssueBlockRoot: FC<Props> = observer((props) => {
|
|||||||
nestingLevel={nestingLevel + 1}
|
nestingLevel={nestingLevel + 1}
|
||||||
spacingLeft={spacingLeft + (displayProperties?.key ? 12 : 0)}
|
spacingLeft={spacingLeft + (displayProperties?.key ? 12 : 0)}
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
isDragAllowed={isDragAllowed}
|
isDragAllowed={isDragAllowed}
|
||||||
canDropOverIssue={canDropOverIssue}
|
canDropOverIssue={canDropOverIssue}
|
||||||
|
@ -8,11 +8,13 @@ import { TIssue, IIssueDisplayProperties, TIssueMap } from "@plane/types";
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui";
|
import { Spinner, Tooltip, ControlLink, DragHandle } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectEntityAction } from "@/components/core";
|
||||||
import { IssueProperties } from "@/components/issues/issue-layouts/properties";
|
import { IssueProperties } from "@/components/issues/issue-layouts/properties";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppRouter, useIssueDetail, useProject } from "@/hooks/store";
|
import { useAppRouter, useIssueDetail, useProject } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// types
|
// types
|
||||||
import { TRenderQuickActions } from "./list-view-types";
|
import { TRenderQuickActions } from "./list-view-types";
|
||||||
@ -29,6 +31,7 @@ interface IssueBlockProps {
|
|||||||
spacingLeft?: number;
|
spacingLeft?: number;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
setExpanded: Dispatch<SetStateAction<boolean>>;
|
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
isCurrentBlockDragging: boolean;
|
isCurrentBlockDragging: boolean;
|
||||||
setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsCurrentBlockDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
canDrag: boolean;
|
canDrag: boolean;
|
||||||
@ -47,6 +50,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
|||||||
spacingLeft = 14,
|
spacingLeft = 14,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
setExpanded,
|
setExpanded,
|
||||||
|
selectionHelpers,
|
||||||
isCurrentBlockDragging,
|
isCurrentBlockDragging,
|
||||||
setIsCurrentBlockDragging,
|
setIsCurrentBlockDragging,
|
||||||
canDrag,
|
canDrag,
|
||||||
@ -55,7 +59,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
|||||||
const issueRef = useRef<HTMLDivElement | null>(null);
|
const issueRef = useRef<HTMLDivElement | null>(null);
|
||||||
const dragHandleRef = useRef(null);
|
const dragHandleRef = useRef(null);
|
||||||
// hooks
|
// hooks
|
||||||
const { workspaceSlug } = useAppRouter();
|
const { workspaceSlug, projectId } = useAppRouter();
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail();
|
const { getIsIssuePeeked, peekIssue, setPeekIssue, subIssues: subIssuesStore } = useIssueDetail();
|
||||||
|
|
||||||
@ -98,8 +102,11 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
|||||||
|
|
||||||
const canEditIssueProperties = canEditProperties(issue.project_id);
|
const canEditIssueProperties = canEditProperties(issue.project_id);
|
||||||
const projectIdentifier = getProjectIdentifierById(issue.project_id);
|
const projectIdentifier = getProjectIdentifierById(issue.project_id);
|
||||||
|
const isIssueSelected = selectionHelpers.getIsEntitySelected(issue.id);
|
||||||
|
const isIssueActive = selectionHelpers.getIsEntityActive(issue.id);
|
||||||
|
const isSubIssue = nestingLevel !== 0;
|
||||||
|
|
||||||
const paddingLeft = `${spacingLeft}px`;
|
const marginLeft = `${spacingLeft}px`;
|
||||||
|
|
||||||
const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleToggleExpand = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -119,39 +126,76 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
|||||||
<div
|
<div
|
||||||
ref={issueRef}
|
ref={issueRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group min-h-11 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 pl-1.5 pr-1 text-sm",
|
"group/list-block min-h-11 relative flex flex-col md:flex-row md:items-center gap-3 bg-custom-background-100 hover:bg-custom-background-90 p-3 pl-1.5 text-sm transition-colors border border-transparent",
|
||||||
{
|
{
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
"border-custom-primary-70": getIsIssuePeeked(issue.id) && peekIssue?.nestingLevel === nestingLevel,
|
||||||
getIsIssuePeeked(issue.id) && peekIssue?.nestingLevel === nestingLevel,
|
"border-custom-border-400": isIssueActive,
|
||||||
"last:border-b-transparent": !getIsIssuePeeked(issue.id),
|
"last:border-b-transparent": !getIsIssuePeeked(issue.id) && !isIssueActive,
|
||||||
|
"bg-custom-primary-100/5 hover:bg-custom-primary-100/10": isIssueSelected,
|
||||||
"bg-custom-background-80": isCurrentBlockDragging,
|
"bg-custom-background-80": isCurrentBlockDragging,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full truncate" style={nestingLevel !== 0 ? { paddingLeft } : {}}>
|
<div className="flex w-full truncate">
|
||||||
<div className="flex flex-grow items-center gap-3 truncate">
|
<div className="flex flex-grow items-center gap-3 truncate">
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5">
|
||||||
<div className="flex items-center">
|
{/* drag handle */}
|
||||||
|
<div className="size-4 flex items-center group/drag-handle">
|
||||||
<DragHandle
|
<DragHandle
|
||||||
ref={dragHandleRef}
|
ref={dragHandleRef}
|
||||||
disabled={!canDrag}
|
disabled={!canDrag}
|
||||||
className={cn("opacity-0 group-hover:opacity-100", {
|
className={cn("opacity-0 group-hover/drag-handle:opacity-100", {
|
||||||
"opacity-100": isCurrentBlockDragging,
|
"opacity-100": isCurrentBlockDragging,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<div className="flex h-5 w-5 items-center justify-center">
|
</div>
|
||||||
{subIssuesCount > 0 && (
|
{/* select checkbox */}
|
||||||
<button
|
{projectId && canEditIssueProperties && (
|
||||||
className="flex items-center justify-center h-5 w-5 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
<Tooltip
|
||||||
onClick={handleToggleExpand}
|
tooltipContent={
|
||||||
>
|
<>
|
||||||
<ChevronRight className={`h-4 w-4 ${isExpanded ? "rotate-90" : ""}`} />
|
Only issues within the current
|
||||||
</button>
|
<br />
|
||||||
)}
|
project can be selected.
|
||||||
</div>
|
</>
|
||||||
|
}
|
||||||
|
disabled={issue.project_id === projectId}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 grid place-items-center w-3.5">
|
||||||
|
<MultipleSelectEntityAction
|
||||||
|
className={cn(
|
||||||
|
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": isIssueSelected,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
groupId={groupId}
|
||||||
|
id={issue.id}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
disabled={issue.project_id !== projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{/* sub-issues chevron */}
|
||||||
|
<div className="size-4 grid place-items-center flex-shrink-0" style={isSubIssue ? { marginLeft } : {}}>
|
||||||
|
{subIssuesCount > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="size-4 grid place-items-center rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||||
|
onClick={handleToggleExpand}
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn("size-4", {
|
||||||
|
"rotate-90": isExpanded,
|
||||||
|
})}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{displayProperties && displayProperties?.key && (
|
{displayProperties && displayProperties?.key && (
|
||||||
<div className="pl-1 flex-shrink-0 text-xs font-medium text-custom-text-300">
|
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||||
{projectIdentifier}-{issue.sequence_id}
|
{projectIdentifier}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -183,7 +227,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!issue?.tempId && (
|
{!issue?.tempId && (
|
||||||
<div className="block md:hidden border border-custom-border-300 rounded ">
|
<div className="block md:hidden border border-custom-border-300 rounded">
|
||||||
{quickActions({
|
{quickActions({
|
||||||
issue,
|
issue,
|
||||||
parentRef: issueRef,
|
parentRef: issueRef,
|
||||||
|
@ -2,6 +2,7 @@ import { FC, MutableRefObject } from "react";
|
|||||||
// components
|
// components
|
||||||
import { TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
||||||
import { IssueBlockRoot } from "@/components/issues/issue-layouts/list";
|
import { IssueBlockRoot } from "@/components/issues/issue-layouts/list";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
// types
|
// types
|
||||||
import { TRenderQuickActions } from "./list-view-types";
|
import { TRenderQuickActions } from "./list-view-types";
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ interface Props {
|
|||||||
containerRef: MutableRefObject<HTMLDivElement | null>;
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
isDragAllowed: boolean;
|
isDragAllowed: boolean;
|
||||||
canDropOverIssue: boolean;
|
canDropOverIssue: boolean;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlocksList: FC<Props> = (props) => {
|
export const IssueBlocksList: FC<Props> = (props) => {
|
||||||
@ -28,33 +30,33 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
|||||||
displayProperties,
|
displayProperties,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
containerRef,
|
containerRef,
|
||||||
|
selectionHelpers,
|
||||||
isDragAllowed,
|
isDragAllowed,
|
||||||
canDropOverIssue,
|
canDropOverIssue,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative size-full">
|
||||||
{issueIds &&
|
{issueIds?.map((issueId, index) => (
|
||||||
issueIds.length > 0 &&
|
<IssueBlockRoot
|
||||||
issueIds.map((issueId: string, index) => (
|
key={`${issueId}`}
|
||||||
<IssueBlockRoot
|
issueIds={issueIds}
|
||||||
key={`${issueId}`}
|
issueId={issueId}
|
||||||
issueIds={issueIds}
|
issuesMap={issuesMap}
|
||||||
issueId={issueId}
|
updateIssue={updateIssue}
|
||||||
issuesMap={issuesMap}
|
quickActions={quickActions}
|
||||||
updateIssue={updateIssue}
|
canEditProperties={canEditProperties}
|
||||||
quickActions={quickActions}
|
displayProperties={displayProperties}
|
||||||
canEditProperties={canEditProperties}
|
nestingLevel={0}
|
||||||
displayProperties={displayProperties}
|
spacingLeft={0}
|
||||||
nestingLevel={0}
|
containerRef={containerRef}
|
||||||
spacingLeft={0}
|
selectionHelpers={selectionHelpers}
|
||||||
containerRef={containerRef}
|
groupId={groupId}
|
||||||
groupId={groupId}
|
isLastChild={index === issueIds.length - 1}
|
||||||
isLastChild={index === issueIds.length - 1}
|
isDragAllowed={isDragAllowed}
|
||||||
isDragAllowed={isDragAllowed}
|
canDropOverIssue={canDropOverIssue}
|
||||||
canDropOverIssue={canDropOverIssue}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||||
// components
|
import { observer } from "mobx-react";
|
||||||
|
// types
|
||||||
import {
|
import {
|
||||||
GroupByColumnTypes,
|
GroupByColumnTypes,
|
||||||
TGroupedIssues,
|
TGroupedIssues,
|
||||||
@ -13,6 +14,9 @@ import {
|
|||||||
TIssueOrderByOptions,
|
TIssueOrderByOptions,
|
||||||
TIssueGroupByOptions,
|
TIssueGroupByOptions,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { MultipleSelectGroup } from "@/components/core";
|
||||||
|
import { IssueBulkOperationsRoot } from "@/components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||||
@ -46,7 +50,7 @@ export interface IGroupByList {
|
|||||||
isCompletedCycle?: boolean;
|
isCompletedCycle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByList: React.FC<IGroupByList> = (props) => {
|
const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
issueIds,
|
issueIds,
|
||||||
issuesMap,
|
issuesMap,
|
||||||
@ -113,43 +117,69 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
|
|
||||||
const is_list = group_by === null ? true : false;
|
const is_list = group_by === null ? true : false;
|
||||||
|
|
||||||
|
// create groupIds array and entities object for bulk ops
|
||||||
|
const groupIds = groups.map((g) => g.id);
|
||||||
|
const orderedGroups: Record<string, string[]> = {};
|
||||||
|
groupIds.forEach((gID) => {
|
||||||
|
orderedGroups[gID] = [];
|
||||||
|
});
|
||||||
|
let entities: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
if (is_list) {
|
||||||
|
entities = Object.assign(orderedGroups, { [groupIds[0]]: issueIds });
|
||||||
|
} else {
|
||||||
|
entities = Object.assign(orderedGroups, { ...issueIds });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative size-full flex flex-col">
|
||||||
ref={containerRef}
|
{groups && (
|
||||||
className="vertical-scrollbar scrollbar-lg relative h-full w-full overflow-auto vertical-scrollbar-margin-top-md"
|
<MultipleSelectGroup containerRef={containerRef} entities={entities}>
|
||||||
>
|
{(helpers) => (
|
||||||
{groups &&
|
<>
|
||||||
groups.length > 0 &&
|
<div
|
||||||
groups.map(
|
ref={containerRef}
|
||||||
(group: IGroupByColumn) =>
|
className="size-full overflow-auto vertical-scrollbar scrollbar-lg vertical-scrollbar-margin-top-md"
|
||||||
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && (
|
>
|
||||||
<ListGroup
|
{groups.map(
|
||||||
key={group.id}
|
(group: IGroupByColumn) =>
|
||||||
group={group}
|
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[group.id]) && (
|
||||||
getGroupIndex={getGroupIndex}
|
<ListGroup
|
||||||
issueIds={issueIds}
|
key={group.id}
|
||||||
issuesMap={issuesMap}
|
group={group}
|
||||||
group_by={group_by}
|
getGroupIndex={getGroupIndex}
|
||||||
orderBy={orderBy}
|
issueIds={issueIds}
|
||||||
updateIssue={updateIssue}
|
issuesMap={issuesMap}
|
||||||
quickActions={quickActions}
|
group_by={group_by}
|
||||||
displayProperties={displayProperties}
|
orderBy={orderBy}
|
||||||
enableIssueQuickAdd={enableIssueQuickAdd}
|
updateIssue={updateIssue}
|
||||||
canEditProperties={canEditProperties}
|
quickActions={quickActions}
|
||||||
storeType={storeType}
|
displayProperties={displayProperties}
|
||||||
containerRef={containerRef}
|
enableIssueQuickAdd={enableIssueQuickAdd}
|
||||||
quickAddCallback={quickAddCallback}
|
canEditProperties={canEditProperties}
|
||||||
disableIssueCreation={disableIssueCreation}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
containerRef={containerRef}
|
||||||
handleOnDrop={handleOnDrop}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
disableIssueCreation={disableIssueCreation}
|
||||||
isCompletedCycle={isCompletedCycle}
|
addIssuesToView={addIssuesToView}
|
||||||
/>
|
handleOnDrop={handleOnDrop}
|
||||||
)
|
viewId={viewId}
|
||||||
)}
|
isCompletedCycle={isCompletedCycle}
|
||||||
|
selectionHelpers={helpers}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MultipleSelectGroup>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
GroupByList.displayName = "GroupByList";
|
||||||
|
|
||||||
export interface IList {
|
export interface IList {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
|
@ -1,138 +1,169 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// lucide icons
|
|
||||||
import { CircleDashed, Plus } from "lucide-react";
|
import { CircleDashed, Plus } from "lucide-react";
|
||||||
import { TIssue, ISearchIssueResponse } from "@plane/types";
|
|
||||||
// components
|
|
||||||
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
import { ExistingIssuesListModal } from "@/components/core";
|
|
||||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
|
||||||
// ui
|
|
||||||
// mobx
|
|
||||||
// hooks
|
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
|
||||||
import { useEventTracker } from "@/hooks/store";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue, ISearchIssueResponse } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { ExistingIssuesListModal, MultipleSelectGroupAction } from "@/components/core";
|
||||||
|
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||||
|
// constants
|
||||||
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
|
|
||||||
interface IHeaderGroupByCard {
|
interface IHeaderGroupByCard {
|
||||||
|
groupID: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
count: number;
|
count: number;
|
||||||
issuePayload: Partial<TIssue>;
|
issuePayload: Partial<TIssue>;
|
||||||
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
storeType: EIssuesStoreType;
|
storeType: EIssuesStoreType;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderGroupByCard = observer(
|
export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||||
({ icon, title, count, issuePayload, disableIssueCreation, storeType, addIssuesToView }: IHeaderGroupByCard) => {
|
const {
|
||||||
const router = useRouter();
|
groupID,
|
||||||
const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
|
icon,
|
||||||
// hooks
|
title,
|
||||||
const { setTrackElement } = useEventTracker();
|
count,
|
||||||
|
issuePayload,
|
||||||
|
canEditProperties,
|
||||||
|
disableIssueCreation,
|
||||||
|
storeType,
|
||||||
|
addIssuesToView,
|
||||||
|
selectionHelpers,
|
||||||
|
} = props;
|
||||||
|
// states
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false);
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
|
||||||
|
// hooks
|
||||||
|
const { setTrackElement } = useEventTracker();
|
||||||
|
// derived values
|
||||||
|
const isDraftIssue = router.pathname.includes("draft-issue");
|
||||||
|
const renderExistingIssueModal = moduleId || cycleId;
|
||||||
|
const existingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
||||||
|
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(groupID) === "empty";
|
||||||
|
// auth
|
||||||
|
const canSelectIssues = canEditProperties(projectId?.toString());
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false);
|
const issues = data.map((i) => i.id);
|
||||||
|
|
||||||
const isDraftIssue = router.pathname.includes("draft-issue");
|
try {
|
||||||
|
await addIssuesToView?.(issues);
|
||||||
|
|
||||||
const renderExistingIssueModal = moduleId || cycleId;
|
setToast({
|
||||||
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issues added to the cycle successfully.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "Error!",
|
||||||
|
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddIssuesToView = async (data: ISearchIssueResponse[]) => {
|
return (
|
||||||
if (!workspaceSlug || !projectId) return;
|
<>
|
||||||
|
<div className="group/list-header relative w-full flex-shrink-0 flex items-center gap-2.5 py-1.5 pl-3.5">
|
||||||
const issues = data.map((i) => i.id);
|
{canSelectIssues && (
|
||||||
|
<div className="flex-shrink-0 flex items-center w-3.5">
|
||||||
try {
|
<MultipleSelectGroupAction
|
||||||
await addIssuesToView?.(issues);
|
className={cn(
|
||||||
|
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
|
||||||
setToast({
|
{
|
||||||
type: TOAST_TYPE.SUCCESS,
|
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
|
||||||
title: "Success!",
|
|
||||||
message: "Issues added to the cycle successfully.",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="relative flex w-full flex-shrink-0 flex-row items-center gap-1.5 py-1.5">
|
|
||||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm">
|
|
||||||
{icon ? icon : <CircleDashed className="h-3.5 w-3.5" strokeWidth={2} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
|
|
||||||
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
|
||||||
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!disableIssueCreation &&
|
|
||||||
(renderExistingIssueModal ? (
|
|
||||||
<CustomMenu
|
|
||||||
customButton={
|
|
||||||
<span className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80">
|
|
||||||
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
>
|
)}
|
||||||
<CustomMenu.MenuItem
|
groupID={groupID}
|
||||||
onClick={() => {
|
selectionHelpers={selectionHelpers}
|
||||||
setTrackElement("List layout");
|
/>
|
||||||
setIsOpen(true);
|
</div>
|
||||||
}}
|
)}
|
||||||
>
|
<div className="flex-shrink-0 grid place-items-center overflow-hidden pl-3">
|
||||||
<span className="flex items-center justify-start gap-2">Create issue</span>
|
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||||
</CustomMenu.MenuItem>
|
</div>
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
|
||||||
setTrackElement("List layout");
|
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
||||||
setOpenExistingIssueListModal(true);
|
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
|
{!disableIssueCreation &&
|
||||||
</CustomMenu.MenuItem>
|
(renderExistingIssueModal ? (
|
||||||
</CustomMenu>
|
<CustomMenu
|
||||||
) : (
|
customButton={
|
||||||
<div
|
<span className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80">
|
||||||
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTrackElement("List layout");
|
setTrackElement("List layout");
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus width={14} strokeWidth={2} />
|
<span className="flex items-center justify-start gap-2">Create issue</span>
|
||||||
</div>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("List layout");
|
||||||
|
setOpenExistingIssueListModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-start gap-2">Add an existing issue</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
||||||
|
onClick={() => {
|
||||||
|
setTrackElement("List layout");
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus width={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={() => setIsOpen(false)}
|
onClose={() => setIsOpen(false)}
|
||||||
data={issuePayload}
|
data={issuePayload}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
isDraft={isDraftIssue}
|
isDraft={isDraftIssue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{renderExistingIssueModal && (
|
||||||
|
<ExistingIssuesListModal
|
||||||
|
workspaceSlug={workspaceSlug?.toString()}
|
||||||
|
projectId={projectId?.toString()}
|
||||||
|
isOpen={openExistingIssueListModal}
|
||||||
|
handleClose={() => setOpenExistingIssueListModal(false)}
|
||||||
|
searchParams={existingIssuesListModalPayload}
|
||||||
|
handleOnSubmit={handleAddIssuesToView}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{renderExistingIssueModal && (
|
</div>
|
||||||
<ExistingIssuesListModal
|
</>
|
||||||
workspaceSlug={workspaceSlug?.toString()}
|
);
|
||||||
projectId={projectId?.toString()}
|
});
|
||||||
isOpen={openExistingIssueListModal}
|
|
||||||
handleClose={() => setOpenExistingIssueListModal(false)}
|
|
||||||
searchParams={ExistingIssuesListModalPayload}
|
|
||||||
handleOnSubmit={handleAddIssuesToView}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
@ -19,6 +19,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
|||||||
import { DRAG_ALLOWED_GROUPS, EIssuesStoreType } from "@/constants/issue";
|
import { DRAG_ALLOWED_GROUPS, EIssuesStoreType } from "@/constants/issue";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectState } from "@/hooks/store";
|
import { useProjectState } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
// components
|
// components
|
||||||
import { GroupDragOverlay } from "../group-drag-overlay";
|
import { GroupDragOverlay } from "../group-drag-overlay";
|
||||||
import {
|
import {
|
||||||
@ -58,6 +59,7 @@ type Props = {
|
|||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
isCompletedCycle?: boolean;
|
isCompletedCycle?: boolean;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ListGroup = observer((props: Props) => {
|
export const ListGroup = observer((props: Props) => {
|
||||||
@ -81,6 +83,7 @@ export const ListGroup = observer((props: Props) => {
|
|||||||
enableIssueQuickAdd,
|
enableIssueQuickAdd,
|
||||||
isCompletedCycle,
|
isCompletedCycle,
|
||||||
storeType,
|
storeType,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
|
const [isDraggingOverColumn, setIsDraggingOverColumn] = useState(false);
|
||||||
@ -190,15 +193,18 @@ export const ListGroup = observer((props: Props) => {
|
|||||||
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled,
|
"border-custom-error-200": isDraggingOverColumn && !!group.isDropDisabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="sticky top-0 z-[3] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 pl-5 py-1">
|
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 px-3 py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
|
groupID={group.id}
|
||||||
icon={group.icon}
|
icon={group.icon}
|
||||||
title={group.name || ""}
|
title={group.name || ""}
|
||||||
count={issueCount}
|
count={issueCount}
|
||||||
issuePayload={group.payload}
|
issuePayload={group.payload}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
|
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy || isCompletedCycle}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -224,6 +230,7 @@ export const ListGroup = observer((props: Props) => {
|
|||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
isDragAllowed={isDragAllowed}
|
isDragAllowed={isDragAllowed}
|
||||||
canDropOverIssue={!canOverlayBeVisible}
|
canDropOverIssue={!canOverlayBeVisible}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { MemberDropdown } from "@/components/dropdowns";
|
import { MemberDropdown } from "@/components/dropdowns";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -36,7 +36,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props
|
|||||||
buttonVariant={
|
buttonVariant={
|
||||||
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"
|
issue?.assignee_ids && issue.assignee_ids.length > 0 ? "transparent-without-text" : "transparent-with-text"
|
||||||
}
|
}
|
||||||
buttonClassName="text-left"
|
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -11,7 +11,7 @@ export const SpreadsheetAttachmentColumn: React.FC<Props> = observer((props) =>
|
|||||||
const { issue } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80">
|
<div className="flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10">
|
||||||
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
|
{issue?.attachment_count} {issue?.attachment_count === 1 ? "attachment" : "attachments"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -11,8 +11,9 @@ type Props = {
|
|||||||
|
|
||||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetCreatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80">
|
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10">
|
||||||
{renderFormattedDate(issue.created_at)}
|
{renderFormattedDate(issue.created_at)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// hooks
|
|
||||||
import { CycleDropdown } from "@/components/dropdowns";
|
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
|
||||||
import { useEventTracker, useIssues } from "@/hooks/store";
|
|
||||||
// components
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { CycleDropdown } from "@/components/dropdowns";
|
||||||
// constants
|
// constants
|
||||||
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker, useIssues } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -17,11 +17,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
|
export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
|
||||||
|
const { issue, disabled, onClose } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// props
|
|
||||||
const { issue, disabled, onClose } = props;
|
|
||||||
// hooks
|
// hooks
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
@ -56,8 +55,8 @@ export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder="Select cycle"
|
placeholder="Select cycle"
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonClassName="relative leading-4 h-4.5 bg-transparent"
|
buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CalendarCheck2 } from "lucide-react";
|
import { CalendarCheck2 } from "lucide-react";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// hooks
|
|
||||||
// components
|
// components
|
||||||
import { DateDropdown } from "@/components/dropdowns";
|
import { DateDropdown } from "@/components/dropdowns";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||||
|
// hooks
|
||||||
import { useProjectState } from "@/hooks/store";
|
import { useProjectState } from "@/hooks/store";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -47,9 +47,12 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
|
|||||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
buttonClassName={cn("rounded-none text-left", {
|
buttonClassName={cn(
|
||||||
"text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
|
"rounded-none text-left group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10",
|
||||||
})}
|
{
|
||||||
|
"text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
|
||||||
|
}
|
||||||
|
)}
|
||||||
clearIconClassName="!text-custom-text-100"
|
clearIconClassName="!text-custom-text-100"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// components
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
import { EstimateDropdown } from "@/components/dropdowns";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { EstimateDropdown } from "@/components/dropdowns";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -25,7 +25,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props
|
|||||||
projectId={issue.project_id}
|
projectId={issue.project_id}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// components
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel } from "@/hooks/store";
|
import { useLabel } from "@/hooks/store";
|
||||||
// types
|
// components
|
||||||
import { IssuePropertyLabels } from "../../properties";
|
import { IssuePropertyLabels } from "../../properties";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -27,8 +27,8 @@ export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) =
|
|||||||
value={issue.label_ids}
|
value={issue.label_ids}
|
||||||
defaultOptions={defaultLabelOptions}
|
defaultOptions={defaultLabelOptions}
|
||||||
onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })}
|
onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })}
|
||||||
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
|
className="h-11 w-full border-b-[0.5px] border-custom-border-200"
|
||||||
buttonClassName="px-2.5 h-full"
|
buttonClassName="px-2.5 h-full group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
maxRender={1}
|
maxRender={1}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -11,7 +11,7 @@ export const SpreadsheetLinkColumn: React.FC<Props> = observer((props: Props) =>
|
|||||||
const { issue } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80">
|
<div className="flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10">
|
||||||
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
|
{issue?.link_count} {issue?.link_count === 1 ? "link" : "links"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,14 +2,14 @@ import React, { useCallback } from "react";
|
|||||||
import xor from "lodash/xor";
|
import xor from "lodash/xor";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// hooks
|
|
||||||
import { ModuleDropdown } from "@/components/dropdowns";
|
|
||||||
import { EIssuesStoreType } from "@/constants/issue";
|
|
||||||
import { useEventTracker, useIssues } from "@/hooks/store";
|
|
||||||
// components
|
|
||||||
// types
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { ModuleDropdown } from "@/components/dropdowns";
|
||||||
// constants
|
// constants
|
||||||
|
import { EIssuesStoreType } from "@/constants/issue";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker, useIssues } from "@/hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -18,11 +18,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
|
export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
|
||||||
|
const { issue, disabled, onClose } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
// props
|
|
||||||
const { issue, disabled, onClose } = props;
|
|
||||||
// hooks
|
// hooks
|
||||||
const { captureIssueEvent } = useEventTracker();
|
const { captureIssueEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
@ -65,8 +64,8 @@ export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder="Select modules"
|
placeholder="Select modules"
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
buttonContainerClassName="w-full relative flex items-center p-2 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonClassName="relative leading-4 h-4.5 bg-transparent"
|
buttonClassName="relative leading-4 h-4.5 bg-transparent hover:bg-transparent"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
multiple
|
multiple
|
||||||
showCount
|
showCount
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { PriorityDropdown } from "@/components/dropdowns";
|
import { PriorityDropdown } from "@/components/dropdowns";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -22,7 +22,7 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props
|
|||||||
onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })}
|
onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CalendarClock } from "lucide-react";
|
import { CalendarClock } from "lucide-react";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { DateDropdown } from "@/components/dropdowns";
|
import { DateDropdown } from "@/components/dropdowns";
|
||||||
// helpers
|
// helpers
|
||||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -38,7 +38,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
|
|||||||
placeholder="Start date"
|
placeholder="Start date"
|
||||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { StateDropdown } from "@/components/dropdowns";
|
import { StateDropdown } from "@/components/dropdowns";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -23,7 +23,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })}
|
onChange={(data) => onChange(issue, { state_id: data }, { changed_property: "state", change_details: data })}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="text-left rounded-none group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
|
@ -34,7 +34,7 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props
|
|||||||
<div
|
<div
|
||||||
onClick={subIssueCount ? redirectToIssueDetail : () => {}}
|
onClick={subIssueCount ? redirectToIssueDetail : () => {}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80",
|
"flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 px-2.5 py-1 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10",
|
||||||
{
|
{
|
||||||
"cursor-pointer": subIssueCount,
|
"cursor-pointer": subIssueCount,
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
@ -11,8 +11,9 @@ type Props = {
|
|||||||
|
|
||||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue } = props;
|
const { issue } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80">
|
<div className="flex h-11 w-full items-center justify-center border-b-[0.5px] border-custom-border-200 text-xs hover:bg-custom-background-80 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10">
|
||||||
{renderFormattedDate(issue.updated_at)}
|
{renderFormattedDate(issue.updated_at)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
|
||||||
// types
|
// types
|
||||||
import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet";
|
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
import { useEventTracker } from "@/hooks/store";
|
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
|
||||||
// constants
|
// constants
|
||||||
|
import { SPREADSHEET_PROPERTY_DETAILS } from "@/constants/spreadsheet";
|
||||||
|
// hooks
|
||||||
|
import { useEventTracker } from "@/hooks/store";
|
||||||
// components
|
// components
|
||||||
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
@ -37,7 +38,7 @@ export const IssueColumn = observer((props: Props) => {
|
|||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100"
|
className="h-11 w-full min-w-[8rem] text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100"
|
||||||
ref={tableCellRef}
|
ref={tableCellRef}
|
||||||
>
|
>
|
||||||
<Column
|
<Column
|
||||||
@ -58,9 +59,7 @@ export const IssueColumn = observer((props: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
onClose={() => {
|
onClose={() => tableCellRef?.current?.focus()}
|
||||||
tableCellRef?.current?.focus();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
import { Dispatch, MouseEvent, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// icons
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
// types
|
||||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, Tooltip } from "@plane/ui";
|
import { ControlLink, Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectEntityAction } from "@/components/core";
|
||||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
// types
|
|
||||||
// local components
|
// local components
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
@ -34,6 +37,7 @@ interface Props {
|
|||||||
issueIds: string[];
|
issueIds: string[];
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
spacingLeft?: number;
|
spacingLeft?: number;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetIssueRow = observer((props: Props) => {
|
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||||
@ -51,12 +55,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
issueIds,
|
issueIds,
|
||||||
spreadsheetColumnsList,
|
spreadsheetColumnsList,
|
||||||
spacingLeft = 6,
|
spacingLeft = 6,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
|
// states
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
|
// store hooks
|
||||||
const { subIssues: subIssuesStore } = useIssueDetail();
|
const { subIssues: subIssuesStore } = useIssueDetail();
|
||||||
|
// derived values
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
||||||
|
const isIssueSelected = selectionHelpers.getIsEntitySelected(issueId);
|
||||||
|
const isIssueActive = selectionHelpers.getIsEntityActive(issueId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -65,7 +73,13 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
as="tr"
|
as="tr"
|
||||||
defaultHeight="calc(2.75rem - 1px)"
|
defaultHeight="calc(2.75rem - 1px)"
|
||||||
root={containerRef}
|
root={containerRef}
|
||||||
placeholderChildren={<td colSpan={100} className="border-b-[0.5px] border-custom-border-200" />}
|
placeholderChildren={
|
||||||
|
<td colSpan={100} className="border-[0.5px] border-transparent border-b-custom-border-200" />
|
||||||
|
}
|
||||||
|
classNames={cn("bg-custom-background-100 transition-[background-color]", {
|
||||||
|
"group selected-issue-row": isIssueSelected,
|
||||||
|
"border-[0.5px] border-custom-border-400": isIssueActive,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<IssueRowDetails
|
<IssueRowDetails
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
@ -81,13 +95,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
setExpanded={setExpanded}
|
setExpanded={setExpanded}
|
||||||
spreadsheetColumnsList={spreadsheetColumnsList}
|
spreadsheetColumnsList={spreadsheetColumnsList}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
</RenderIfVisible>
|
</RenderIfVisible>
|
||||||
|
|
||||||
{isExpanded &&
|
{isExpanded &&
|
||||||
subIssues &&
|
subIssues?.map((subIssueId: string) => (
|
||||||
subIssues.length > 0 &&
|
|
||||||
subIssues.map((subIssueId: string) => (
|
|
||||||
<SpreadsheetIssueRow
|
<SpreadsheetIssueRow
|
||||||
key={subIssueId}
|
key={subIssueId}
|
||||||
issueId={subIssueId}
|
issueId={subIssueId}
|
||||||
@ -103,6 +116,7 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
spreadsheetColumnsList={spreadsheetColumnsList}
|
spreadsheetColumnsList={spreadsheetColumnsList}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@ -123,6 +137,7 @@ interface IssueRowDetailsProps {
|
|||||||
setExpanded: Dispatch<SetStateAction<boolean>>;
|
setExpanded: Dispatch<SetStateAction<boolean>>;
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
spacingLeft?: number;
|
spacingLeft?: number;
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||||
@ -140,6 +155,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
setExpanded,
|
setExpanded,
|
||||||
spreadsheetColumnsList,
|
spreadsheetColumnsList,
|
||||||
spacingLeft = 6,
|
spacingLeft = 6,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
@ -148,7 +164,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
const { getIsIssuePeeked, peekIssue, setPeekIssue } = useIssueDetail();
|
const { getIsIssuePeeked, peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
@ -171,7 +187,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
const issueDetail = issue.getIssueById(issueId);
|
||||||
|
|
||||||
const paddingLeft = `${spacingLeft}px`;
|
const marginLeft = `${spacingLeft}px`;
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
@ -204,16 +220,22 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
|
|
||||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||||
const subIssuesCount = issueDetail?.sub_issues_count ?? 0;
|
const subIssuesCount = issueDetail?.sub_issues_count ?? 0;
|
||||||
|
const isIssueSelected = selectionHelpers.getIsEntitySelected(issueDetail.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td id={`issue-${issueId}`} ref={cellRef} tabIndex={0} className="sticky left-0 z-10">
|
<td
|
||||||
|
id={`issue-${issueId}`}
|
||||||
|
ref={cellRef}
|
||||||
|
tabIndex={0}
|
||||||
|
className="sticky left-0 z-10 group/list-block bg-custom-background-100"
|
||||||
|
>
|
||||||
<ControlLink
|
<ControlLink
|
||||||
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issueDetail)}
|
onClick={() => handleIssuePeekOverview(issueDetail)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group clickable cursor-pointer h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
|
"group clickable cursor-pointer h-11 w-[28rem] flex items-center text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200 bg-transparent group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-100/10",
|
||||||
{
|
{
|
||||||
"border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
|
"border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70":
|
"border border-custom-primary-70 hover:border-custom-primary-70":
|
||||||
@ -223,23 +245,51 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
|||||||
)}
|
)}
|
||||||
disabled={!!issueDetail?.tempId}
|
disabled={!!issueDetail?.tempId}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-1 min-w-min py-2.5 pl-2">
|
||||||
className="flex min-w-min items-center gap-0.5 px-4 py-2.5 pl-1.5 pr-0"
|
{/* select checkbox */}
|
||||||
style={nestingLevel !== 0 ? { paddingLeft } : {}}
|
{projectId && !disableUserActions && (
|
||||||
>
|
<Tooltip
|
||||||
<div className="flex items-center">
|
tooltipContent={
|
||||||
{/* bulk ops */}
|
<>
|
||||||
<span className="size-3.5" />
|
Only issues within the current
|
||||||
<div className="flex size-4 items-center justify-center">
|
<br />
|
||||||
{subIssuesCount > 0 && (
|
project can be selected.
|
||||||
<button
|
</>
|
||||||
className="flex items-center justify-center size-4 cursor-pointer rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
}
|
||||||
onClick={handleToggleExpand}
|
disabled={issueDetail.project_id === projectId}
|
||||||
>
|
>
|
||||||
<ChevronRight className={`size-4 ${isExpanded ? "rotate-90" : ""}`} />
|
<div className="flex-shrink-0 grid place-items-center w-3.5">
|
||||||
</button>
|
<MultipleSelectEntityAction
|
||||||
)}
|
className={cn(
|
||||||
</div>
|
"opacity-0 pointer-events-none group-hover/list-block:opacity-100 group-hover/list-block:pointer-events-auto transition-opacity",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": isIssueSelected,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
groupId={SPREADSHEET_SELECT_GROUP}
|
||||||
|
id={issueDetail.id}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
disabled={issueDetail.project_id !== projectId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{/* sub-issues chevron */}
|
||||||
|
<div className="grid place-items-center size-4" style={nestingLevel !== 0 ? { marginLeft } : {}}>
|
||||||
|
{subIssuesCount > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center size-4 rounded-sm text-custom-text-400 hover:text-custom-text-300"
|
||||||
|
onClick={handleToggleExpand}
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn("size-4", {
|
||||||
|
"rotate-90": isExpanded,
|
||||||
|
})}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
|
@ -1,33 +1,68 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// ui
|
// ui
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||||
// types
|
|
||||||
import { LayersIcon } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
|
import { MultipleSelectGroupAction } from "@/components/core";
|
||||||
import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts";
|
import { SpreadsheetHeaderColumn } from "@/components/issues/issue-layouts";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
// hooks
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
isEstimateEnabled: boolean;
|
isEstimateEnabled: boolean;
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetHeader = (props: Props) => {
|
export const SpreadsheetHeader = observer((props: Props) => {
|
||||||
const { displayProperties, displayFilters, handleDisplayFilterUpdate, isEstimateEnabled, spreadsheetColumnsList } =
|
const {
|
||||||
props;
|
displayProperties,
|
||||||
|
displayFilters,
|
||||||
|
handleDisplayFilterUpdate,
|
||||||
|
canEditProperties,
|
||||||
|
isEstimateEnabled,
|
||||||
|
spreadsheetColumnsList,
|
||||||
|
selectionHelpers,
|
||||||
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { projectId } = router.query;
|
||||||
|
// derived values
|
||||||
|
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(SPREADSHEET_SELECT_GROUP) === "empty";
|
||||||
|
// auth
|
||||||
|
const canSelectIssues = canEditProperties(projectId?.toString());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100">
|
<thead className="sticky top-0 left-0 z-[12] border-b-[0.5px] border-custom-border-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
className="sticky left-0 z-[15] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
|
className="group/list-header sticky left-0 z-[15] h-11 w-[28rem] flex items-center gap-1 bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100 pl-2"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<span className="flex h-full w-full flex-grow items-center pl-5 px-4 py-2.5">
|
{canSelectIssues && (
|
||||||
<LayersIcon className="mr-1 h-4 w-4 text-custom-text-400" />
|
<div className="flex-shrink-0 flex items-center w-3.5">
|
||||||
Issue
|
<MultipleSelectGroupAction
|
||||||
</span>
|
className={cn(
|
||||||
|
"size-3.5 opacity-0 pointer-events-none group-hover/list-header:opacity-100 group-hover/list-header:pointer-events-auto !outline-none",
|
||||||
|
{
|
||||||
|
"opacity-100 pointer-events-auto": !isGroupSelectionEmpty,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
groupID={SPREADSHEET_SELECT_GROUP}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="size-4" />
|
||||||
|
<span className="flex h-full w-full flex-grow items-center py-2.5">Issues</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
{spreadsheetColumnsList.map((property) => (
|
{spreadsheetColumnsList.map((property) => (
|
||||||
@ -43,4 +78,4 @@ export const SpreadsheetHeader = (props: Props) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { MutableRefObject, useCallback, useEffect, useRef } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
//types
|
//types
|
||||||
|
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||||
import { useTableKeyboardNavigation } from "@/hooks/use-table-keyboard-navigation";
|
import { useTableKeyboardNavigation } from "@/hooks/use-table-keyboard-navigation";
|
||||||
//components
|
//components
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
@ -20,6 +21,7 @@ type Props = {
|
|||||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
containerRef: MutableRefObject<HTMLTableElement | null>;
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
spreadsheetColumnsList: (keyof IIssueDisplayProperties)[];
|
||||||
|
selectionHelpers: TSelectionHelper;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetTable = observer((props: Props) => {
|
export const SpreadsheetTable = observer((props: Props) => {
|
||||||
@ -35,6 +37,7 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
canEditProperties,
|
canEditProperties,
|
||||||
containerRef,
|
containerRef,
|
||||||
spreadsheetColumnsList,
|
spreadsheetColumnsList,
|
||||||
|
selectionHelpers,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// states
|
// states
|
||||||
@ -81,8 +84,10 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
displayFilters={displayFilters}
|
displayFilters={displayFilters}
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
spreadsheetColumnsList={spreadsheetColumnsList}
|
spreadsheetColumnsList={spreadsheetColumnsList}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issueIds.map((id) => (
|
{issueIds.map((id) => (
|
||||||
@ -100,6 +105,7 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
isScrolled={isScrolled}
|
isScrolled={isScrolled}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
spreadsheetColumnsList={spreadsheetColumnsList}
|
spreadsheetColumnsList={spreadsheetColumnsList}
|
||||||
|
selectionHelpers={selectionHelpers}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
// types
|
||||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common";
|
import { LogoSpinner } from "@/components/common";
|
||||||
import { SpreadsheetQuickAddIssueForm } from "@/components/issues";
|
import { MultipleSelectGroup } from "@/components/core";
|
||||||
import { SPREADSHEET_PROPERTY_LIST } from "@/constants/spreadsheet";
|
import { IssueBulkOperationsRoot, SpreadsheetQuickAddIssueForm } from "@/components/issues";
|
||||||
|
// constants
|
||||||
|
import { SPREADSHEET_PROPERTY_LIST, SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet";
|
||||||
|
// hooks
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
|
// types
|
||||||
import { TRenderQuickActions } from "../list/list-view-types";
|
import { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { SpreadsheetTable } from "./spreadsheet-table";
|
import { SpreadsheetTable } from "./spreadsheet-table";
|
||||||
// types
|
|
||||||
//hooks
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
@ -73,28 +76,41 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
<div className="relative flex h-full w-full flex-col overflow-x-hidden whitespace-nowrap rounded-lg bg-custom-background-200 text-custom-text-200">
|
||||||
<div ref={portalRef} className="spreadsheet-menu-portal" />
|
<div ref={portalRef} className="spreadsheet-menu-portal" />
|
||||||
<div ref={containerRef} className="vertical-scrollbar horizontal-scrollbar scrollbar-lg h-full w-full">
|
<MultipleSelectGroup
|
||||||
<SpreadsheetTable
|
containerRef={containerRef}
|
||||||
displayProperties={displayProperties}
|
entities={{
|
||||||
displayFilters={displayFilters}
|
[SPREADSHEET_SELECT_GROUP]: issueIds,
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
}}
|
||||||
issueIds={issueIds}
|
>
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
{(helpers) => (
|
||||||
portalElement={portalRef}
|
<>
|
||||||
quickActions={quickActions}
|
<div ref={containerRef} className="vertical-scrollbar horizontal-scrollbar scrollbar-lg h-full w-full">
|
||||||
updateIssue={updateIssue}
|
<SpreadsheetTable
|
||||||
canEditProperties={canEditProperties}
|
displayProperties={displayProperties}
|
||||||
containerRef={containerRef}
|
displayFilters={displayFilters}
|
||||||
spreadsheetColumnsList={spreadsheetColumnsList}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
/>
|
issueIds={issueIds}
|
||||||
</div>
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
<div className="border-t border-custom-border-100">
|
portalElement={portalRef}
|
||||||
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
quickActions={quickActions}
|
||||||
{enableQuickCreateIssue && !disableIssueCreation && (
|
updateIssue={updateIssue}
|
||||||
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
canEditProperties={canEditProperties}
|
||||||
)}
|
containerRef={containerRef}
|
||||||
</div>
|
spreadsheetColumnsList={spreadsheetColumnsList}
|
||||||
</div>
|
selectionHelpers={helpers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-custom-border-100">
|
||||||
|
<div className="z-5 sticky bottom-0 left-0 mb-3">
|
||||||
|
{enableQuickCreateIssue && !disableIssueCreation && (
|
||||||
|
<SpreadsheetQuickAddIssueForm formKey="name" quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IssueBulkOperationsRoot selectionHelpers={helpers} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MultipleSelectGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,22 +5,30 @@ import { observer } from "mobx-react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
|
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
|
// types
|
||||||
import { IProject } from "@plane/types";
|
import { IProject } from "@plane/types";
|
||||||
// hooks
|
// ui
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
|
// components
|
||||||
import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project";
|
import { CreateProjectModal, ProjectSidebarListItem } from "@/components/project";
|
||||||
|
// constants
|
||||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { orderJoinedProjects } from "@/helpers/project.helper";
|
import { orderJoinedProjects } from "@/helpers/project.helper";
|
||||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||||
|
// hooks
|
||||||
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||||
// ui
|
|
||||||
// components
|
|
||||||
// helpers
|
|
||||||
// constants
|
|
||||||
|
|
||||||
export const ProjectSidebarList: FC = observer(() => {
|
export const ProjectSidebarList: FC = observer(() => {
|
||||||
|
// get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen
|
||||||
|
const isFavProjectsListOpenInLocalStorage = localStorage.getItem("isFavoriteProjectsListOpen");
|
||||||
|
const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen");
|
||||||
// states
|
// states
|
||||||
|
const [isFavoriteProjectsListOpen, setIsFavoriteProjectsListOpen] = useState(
|
||||||
|
isFavProjectsListOpenInLocalStorage === "true"
|
||||||
|
);
|
||||||
|
const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(isAllProjectsListOpenInLocalStorage === "true");
|
||||||
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
|
const [isFavoriteProjectCreate, setIsFavoriteProjectCreate] = useState(false);
|
||||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||||
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
|
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
|
||||||
@ -122,6 +130,16 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
);
|
);
|
||||||
}, [containerRef]);
|
}, [containerRef]);
|
||||||
|
|
||||||
|
const toggleListDisclosure = (isOpen: boolean, type: "all" | "favorite") => {
|
||||||
|
if (type === "all") {
|
||||||
|
setIsAllProjectsListOpen(isOpen);
|
||||||
|
localStorage.setItem("isAllProjectsListOpen", isOpen.toString());
|
||||||
|
} else {
|
||||||
|
setIsFavoriteProjectsListOpen(isOpen);
|
||||||
|
localStorage.setItem("isFavoriteProjectsListOpen", isOpen.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaceSlug && (
|
{workspaceSlug && (
|
||||||
@ -147,42 +165,48 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{favoriteProjects && favoriteProjects.length > 0 && (
|
{favoriteProjects && favoriteProjects.length > 0 && (
|
||||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
<Disclosure as="div" className="flex flex-col" defaultOpen={isFavoriteProjectCreate}>
|
||||||
{({ open }) => (
|
<>
|
||||||
<>
|
{!isCollapsed && (
|
||||||
{!isCollapsed && (
|
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
<Disclosure.Button
|
||||||
<Disclosure.Button
|
as="button"
|
||||||
as="button"
|
type="button"
|
||||||
type="button"
|
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
onClick={() => toggleListDisclosure(!isFavoriteProjectsListOpen, "favorite")}
|
||||||
>
|
>
|
||||||
Favorites
|
Favorites
|
||||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
{isFavoriteProjectsListOpen ? (
|
||||||
</Disclosure.Button>
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
{isAuthorizedUser && (
|
) : (
|
||||||
<button
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
className="opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={() => {
|
|
||||||
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
|
||||||
setIsFavoriteProjectCreate(true);
|
|
||||||
setIsProjectModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Disclosure.Button>
|
||||||
)}
|
{isAuthorizedUser && (
|
||||||
<Transition
|
<button
|
||||||
enter="transition duration-100 ease-out"
|
className="opacity-0 group-hover:opacity-100"
|
||||||
enterFrom="transform scale-95 opacity-0"
|
onClick={() => {
|
||||||
enterTo="transform scale-100 opacity-100"
|
setTrackElement("APP_SIDEBAR_FAVORITES_BLOCK");
|
||||||
leave="transition duration-75 ease-out"
|
setIsFavoriteProjectCreate(true);
|
||||||
leaveFrom="transform scale-100 opacity-100"
|
setIsProjectModalOpen(true);
|
||||||
leaveTo="transform scale-95 opacity-0"
|
}}
|
||||||
>
|
>
|
||||||
<Disclosure.Panel as="div" className="space-y-2">
|
<Plus className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Transition
|
||||||
|
show={isFavoriteProjectsListOpen}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform scale-95 opacity-0"
|
||||||
|
enterTo="transform scale-100 opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
|
leaveTo="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
{isFavoriteProjectsListOpen && (
|
||||||
|
<Disclosure.Panel as="div" className={`space-y-2`} static>
|
||||||
{favoriteProjects.map((projectId, index) => (
|
{favoriteProjects.map((projectId, index) => (
|
||||||
<ProjectSidebarListItem
|
<ProjectSidebarListItem
|
||||||
key={projectId}
|
key={projectId}
|
||||||
@ -195,50 +219,56 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
)}
|
||||||
</>
|
</Transition>
|
||||||
)}
|
</>
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{joinedProjects && joinedProjects.length > 0 && (
|
{joinedProjects && joinedProjects.length > 0 && (
|
||||||
<Disclosure as="div" className="flex flex-col" defaultOpen>
|
<Disclosure as="div" className="flex flex-col" defaultOpen={isAllProjectsListOpen}>
|
||||||
{({ open }) => (
|
<>
|
||||||
<>
|
{!isCollapsed && (
|
||||||
{!isCollapsed && (
|
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
||||||
<div className="group flex w-full items-center justify-between rounded p-1.5 text-xs text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80">
|
<Disclosure.Button
|
||||||
<Disclosure.Button
|
as="button"
|
||||||
as="button"
|
type="button"
|
||||||
type="button"
|
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
||||||
className="group flex w-full items-center gap-1 whitespace-nowrap rounded px-1.5 text-left text-sm font-semibold text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80"
|
onClick={() => toggleListDisclosure(!isAllProjectsListOpen, "all")}
|
||||||
>
|
>
|
||||||
Your projects
|
Your projects
|
||||||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
{isAllProjectsListOpen ? (
|
||||||
</Disclosure.Button>
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
{isAuthorizedUser && (
|
) : (
|
||||||
<button
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
className="opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={() => {
|
|
||||||
setTrackElement("Sidebar");
|
|
||||||
setIsFavoriteProjectCreate(false);
|
|
||||||
setIsProjectModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</Disclosure.Button>
|
||||||
)}
|
{isAuthorizedUser && (
|
||||||
<Transition
|
<button
|
||||||
enter="transition duration-100 ease-out"
|
className="opacity-0 group-hover:opacity-100"
|
||||||
enterFrom="transform scale-95 opacity-0"
|
onClick={() => {
|
||||||
enterTo="transform scale-100 opacity-100"
|
setTrackElement("Sidebar");
|
||||||
leave="transition duration-75 ease-out"
|
setIsFavoriteProjectCreate(false);
|
||||||
leaveFrom="transform scale-100 opacity-100"
|
setIsProjectModalOpen(true);
|
||||||
leaveTo="transform scale-95 opacity-0"
|
}}
|
||||||
>
|
>
|
||||||
<Disclosure.Panel as="div">
|
<Plus className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Transition
|
||||||
|
show={isAllProjectsListOpen}
|
||||||
|
enter="transition duration-100 ease-out"
|
||||||
|
enterFrom="transform scale-95 opacity-0"
|
||||||
|
enterTo="transform scale-100 opacity-100"
|
||||||
|
leave="transition duration-75 ease-out"
|
||||||
|
leaveFrom="transform scale-100 opacity-100"
|
||||||
|
leaveTo="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
{isAllProjectsListOpen && (
|
||||||
|
<Disclosure.Panel as="div" static>
|
||||||
{joinedProjects.map((projectId, index) => (
|
{joinedProjects.map((projectId, index) => (
|
||||||
<ProjectSidebarListItem
|
<ProjectSidebarListItem
|
||||||
key={projectId}
|
key={projectId}
|
||||||
@ -250,9 +280,9 @@ export const ProjectSidebarList: FC = observer(() => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
)}
|
||||||
</>
|
</Transition>
|
||||||
)}
|
</>
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -234,6 +234,7 @@ export const CreateUpdateStateInline: React.FC<Props> = observer((props) => {
|
|||||||
hasError={Boolean(errors.name)}
|
hasError={Boolean(errors.name)}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// headless ui
|
|
||||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
|
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
// icons
|
|
||||||
// ui
|
// ui
|
||||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAppTheme, useCommandPalette } from "@/hooks/store";
|
import { useAppTheme, useCommandPalette } from "@/hooks/store";
|
||||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||||
@ -59,9 +59,12 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-[6px] ${
|
className={cn(
|
||||||
isCollapsed ? "flex-col" : ""
|
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
||||||
}`}
|
{
|
||||||
|
"flex-col h-auto py-1.5": isCollapsed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
|
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
|
||||||
|
@ -3,3 +3,5 @@ export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|||||||
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
|
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
|
||||||
|
|
||||||
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
|
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
|
||||||
|
|
||||||
|
export const MARKETING_PLANE_ONE_PAGE_LINK = "https://plane.so/one";
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
// icons
|
// icons
|
||||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarCheck2, CalendarClock, Users } from "lucide-react";
|
import {
|
||||||
|
CalendarDays,
|
||||||
|
Link2,
|
||||||
|
Signal,
|
||||||
|
Tag,
|
||||||
|
Triangle,
|
||||||
|
Paperclip,
|
||||||
|
CalendarCheck2,
|
||||||
|
CalendarClock,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
@ -184,3 +194,5 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
|||||||
"attachment_count",
|
"attachment_count",
|
||||||
"sub_issue_count",
|
"sub_issue_count",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const SPREADSHEET_SELECT_GROUP = "spreadsheet-issues";
|
||||||
|
@ -9,12 +9,12 @@ export type UseIntersectionObserverProps = {
|
|||||||
|
|
||||||
export const useIntersectionObserver = (
|
export const useIntersectionObserver = (
|
||||||
containerRef: RefObject<HTMLDivElement>,
|
containerRef: RefObject<HTMLDivElement>,
|
||||||
elementRef: RefObject<HTMLDivElement>,
|
elementRef: HTMLDivElement | null,
|
||||||
callback: () => void,
|
callback: () => void,
|
||||||
rootMargin?: string
|
rootMargin?: string
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (elementRef.current) {
|
if (elementRef) {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[entries.length - 1].isIntersecting) {
|
if (entries[entries.length - 1].isIntersecting) {
|
||||||
@ -26,16 +26,16 @@ export const useIntersectionObserver = (
|
|||||||
rootMargin,
|
rootMargin,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
observer.observe(elementRef.current);
|
observer.observe(elementRef);
|
||||||
return () => {
|
return () => {
|
||||||
if (elementRef.current) {
|
if (elementRef) {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
observer.unobserve(elementRef.current);
|
observer.unobserve(elementRef);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// while removing the current from the refs, the observer is not not working as expected
|
// while removing the current from the refs, the observer is not not working as expected
|
||||||
// fix this eslint warning with caution
|
// fix this eslint warning with caution
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [rootMargin, callback, elementRef.current, containerRef.current]);
|
}, [rootMargin, callback, elementRef, containerRef.current]);
|
||||||
};
|
};
|
||||||
|
@ -33,6 +33,7 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
|
selectedEntityIds,
|
||||||
updateSelectedEntityDetails,
|
updateSelectedEntityDetails,
|
||||||
bulkUpdateSelectedEntityDetails,
|
bulkUpdateSelectedEntityDetails,
|
||||||
getActiveEntityDetails,
|
getActiveEntityDetails,
|
||||||
@ -45,6 +46,7 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
getIsEntitySelected,
|
getIsEntitySelected,
|
||||||
getIsEntityActive,
|
getIsEntityActive,
|
||||||
|
getEntityDetailsFromEntityID,
|
||||||
} = useMultipleSelectStore();
|
} = useMultipleSelectStore();
|
||||||
|
|
||||||
const groups = useMemo(() => Object.keys(entities), [entities]);
|
const groups = useMemo(() => Object.keys(entities), [entities]);
|
||||||
@ -248,10 +250,6 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
(groupID: string) => {
|
(groupID: string) => {
|
||||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
||||||
const groupSelectionStatus = isGroupSelected(groupID);
|
const groupSelectionStatus = isGroupSelected(groupID);
|
||||||
// groupEntities.map((entity) => {
|
|
||||||
// console.log("group click");
|
|
||||||
// handleEntitySelection(entity, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
|
||||||
// });
|
|
||||||
handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
||||||
},
|
},
|
||||||
[entitiesList, handleEntitySelection, isGroupSelected]
|
[entitiesList, handleEntitySelection, isGroupSelected]
|
||||||
@ -346,6 +344,19 @@ export const useMultipleSelect = (props: Props) => {
|
|||||||
};
|
};
|
||||||
}, [clearSelection, router.events]);
|
}, [clearSelection, router.events]);
|
||||||
|
|
||||||
|
// when entities list change, remove entityIds from the selected entities array, which are not present in the new list
|
||||||
|
useEffect(() => {
|
||||||
|
selectedEntityIds.map((entityID) => {
|
||||||
|
const isEntityPresent = entitiesList.find((en) => en.entityID === entityID);
|
||||||
|
if (!isEntityPresent) {
|
||||||
|
const entityDetails = getEntityDetailsFromEntityID(entityID);
|
||||||
|
if (entityDetails) {
|
||||||
|
handleEntitySelection(entityDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [entitiesList, getEntityDetailsFromEntityID, handleEntitySelection, selectedEntityIds]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description helper functions for selection
|
* @description helper functions for selection
|
||||||
*/
|
*/
|
||||||
|
@ -43,8 +43,6 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* TODO: Need to handle custom themes for toast */}
|
|
||||||
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
|
||||||
<InstanceWrapper>
|
<InstanceWrapper>
|
||||||
<StoreWrapper>
|
<StoreWrapper>
|
||||||
<CrispWrapper user={currentUser}>
|
<CrispWrapper user={currentUser}>
|
||||||
@ -56,6 +54,8 @@ export const AppProvider: FC<IAppProvider> = observer((props) => {
|
|||||||
posthogAPIKey={config?.posthog_api_key || undefined}
|
posthogAPIKey={config?.posthog_api_key || undefined}
|
||||||
posthogHost={config?.posthog_host || undefined}
|
posthogHost={config?.posthog_host || undefined}
|
||||||
>
|
>
|
||||||
|
{/* TODO: Need to handle custom themes for toast */}
|
||||||
|
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
|
||||||
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
<SWRConfig value={SWR_CONFIG}>{children}</SWRConfig>
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</CrispWrapper>
|
</CrispWrapper>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactElement, useEffect } from "react";
|
import { ReactElement } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// components
|
// components
|
||||||
@ -11,7 +11,7 @@ import { EmptyStateType } from "@/constants/empty-state";
|
|||||||
// helpers
|
// helpers
|
||||||
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useProjectInbox } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "@/layouts/app-layout";
|
import { AppLayout } from "@/layouts/app-layout";
|
||||||
// types
|
// types
|
||||||
@ -23,12 +23,6 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query;
|
const { workspaceSlug, projectId, currentTab: navigationTab, inboxIssueId } = router.query;
|
||||||
// hooks
|
// hooks
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { currentTab, handleCurrentTab } = useProjectInbox();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (navigationTab && currentTab != navigationTab)
|
|
||||||
handleCurrentTab(navigationTab === "open" ? EInboxIssueCurrentTab.OPEN : EInboxIssueCurrentTab.CLOSED);
|
|
||||||
}, [currentTab, navigationTab, handleCurrentTab]);
|
|
||||||
|
|
||||||
// No access to inbox
|
// No access to inbox
|
||||||
if (currentProjectDetails?.inbox_view === false)
|
if (currentProjectDetails?.inbox_view === false)
|
||||||
@ -44,6 +38,12 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
// derived values
|
// derived values
|
||||||
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
|
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Inbox` : "Plane - Inbox";
|
||||||
|
|
||||||
|
const currentNavigationTab = navigationTab
|
||||||
|
? navigationTab === "open"
|
||||||
|
? EInboxIssueCurrentTab.OPEN
|
||||||
|
: EInboxIssueCurrentTab.CLOSED
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!workspaceSlug || !projectId) return <></>;
|
if (!workspaceSlug || !projectId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,6 +55,7 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
|||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||||
inboxAccessible={currentProjectDetails?.inbox_view || false}
|
inboxAccessible={currentProjectDetails?.inbox_view || false}
|
||||||
|
navigationTab={currentNavigationTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -321,8 +321,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
|||||||
(this.inboxIssuePaginationInfo?.total_results &&
|
(this.inboxIssuePaginationInfo?.total_results &&
|
||||||
this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results))
|
this.inboxIssueIds.length < this.inboxIssuePaginationInfo?.total_results))
|
||||||
) {
|
) {
|
||||||
this.loader = "pagination-loading";
|
|
||||||
|
|
||||||
const queryParams = this.inboxIssueQueryParams(
|
const queryParams = this.inboxIssueQueryParams(
|
||||||
this.inboxFilters,
|
this.inboxFilters,
|
||||||
this.inboxSorting,
|
this.inboxSorting,
|
||||||
@ -332,7 +330,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
|||||||
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
|
const { results, ...paginationInfo } = await this.inboxIssueService.list(workspaceSlug, projectId, queryParams);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.loader = undefined;
|
|
||||||
set(this, "inboxIssuePaginationInfo", paginationInfo);
|
set(this, "inboxIssuePaginationInfo", paginationInfo);
|
||||||
if (results && results.length > 0) {
|
if (results && results.length > 0) {
|
||||||
const issueIds = results.map((value) => value?.issue?.id);
|
const issueIds = results.map((value) => value?.issue?.id);
|
||||||
@ -343,7 +340,6 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
|||||||
} else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
|
} else set(this, ["inboxIssuePaginationInfo", "next_page_results"], false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching the inbox issues", error);
|
console.error("Error fetching the inbox issues", error);
|
||||||
this.loader = undefined;
|
|
||||||
this.error = {
|
this.error = {
|
||||||
message: "Error fetching the paginated inbox issues please try again later.",
|
message: "Error fetching the paginated inbox issues please try again later.",
|
||||||
status: "pagination-error",
|
status: "pagination-error",
|
||||||
|
@ -19,6 +19,7 @@ export type IMultipleSelectStore = {
|
|||||||
getPreviousActiveEntity: () => TEntityDetails | null;
|
getPreviousActiveEntity: () => TEntityDetails | null;
|
||||||
getNextActiveEntity: () => TEntityDetails | null;
|
getNextActiveEntity: () => TEntityDetails | null;
|
||||||
getActiveEntityDetails: () => TEntityDetails | null;
|
getActiveEntityDetails: () => TEntityDetails | null;
|
||||||
|
getEntityDetailsFromEntityID: (entityID: string) => TEntityDetails | null;
|
||||||
// entity actions
|
// entity actions
|
||||||
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
|
||||||
bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
|
bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
|
||||||
@ -119,6 +120,16 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
|||||||
*/
|
*/
|
||||||
getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
|
getActiveEntityDetails = computedFn(() => this.activeEntityDetails);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get the entity details from entityID
|
||||||
|
* @param {string} entityID
|
||||||
|
* @returns {TEntityDetails | null}
|
||||||
|
*/
|
||||||
|
getEntityDetailsFromEntityID = computedFn(
|
||||||
|
(entityID: string): TEntityDetails | null =>
|
||||||
|
this.selectedEntityDetails.find((en) => en.entityID === entityID) ?? null
|
||||||
|
);
|
||||||
|
|
||||||
// entity actions
|
// entity actions
|
||||||
/**
|
/**
|
||||||
* @description add or remove entities
|
* @description add or remove entities
|
||||||
@ -159,8 +170,11 @@ export class MultipleSelectStore implements IMultipleSelectStore {
|
|||||||
if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
|
if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const newEntities = differenceWith(this.selectedEntityDetails, entitiesList, (obj1, obj2) =>
|
||||||
|
isEqual(obj1.entityID, obj2.entityID)
|
||||||
|
);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.selectedEntityDetails = differenceWith(this.selectedEntityDetails, entitiesList, isEqual);
|
this.selectedEntityDetails = newEntities;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user