Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cee833cd45 | |||
| 27c6d0e545 | |||
| 63bb868e23 | |||
| edeeb70efb | |||
| 7005ae2b53 | |||
| 21d7a1865c | |||
| f65b9a4dcb | |||
| 6d216f2607 | |||
| 4958be7898 | |||
| a40e44c6d5 | |||
| 44af90dc6c | |||
| f01d82ad1e | |||
| ac6fef3073 | |||
| c64c15948b | |||
| e58b68b6fc | |||
| 68325866ef | |||
| 80198f5fda | |||
| 6ac28ad614 | |||
| c021ffddf2 |
+3
-1
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.25.0",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.25.0"
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class IssueSerializer(BaseSerializer):
|
||||
data["assignees"] = ProjectMember.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
is_active=True,
|
||||
role__gte=15,
|
||||
member_id__in=data["assignees"],
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
@@ -158,8 +159,13 @@ class IssueSerializer(BaseSerializer):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
# Then assign it to default assignee
|
||||
if default_assignee_id is not None:
|
||||
# Then assign it to default assignee, if it is a valid assignee
|
||||
if default_assignee_id is not None and ProjectMember.objects.filter(
|
||||
member_id=default_assignee_id,
|
||||
project_id=project_id,
|
||||
role__gte=15,
|
||||
is_active=True
|
||||
).exists():
|
||||
IssueAssignee.objects.create(
|
||||
assignee_id=default_assignee_id,
|
||||
issue=issue,
|
||||
|
||||
@@ -36,6 +36,7 @@ from plane.db.models import (
|
||||
State,
|
||||
IssueVersion,
|
||||
IssueDescriptionVersion,
|
||||
ProjectMember,
|
||||
)
|
||||
|
||||
|
||||
@@ -119,6 +120,17 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
def get_valid_assignees(self, assignees, project_id):
|
||||
if not assignees:
|
||||
return []
|
||||
|
||||
return ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
role__gte=15,
|
||||
is_active=True,
|
||||
member_id__in=assignees
|
||||
).values_list('member_id', flat=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
assignees = validated_data.pop("assignee_ids", None)
|
||||
labels = validated_data.pop("label_ids", None)
|
||||
@@ -134,27 +146,33 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = issue.created_by_id
|
||||
updated_by_id = issue.updated_by_id
|
||||
|
||||
if assignees is not None and len(assignees):
|
||||
valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
|
||||
if valid_assignee_ids is not None and len(valid_assignee_ids):
|
||||
try:
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
assignee=user,
|
||||
assignee_id=user_id,
|
||||
issue=issue,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for user in assignees
|
||||
for user_id in valid_assignee_ids
|
||||
],
|
||||
batch_size=10,
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
else:
|
||||
# Then assign it to default assignee
|
||||
if default_assignee_id is not None:
|
||||
# Then assign it to default assignee, if it is a valid assignee
|
||||
if default_assignee_id is not None and ProjectMember.objects.filter(
|
||||
member_id=default_assignee_id,
|
||||
project_id=project_id,
|
||||
role__gte=15,
|
||||
is_active=True
|
||||
).exists():
|
||||
try:
|
||||
IssueAssignee.objects.create(
|
||||
assignee_id=default_assignee_id,
|
||||
@@ -198,20 +216,21 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
created_by_id = instance.created_by_id
|
||||
updated_by_id = instance.updated_by_id
|
||||
|
||||
if assignees is not None:
|
||||
valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
|
||||
if valid_assignee_ids is not None:
|
||||
IssueAssignee.objects.filter(issue=instance).delete()
|
||||
try:
|
||||
IssueAssignee.objects.bulk_create(
|
||||
[
|
||||
IssueAssignee(
|
||||
assignee=user,
|
||||
assignee_id=user_id,
|
||||
issue=instance,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
created_by_id=created_by_id,
|
||||
updated_by_id=updated_by_id,
|
||||
)
|
||||
for user in assignees
|
||||
for user_id in valid_assignee_ids
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
|
||||
+3
-3
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "0.25.0",
|
||||
"description": "",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -14,7 +15,6 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hocuspocus/extension-database": "^2.15.0",
|
||||
"@hocuspocus/extension-logger": "^2.15.0",
|
||||
|
||||
+4
-3
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
@@ -28,6 +30,5 @@
|
||||
"nanoid": "3.3.8",
|
||||
"esbuild": "0.25.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"name": "plane"
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts"
|
||||
"main": "./src/index.ts",
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
IIssueDisplayProperties,
|
||||
} from "@plane/types";
|
||||
import { TIssueGroupByOptions, TIssueOrderByOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
|
||||
export const ALL_ISSUES = "All Issues";
|
||||
|
||||
@@ -149,25 +145,24 @@ export const ISSUE_ORDER_BY_OPTIONS: {
|
||||
{ key: "-priority", titleTranslationKey: "common.priority" },
|
||||
];
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
|
||||
[
|
||||
"assignee",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"labels",
|
||||
"key",
|
||||
"priority",
|
||||
"state",
|
||||
"sub_issue_count",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"modules",
|
||||
"cycle",
|
||||
"issue_type",
|
||||
];
|
||||
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [
|
||||
"assignee",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"labels",
|
||||
"key",
|
||||
"priority",
|
||||
"state",
|
||||
"sub_issue_count",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"modules",
|
||||
"cycle",
|
||||
"issue_type",
|
||||
];
|
||||
|
||||
export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
key: keyof IIssueDisplayProperties;
|
||||
@@ -215,3 +210,144 @@ export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
{ key: "modules", titleTranslationKey: "common.module" },
|
||||
{ key: "cycle", titleTranslationKey: "common.cycle" },
|
||||
];
|
||||
|
||||
export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
||||
"state",
|
||||
"priority",
|
||||
"assignee",
|
||||
"labels",
|
||||
"modules",
|
||||
"cycle",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"sub_issue_count",
|
||||
];
|
||||
|
||||
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
[key in keyof IIssueDisplayProperties]: {
|
||||
i18n_title: string;
|
||||
ascendingOrderKey: TIssueOrderByOptions;
|
||||
ascendingOrderTitle: string;
|
||||
descendingOrderKey: TIssueOrderByOptions;
|
||||
descendingOrderTitle: string;
|
||||
icon: string;
|
||||
};
|
||||
} = {
|
||||
assignee: {
|
||||
i18n_title: "common.assignees",
|
||||
ascendingOrderKey: "assignees__first_name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-assignees__first_name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: "Users",
|
||||
},
|
||||
created_on: {
|
||||
i18n_title: "common.sort.created_on",
|
||||
ascendingOrderKey: "-created_at",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "created_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: "CalendarDays",
|
||||
},
|
||||
due_date: {
|
||||
i18n_title: "common.order_by.due_date",
|
||||
ascendingOrderKey: "-target_date",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "target_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: "CalendarCheck2",
|
||||
},
|
||||
estimate: {
|
||||
i18n_title: "common.estimate",
|
||||
ascendingOrderKey: "estimate_point__key",
|
||||
ascendingOrderTitle: "Low",
|
||||
descendingOrderKey: "-estimate_point__key",
|
||||
descendingOrderTitle: "High",
|
||||
icon: "Triangle",
|
||||
},
|
||||
labels: {
|
||||
i18n_title: "common.labels",
|
||||
ascendingOrderKey: "labels__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-labels__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: "Tag",
|
||||
},
|
||||
modules: {
|
||||
i18n_title: "common.modules",
|
||||
ascendingOrderKey: "issue_module__module__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-issue_module__module__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: "DiceIcon",
|
||||
},
|
||||
cycle: {
|
||||
i18n_title: "common.cycle",
|
||||
ascendingOrderKey: "issue_cycle__cycle__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-issue_cycle__cycle__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: "ContrastIcon",
|
||||
},
|
||||
priority: {
|
||||
i18n_title: "common.priority",
|
||||
ascendingOrderKey: "priority",
|
||||
ascendingOrderTitle: "None",
|
||||
descendingOrderKey: "-priority",
|
||||
descendingOrderTitle: "Urgent",
|
||||
icon: "Signal",
|
||||
},
|
||||
start_date: {
|
||||
i18n_title: "common.order_by.start_date",
|
||||
ascendingOrderKey: "-start_date",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "start_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: "CalendarClock",
|
||||
},
|
||||
state: {
|
||||
i18n_title: "common.state",
|
||||
ascendingOrderKey: "state__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-state__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: "DoubleCircleIcon",
|
||||
},
|
||||
updated_on: {
|
||||
i18n_title: "common.sort.updated_on",
|
||||
ascendingOrderKey: "-updated_at",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "updated_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: "CalendarDays",
|
||||
},
|
||||
link: {
|
||||
i18n_title: "common.link",
|
||||
ascendingOrderKey: "-link_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "link_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: "Link2",
|
||||
},
|
||||
attachment_count: {
|
||||
i18n_title: "common.attachment",
|
||||
ascendingOrderKey: "-attachment_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "attachment_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: "Paperclip",
|
||||
},
|
||||
sub_issue_count: {
|
||||
i18n_title: "issue.display.properties.sub_issue",
|
||||
ascendingOrderKey: "-sub_issues_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "sub_issues_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: "LayersIcon",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./common";
|
||||
export * from "./filter";
|
||||
export * from "./layout";
|
||||
export * from "./modal";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// plane imports
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
export const DEFAULT_WORK_ITEM_FORM_VALUES: Partial<TIssue> = {
|
||||
project_id: "",
|
||||
type_id: null,
|
||||
name: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
state_id: "",
|
||||
parent_id: null,
|
||||
priority: "none",
|
||||
assignee_ids: [],
|
||||
label_ids: [],
|
||||
cycle_id: null,
|
||||
module_ids: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -76,7 +76,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => {
|
||||
failedToLoadImage={failedToLoadImage}
|
||||
getPos={getPos}
|
||||
loadImageFromFileSystem={setImageFromFileSystem}
|
||||
maxFileSize={editor.storage.imageComponent.maxFileSize}
|
||||
maxFileSize={editor.storage.imageComponent?.maxFileSize}
|
||||
node={node}
|
||||
setIsUploaded={setIsUploaded}
|
||||
selected={selected}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ImageUploadStatus: React.FC<Props> = (props) => {
|
||||
// subscribe to image upload status
|
||||
const uploadStatus: number | undefined = useEditorState({
|
||||
editor,
|
||||
selector: ({ editor }) => editor.storage.imageComponent.assetsUploadStatus[nodeId],
|
||||
selector: ({ editor }) => editor.storage.imageComponent?.assetsUploadStatus[nodeId],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -22,7 +22,7 @@ declare module "@tiptap/core" {
|
||||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
|
||||
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
|
||||
updateAssetsUploadStatus?: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
|
||||
getImageSource?: (path: string) => () => Promise<string>;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -50,8 +50,7 @@ type TArguments = {
|
||||
export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args;
|
||||
|
||||
return [
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const extensions = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@@ -109,12 +108,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageExtension(fileHandler),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
@@ -152,7 +145,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
|
||||
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
||||
|
||||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
if (editor.storage.imageComponent?.uploadInProgress) return "";
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") ||
|
||||
@@ -179,4 +172,18 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
disabledExtensions,
|
||||
}),
|
||||
];
|
||||
|
||||
if (!disabledExtensions.includes("image")) {
|
||||
extensions.push(
|
||||
ImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageExtension(fileHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
return extensions;
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ export const CustomImageComponentWithoutProps = () =>
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
assetsUploadStatus: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,8 +41,7 @@ type Props = {
|
||||
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
const { disabledExtensions, fileHandler, mentionHandler } = props;
|
||||
|
||||
return [
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
const extensions = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@@ -94,12 +93,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ReadOnlyImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomReadOnlyImageExtension(fileHandler),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
@@ -136,4 +129,18 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
disabledExtensions,
|
||||
}),
|
||||
];
|
||||
|
||||
if (!disabledExtensions.includes("image")) {
|
||||
extensions.push(
|
||||
ReadOnlyImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomReadOnlyImageExtension(fileHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error tiptap types are incorrect
|
||||
return extensions;
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/typ
|
||||
// plane editor extensions
|
||||
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
|
||||
// local types
|
||||
import { TExtensionProps } from "./root";
|
||||
import { TExtensionProps, TSlashCommandAdditionalOption } from "./root";
|
||||
|
||||
export type TSlashCommandSection = {
|
||||
key: TSlashCommandSectionKeys;
|
||||
@@ -54,7 +54,7 @@ export type TSlashCommandSection = {
|
||||
export const getSlashCommandFilteredSections =
|
||||
(args: TExtensionProps) =>
|
||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||
const { additionalOptions, disabledExtensions } = args;
|
||||
const { additionalOptions: externalAdditionalOptions, disabledExtensions } = args;
|
||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||
{
|
||||
key: "general",
|
||||
@@ -176,15 +176,6 @@ export const getSlashCommandFilteredSections =
|
||||
icon: <Code2 className="size-3.5" />,
|
||||
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
commandKey: "image",
|
||||
key: "image",
|
||||
title: "Image",
|
||||
icon: <ImageIcon className="size-3.5" />,
|
||||
description: "Insert an image",
|
||||
searchTerms: ["img", "photo", "picture", "media", "upload"],
|
||||
command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }),
|
||||
},
|
||||
{
|
||||
commandKey: "callout",
|
||||
key: "callout",
|
||||
@@ -284,8 +275,24 @@ export const getSlashCommandFilteredSections =
|
||||
},
|
||||
];
|
||||
|
||||
const internalAdditionalOptions: TSlashCommandAdditionalOption[] = [];
|
||||
if (!disabledExtensions?.includes("image")) {
|
||||
internalAdditionalOptions.push({
|
||||
commandKey: "image",
|
||||
key: "image",
|
||||
title: "Image",
|
||||
icon: <ImageIcon className="size-3.5" />,
|
||||
description: "Insert an image",
|
||||
searchTerms: ["img", "photo", "picture", "media", "upload"],
|
||||
command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }),
|
||||
section: "general",
|
||||
pushAfter: "code",
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
...(additionalOptions ?? []),
|
||||
...internalAdditionalOptions,
|
||||
...(externalAdditionalOptions ?? []),
|
||||
...coreEditorAdditionalSlashCommandOptions({
|
||||
disabledExtensions,
|
||||
}),
|
||||
|
||||
@@ -111,7 +111,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
// value is null when intentionally passed where syncing is not yet
|
||||
// supported and value is undefined when the data from swr is not populated
|
||||
if (value == null) return;
|
||||
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
|
||||
if (editor && !editor.isDestroyed && !editor.storage.imageComponent?.uploadInProgress) {
|
||||
try {
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
if (editor.state.selection) {
|
||||
@@ -129,7 +129,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const assetsUploadStatus = fileHandler.assetsUploadStatus;
|
||||
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
|
||||
editor.commands.updateAssetsUploadStatus?.(assetsUploadStatus);
|
||||
}, [editor, fileHandler.assetsUploadStatus]);
|
||||
|
||||
useImperativeHandle(
|
||||
@@ -221,7 +221,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
},
|
||||
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
|
||||
isEditorReadyToDiscard: () => editor?.storage.imageComponent?.uploadInProgress === false,
|
||||
setFocusAtPosition: (position: number) => {
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.error("Editor reference is not available or has been destroyed.");
|
||||
|
||||
@@ -21,7 +21,9 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
const uploadFile = useCallback(
|
||||
async (file: File) => {
|
||||
const setImageUploadInProgress = (isUploading: boolean) => {
|
||||
editor.storage.imageComponent.uploadInProgress = isUploading;
|
||||
if (editor.storage.imageComponent) {
|
||||
editor.storage.imageComponent.uploadInProgress = isUploading;
|
||||
}
|
||||
};
|
||||
setImageUploadInProgress(true);
|
||||
setUploading(true);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key";
|
||||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key" | "image";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -687,7 +687,7 @@
|
||||
"you": "You",
|
||||
"upgrade_cta": {
|
||||
"higher_subscription": "Upgrade to higher subscription",
|
||||
"talk_to_sales": "Talk to sales"
|
||||
"talk_to_sales": "Talk to Sales"
|
||||
},
|
||||
"category": "Category",
|
||||
"categories": "Categories",
|
||||
@@ -696,7 +696,8 @@
|
||||
"delete": "Delete",
|
||||
"deleting": "Deleting",
|
||||
"pending": "Pending",
|
||||
"invite": "Invite"
|
||||
"invite": "Invite",
|
||||
"view": "View"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -814,7 +815,8 @@
|
||||
"sub_issue_count": "Sub-work item count",
|
||||
"attachment_count": "Attachment count",
|
||||
"created_on": "Created on",
|
||||
"sub_issue": "Sub-work item"
|
||||
"sub_issue": "Sub-work item",
|
||||
"work_item_count": "Work item count"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "Show sub-work items",
|
||||
|
||||
@@ -867,7 +867,8 @@
|
||||
"delete": "Eliminar",
|
||||
"deleting": "Eliminando",
|
||||
"pending": "Pendiente",
|
||||
"invite": "Invitar"
|
||||
"invite": "Invitar",
|
||||
"view": "Ver"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -985,7 +986,8 @@
|
||||
"sub_issue_count": "Cantidad de sub-elementos",
|
||||
"attachment_count": "Cantidad de archivos adjuntos",
|
||||
"created_on": "Creado el",
|
||||
"sub_issue": "Sub-elemento de trabajo"
|
||||
"sub_issue": "Sub-elemento de trabajo",
|
||||
"work_item_count": "Recuento de elementos de trabajo"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "Mostrar sub-elementos",
|
||||
|
||||
@@ -856,7 +856,7 @@
|
||||
"you": "Vous",
|
||||
"upgrade_cta": {
|
||||
"higher_subscription": "Passer à une abonnement plus élevé",
|
||||
"talk_to_sales": "Parler à la vente"
|
||||
"talk_to_sales": "Parler aux ventes"
|
||||
},
|
||||
"category": "Catégorie",
|
||||
"categories": "Catégories",
|
||||
@@ -865,7 +865,8 @@
|
||||
"delete": "Supprimer",
|
||||
"deleting": "Suppression",
|
||||
"pending": "En attente",
|
||||
"invite": "Inviter"
|
||||
"invite": "Inviter",
|
||||
"view": "Afficher"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -983,7 +984,8 @@
|
||||
"sub_issue_count": "Nombre de sous-éléments",
|
||||
"attachment_count": "Nombre de pièces jointes",
|
||||
"created_on": "Créé le",
|
||||
"sub_issue": "Sous-élément de travail"
|
||||
"sub_issue": "Sous-élément de travail",
|
||||
"work_item_count": "Nombre d'éléments de travail"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "Afficher les sous-éléments",
|
||||
|
||||
@@ -862,7 +862,8 @@
|
||||
"delete": "Elimina",
|
||||
"deleting": "Eliminazione in corso",
|
||||
"pending": "In sospeso",
|
||||
"invite": "Invita"
|
||||
"invite": "Invita",
|
||||
"view": "Visualizza"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -980,7 +981,8 @@
|
||||
"sub_issue_count": "Numero di sotto-elementi di lavoro",
|
||||
"attachment_count": "Numero di allegati",
|
||||
"created_on": "Creato il",
|
||||
"sub_issue": "Sotto-elemento di lavoro"
|
||||
"sub_issue": "Sotto-elemento di lavoro",
|
||||
"work_item_count": "Conteggio degli elementi di lavoro"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "Mostra sotto-elementi di lavoro",
|
||||
|
||||
@@ -856,7 +856,7 @@
|
||||
"you": "あなた",
|
||||
"upgrade_cta": {
|
||||
"higher_subscription": "高いサブスクリプションにアップグレード",
|
||||
"talk_to_sales": "セールスに連絡"
|
||||
"talk_to_sales": "トーク トゥ セールス"
|
||||
},
|
||||
"category": "カテゴリー",
|
||||
"categories": "カテゴリーズ",
|
||||
@@ -865,7 +865,8 @@
|
||||
"delete": "デリート",
|
||||
"deleting": "デリーティング",
|
||||
"pending": "保留中",
|
||||
"invite": "招待"
|
||||
"invite": "招待",
|
||||
"view": "ビュー"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -983,7 +984,8 @@
|
||||
"sub_issue_count": "サブ作業項目数",
|
||||
"attachment_count": "添付ファイル数",
|
||||
"created_on": "作成日",
|
||||
"sub_issue": "サブ作業項目"
|
||||
"sub_issue": "サブ作業項目",
|
||||
"work_item_count": "作業項目数"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "サブ作業項目を表示",
|
||||
|
||||
@@ -864,7 +864,8 @@
|
||||
"delete": "Удалить",
|
||||
"deleting": "Удаление",
|
||||
"pending": "Ожидание",
|
||||
"invite": "Пригласить"
|
||||
"invite": "Пригласить",
|
||||
"view": "Просмотр"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -982,7 +983,8 @@
|
||||
"sub_issue_count": "Количество подэлементов",
|
||||
"attachment_count": "Количество вложений",
|
||||
"created_on": "Дата создания",
|
||||
"sub_issue": "Подэлемент"
|
||||
"sub_issue": "Подэлемент",
|
||||
"work_item_count": "Количество рабочих элементов"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "Показывать подэлементы",
|
||||
@@ -1866,7 +1868,7 @@
|
||||
}
|
||||
},
|
||||
"completed_no_issues": {
|
||||
"title": "Нет рабочих элементов в цикле",
|
||||
"title": "Нет рабочих элементов в цикле",
|
||||
"description": "Нет рабочих элементов. Рабочие элементы были перенесены или скрыты. Для просмотра измените настройки отображения."
|
||||
},
|
||||
"active": {
|
||||
@@ -2285,7 +2287,7 @@
|
||||
"short_description": "Экспорт в csv"
|
||||
},
|
||||
"excel": {
|
||||
"title": "Excel",
|
||||
"title": "Excel",
|
||||
"description": "Экспорт рабочих элементов в файл Excel.",
|
||||
"short_description": "Экспорт в excel"
|
||||
},
|
||||
@@ -2303,7 +2305,7 @@
|
||||
"default_global_view": {
|
||||
"all_issues": "Все рабочие элементы",
|
||||
"assigned": "Назначенные",
|
||||
"created": "Созданные",
|
||||
"created": "Созданные",
|
||||
"subscribed": "Подписанные"
|
||||
},
|
||||
|
||||
@@ -2332,7 +2334,7 @@
|
||||
"project_modules": {
|
||||
"status": {
|
||||
"backlog": "Бэклог",
|
||||
"planned": "Запланировано",
|
||||
"planned": "Запланировано",
|
||||
"in_progress": "В процессе",
|
||||
"paused": "Приостановлено",
|
||||
"completed": "Завершено",
|
||||
|
||||
@@ -865,7 +865,8 @@
|
||||
"delete": "删除",
|
||||
"deleting": "删除中",
|
||||
"pending": "待处理",
|
||||
"invite": "邀请"
|
||||
"invite": "邀请",
|
||||
"view": "查看"
|
||||
},
|
||||
|
||||
"chart": {
|
||||
@@ -983,7 +984,8 @@
|
||||
"sub_issue_count": "子工作项数量",
|
||||
"attachment_count": "附件数量",
|
||||
"created_on": "创建于",
|
||||
"sub_issue": "子工作项"
|
||||
"sub_issue": "子工作项",
|
||||
"work_item_count": "工作项数量"
|
||||
},
|
||||
"extra": {
|
||||
"show_sub_issues": "显示子工作项",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/services",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
"main": "./src/index.d.ts"
|
||||
|
||||
Vendored
+1
@@ -40,3 +40,4 @@ export * from "./epics";
|
||||
export * from "./charts";
|
||||
export * from "./home";
|
||||
export * from "./stickies";
|
||||
export * from "./utils";
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export type PartialDeep<K> = {
|
||||
[attr in keyof K]?: K[attr] extends object ? PartialDeep<K[attr]> : K[attr];
|
||||
};
|
||||
|
||||
export type CompleteOrEmpty<T> = T | Record<string, never>;
|
||||
Vendored
+10
-6
@@ -1,9 +1,6 @@
|
||||
export type TIssueLayouts =
|
||||
| "list"
|
||||
| "kanban"
|
||||
| "calendar"
|
||||
| "spreadsheet"
|
||||
| "gantt_chart";
|
||||
import { TIssue } from "./issues/issue";
|
||||
|
||||
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
||||
|
||||
export type TIssueGroupByOptions =
|
||||
| "state"
|
||||
@@ -211,3 +208,10 @@ export interface IssuePaginationOptions {
|
||||
subGroupedBy?: TIssueGroupByOptions;
|
||||
orderBy?: TIssueOrderByOptions;
|
||||
}
|
||||
|
||||
export type TSpreadsheetColumn = React.FC<{
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -5,3 +5,37 @@ import { twMerge } from "tailwind-merge";
|
||||
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
/**
|
||||
* Extracts IDs from an array of objects with ID property
|
||||
*/
|
||||
export const extractIds = <T extends { id: string }>(items: T[]): string[] => items.map((item) => item.id);
|
||||
|
||||
/**
|
||||
* Checks if an ID exists and is valid within the provided list
|
||||
*/
|
||||
export const isValidId = (id: string | null | undefined, validIds: string[]): boolean => !!id && validIds.includes(id);
|
||||
|
||||
/**
|
||||
* Filters an array to only include valid IDs
|
||||
*/
|
||||
export const filterValidIds = (ids: string[], validIds: string[]): string[] =>
|
||||
ids.filter((id) => validIds.includes(id));
|
||||
|
||||
/**
|
||||
* Filters an array to include only valid IDs, returning both valid and invalid IDs
|
||||
*/
|
||||
export const partitionValidIds = (ids: string[], validIds: string[]): { valid: string[]; invalid: string[] } => {
|
||||
const valid: string[] = [];
|
||||
const invalid: string[] = [];
|
||||
|
||||
ids.forEach((id) => {
|
||||
if (validIds.includes(id)) {
|
||||
valid.push(id);
|
||||
} else {
|
||||
invalid.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return { valid, invalid };
|
||||
};
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from "./state";
|
||||
export * from "./string";
|
||||
export * from "./theme";
|
||||
export * from "./workspace";
|
||||
export * from "./work-item";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./modal";
|
||||
@@ -0,0 +1,33 @@
|
||||
// plane imports
|
||||
import { DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants";
|
||||
import { IPartialProject, ISearchIssueResponse, IState, TIssue } from "@plane/types";
|
||||
|
||||
export const getUpdateFormDataForReset = (projectId: string | null | undefined, formData: Partial<TIssue>) => ({
|
||||
...DEFAULT_WORK_ITEM_FORM_VALUES,
|
||||
project_id: projectId,
|
||||
name: formData.name,
|
||||
description_html: formData.description_html,
|
||||
priority: formData.priority,
|
||||
start_date: formData.start_date,
|
||||
target_date: formData.target_date,
|
||||
});
|
||||
|
||||
export const convertWorkItemDataToSearchResponse = (
|
||||
workspaceSlug: string,
|
||||
workItem: TIssue,
|
||||
project: IPartialProject | undefined,
|
||||
state: IState | undefined
|
||||
): ISearchIssueResponse => ({
|
||||
id: workItem.id,
|
||||
name: workItem.name,
|
||||
project_id: workItem.project_id ?? "",
|
||||
project__identifier: project?.identifier ?? "",
|
||||
project__name: project?.name ?? "",
|
||||
sequence_id: workItem.sequence_id,
|
||||
type_id: workItem.type_id ?? "",
|
||||
state__color: state?.color ?? "",
|
||||
start_date: workItem.start_date,
|
||||
state__group: state?.group ?? "backlog",
|
||||
state__name: state?.name ?? "",
|
||||
workspace__slug: workspaceSlug,
|
||||
});
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
"develop": "next dev -p 3002",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { FC, ReactNode } from "react";
|
||||
// components
|
||||
import { AppHeader } from "@/components/core";
|
||||
// local components
|
||||
import { ProjectSettingHeader } from "./header";
|
||||
import { ProjectSettingHeader } from "../header";
|
||||
import { ProjectSettingsSidebar } from "./sidebar";
|
||||
|
||||
export interface IProjectSettingLayout {
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { AppHeader } from "@/components/core";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
// local components
|
||||
import { WorkspaceSettingHeader } from "./header";
|
||||
import { WorkspaceSettingHeader } from "../header";
|
||||
import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs";
|
||||
import { WorkspaceSettingsSidebar } from "./sidebar";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FC } from "react";
|
||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||
|
||||
export type TWorkItemLayoutAdditionalProperties = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const WorkItemLayoutAdditionalProperties: FC<TWorkItemLayoutAdditionalProperties> = (props) => <></>;
|
||||
@@ -4,9 +4,10 @@ import React, { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
showProgressText?: boolean;
|
||||
showLabel?: boolean;
|
||||
};
|
||||
|
||||
export const IssueStats: FC<Props> = (props) => {
|
||||
const { issueId } = props;
|
||||
return <></>;
|
||||
};
|
||||
export const IssueStats: FC<Props> = (props) => <></>;
|
||||
|
||||
@@ -1,4 +1,69 @@
|
||||
import { FC } from "react";
|
||||
import {
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CalendarDays,
|
||||
ContrastIcon,
|
||||
LayersIcon,
|
||||
Link2,
|
||||
Paperclip,
|
||||
Signal,
|
||||
Tag,
|
||||
Triangle,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
// types
|
||||
import { IGroupByColumn } from "@plane/types";
|
||||
import { IGroupByColumn, IIssueDisplayProperties, TSpreadsheetColumn } from "@plane/types";
|
||||
import { DiceIcon, DoubleCircleIcon, ISvgIcons } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetAttachmentColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetModuleColumn,
|
||||
SpreadsheetCycleColumn,
|
||||
SpreadsheetLinkColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "@/components/issues/issue-layouts/spreadsheet";
|
||||
|
||||
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
|
||||
|
||||
export const SpreadSheetPropertyIconMap: Record<string, FC<ISvgIcons>> = {
|
||||
Users: Users,
|
||||
CalenderDays: CalendarDays,
|
||||
CalenderCheck2: CalendarCheck2,
|
||||
Triangle: Triangle,
|
||||
Tag: Tag,
|
||||
DiceIcon: DiceIcon,
|
||||
ContrastIcon: ContrastIcon,
|
||||
Signal: Signal,
|
||||
CalendarClock: CalendarClock,
|
||||
DoubleCircleIcon: DoubleCircleIcon,
|
||||
Link2: Link2,
|
||||
Paperclip: Paperclip,
|
||||
LayersIcon: LayersIcon,
|
||||
};
|
||||
|
||||
export const SPREADSHEET_COLUMNS: { [key in keyof IIssueDisplayProperties]: TSpreadsheetColumn } = {
|
||||
assignee: SpreadsheetAssigneeColumn,
|
||||
created_on: SpreadsheetCreatedOnColumn,
|
||||
due_date: SpreadsheetDueDateColumn,
|
||||
estimate: SpreadsheetEstimateColumn,
|
||||
labels: SpreadsheetLabelColumn,
|
||||
modules: SpreadsheetModuleColumn,
|
||||
cycle: SpreadsheetCycleColumn,
|
||||
link: SpreadsheetLinkColumn,
|
||||
priority: SpreadsheetPriorityColumn,
|
||||
start_date: SpreadsheetStartDateColumn,
|
||||
state: SpreadsheetStateColumn,
|
||||
sub_issue_count: SpreadsheetSubIssueColumn,
|
||||
updated_on: SpreadsheetUpdatedOnColumn,
|
||||
attachment_count: SpreadsheetAttachmentColumn,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./provider";
|
||||
export * from "./issue-type-select";
|
||||
export * from "./additional-properties";
|
||||
export * from "./template-select";
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Control } from "react-hook-form";
|
||||
// plane imports
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { TBulkIssueProperties, TIssue } from "@plane/types";
|
||||
|
||||
@@ -9,6 +11,7 @@ export type TIssueTypeDropdownVariant = "xs" | "sm";
|
||||
export type TIssueTypeSelectProps<T extends Partial<TIssueFields>> = {
|
||||
control: Control<T>;
|
||||
projectId: string | null;
|
||||
editorRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
disabled?: boolean;
|
||||
variant?: TIssueTypeDropdownVariant;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// plane imports
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
// components
|
||||
import { IssueModalContext } from "@/components/issues";
|
||||
|
||||
type TIssueModalProviderProps = {
|
||||
export type TIssueModalProviderProps = {
|
||||
templateId?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const IssueModalProvider = observer((props: TIssueModalProviderProps) => {
|
||||
const { children } = props;
|
||||
// states
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
|
||||
return (
|
||||
<IssueModalContext.Provider
|
||||
value={{
|
||||
workItemTemplateId: null,
|
||||
setWorkItemTemplateId: () => {},
|
||||
isApplyingTemplate: false,
|
||||
setIsApplyingTemplate: () => {},
|
||||
selectedParentIssue,
|
||||
setSelectedParentIssue,
|
||||
issuePropertyValues: {},
|
||||
setIssuePropertyValues: () => {},
|
||||
issuePropertyValueErrors: {},
|
||||
@@ -20,6 +32,9 @@ export const IssueModalProvider = observer((props: TIssueModalProviderProps) =>
|
||||
getActiveAdditionalPropertiesLength: () => 0,
|
||||
handlePropertyValuesValidation: () => true,
|
||||
handleCreateUpdatePropertyValues: () => Promise.resolve(),
|
||||
handleParentWorkItemDetails: () => Promise.resolve(undefined),
|
||||
handleProjectEntitiesFetch: () => Promise.resolve(),
|
||||
handleTemplateChange: () => Promise.resolve(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export type TWorkItemTemplateDropdownSize = "xs" | "sm";
|
||||
|
||||
export type TWorkItemTemplateSelect = {
|
||||
projectId: string | null;
|
||||
typeId: string | null;
|
||||
disabled?: boolean;
|
||||
size?: TWorkItemTemplateDropdownSize;
|
||||
placeholder?: string;
|
||||
renderChevron?: boolean;
|
||||
dropDownContainerClassName?: string;
|
||||
handleFormChange?: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const WorkItemTemplateSelect = (props: TWorkItemTemplateSelect) => <></>;
|
||||
@@ -26,7 +26,7 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||
} = useMember();
|
||||
// derived values
|
||||
const projectId = issue?.project_id ?? "";
|
||||
const projectMemberIds = getProjectMemberIds(projectId);
|
||||
const projectMemberIds = getProjectMemberIds(projectId, false);
|
||||
|
||||
const options =
|
||||
projectMemberIds
|
||||
|
||||
@@ -24,13 +24,15 @@ import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string | undefined;
|
||||
projectId: string | undefined;
|
||||
projectId?: string;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
searchParams: Partial<TProjectIssuesSearchParams>;
|
||||
handleOnSubmit: (data: ISearchIssueResponse[]) => Promise<void>;
|
||||
workspaceLevelToggle?: boolean;
|
||||
shouldHideIssue?: (issue: ISearchIssueResponse) => boolean;
|
||||
selectedWorkItems?: ISearchIssueResponse[];
|
||||
workItemSearchServiceCallback?: (params: TProjectIssuesSearchParams) => Promise<ISearchIssueResponse[]>;
|
||||
};
|
||||
|
||||
const projectService = new ProjectService();
|
||||
@@ -47,6 +49,8 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
handleOnSubmit,
|
||||
workspaceLevelToggle = false,
|
||||
shouldHideIssue,
|
||||
selectedWorkItems,
|
||||
workItemSearchServiceCallback,
|
||||
} = props;
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -85,20 +89,35 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
const handleSearch = () => {
|
||||
if (!isOpen || !workspaceSlug) return;
|
||||
setIsLoading(true);
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||
search: debouncedSearchTerm,
|
||||
...searchParams,
|
||||
workspace_search: isWorkspaceLevel,
|
||||
})
|
||||
const searchService =
|
||||
workItemSearchServiceCallback ??
|
||||
(projectId
|
||||
? projectService.projectIssuesSearch.bind(projectService, workspaceSlug?.toString(), projectId?.toString())
|
||||
: undefined);
|
||||
if (!searchService) return;
|
||||
searchService({
|
||||
search: debouncedSearchTerm,
|
||||
...searchParams,
|
||||
workspace_search: isWorkspaceLevel,
|
||||
})
|
||||
.then((res) => setIssues(res))
|
||||
.finally(() => {
|
||||
setIsSearching(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWorkItems) {
|
||||
setSelectedIssues(selectedWorkItems);
|
||||
}
|
||||
}, [isOpen, selectedWorkItems]);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearch();
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]);
|
||||
|
||||
const filteredIssues = issues.filter((issue) => !shouldHideIssue?.(issue));
|
||||
|
||||
@@ -68,7 +68,11 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||
}
|
||||
}, [isOpen, isMobile]);
|
||||
|
||||
const memberIds = propsMemberIds ? propsMemberIds : projectId ? getProjectMemberIds(projectId) : workspaceMemberIds;
|
||||
const memberIds = propsMemberIds
|
||||
? propsMemberIds
|
||||
: projectId
|
||||
? getProjectMemberIds(projectId, true)
|
||||
: workspaceMemberIds;
|
||||
const onOpen = () => {
|
||||
if (!memberIds && workspaceSlug && projectId) fetchProjectMembers(workspaceSlug.toString(), projectId);
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
}}
|
||||
{...rest}
|
||||
containerClassName={cn("relative pl-3", containerClassName)}
|
||||
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ type Props = {
|
||||
secondaryButton?: ButtonConfig;
|
||||
customPrimaryButton?: React.ReactNode;
|
||||
customSecondaryButton?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
@@ -66,12 +67,18 @@ export const DetailedEmptyState: React.FC<Props> = observer((props) => {
|
||||
customPrimaryButton,
|
||||
customSecondaryButton,
|
||||
assetPath,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center min-h-full min-w-full overflow-y-auto py-10 md:px-20 px-5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex flex-col gap-5", sizeClasses[size])}>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink">
|
||||
<h3 className={cn("text-xl font-semibold", { "font-medium": !description })}>{title}</h3>
|
||||
|
||||
@@ -66,7 +66,7 @@ export const IssueAttachmentsListItem: FC<TIssueAttachmentsListItem> = observer(
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{attachment?.updated_by && (
|
||||
{attachment?.created_by && (
|
||||
<>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
|
||||
}
|
||||
}).map((property) => {
|
||||
if (isEpic && property.key === "sub_issue_count") {
|
||||
return { ...property, title: "Work item count" };
|
||||
return { ...property, titleTranslationKey: "issue.display.properties.work_item_count" };
|
||||
}
|
||||
return property;
|
||||
});
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { Tooltip, ControlLink } from "@plane/ui";
|
||||
// components
|
||||
import { findTotalDaysInRange } from "@plane/utils";
|
||||
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { generateWorkItemLink } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useIssues, useProject, useProjectState } from "@/hooks/store";
|
||||
@@ -17,6 +17,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
//
|
||||
import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
|
||||
import { getBlockViewDetails } from "../utils";
|
||||
import { GanttStoreType } from "./base-gantt-root";
|
||||
|
||||
@@ -48,6 +49,8 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
||||
|
||||
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
|
||||
const duration = findTotalDaysInRange(issueDetails?.start_date, issueDetails?.target_date) || 0;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
@@ -62,17 +65,24 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
|
||||
>
|
||||
<div
|
||||
id={`issue-${issueId}`}
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded"
|
||||
className="relative flex h-full w-full cursor-pointer items-center rounded space-between"
|
||||
style={blockStyle}
|
||||
onClick={handleIssuePeekOverview}
|
||||
>
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
|
||||
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50 " />
|
||||
<div
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
|
||||
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100 flex-1"
|
||||
style={{ left: `${SIDEBAR_WIDTH}px` }}
|
||||
>
|
||||
{issueDetails?.name}
|
||||
</div>
|
||||
{isEpic && (
|
||||
<IssueStats
|
||||
issueId={issueId}
|
||||
className="sticky mx-2 font-medium text-custom-text-100 overflow-hidden truncate w-auto justify-end flex-shrink-0"
|
||||
showProgressText={duration >= 2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -25,8 +25,10 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { getIssueBlockId } from "../utils";
|
||||
|
||||
interface IssueBlockProps {
|
||||
@@ -61,6 +63,9 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
// derived values
|
||||
const subIssueCount = issue?.sub_issues_count ?? 0;
|
||||
|
||||
const handleEventPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -105,6 +110,16 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
||||
isReadOnly={isReadOnly}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
|
||||
{isEpic && displayProperties && (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey="sub_issue_count"
|
||||
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!subIssueCount}
|
||||
>
|
||||
<IssueStats issueId={issue.id} className="mt-2 font-medium text-custom-text-350" />
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
|
||||
// types
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
@@ -269,7 +270,15 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
>
|
||||
<p className="truncate cursor-pointer text-sm text-custom-text-100">{issue.name}</p>
|
||||
</Tooltip>
|
||||
{isEpic && <IssueStats issueId={issue.id} />}
|
||||
{isEpic && displayProperties && (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey="sub_issue_count"
|
||||
shouldRenderProperty={(properties) => !!properties.sub_issue_count}
|
||||
>
|
||||
<IssueStats issueId={issue.id} className="ml-2 font-medium text-custom-text-350" />
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
</div>
|
||||
{!issue?.tempId && (
|
||||
<div
|
||||
|
||||
@@ -33,6 +33,8 @@ import { useEventTracker, useLabel, useIssues, useProjectState, useProject, useP
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { WorkItemLayoutAdditionalProperties } from "@/plane-web/components/issues/issue-layouts/additional-properties";
|
||||
// local components
|
||||
import { IssuePropertyLabels } from "./labels";
|
||||
import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC";
|
||||
@@ -429,36 +431,38 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
|
||||
{/* extra render properties */}
|
||||
{/* sub-issues */}
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey="sub_issue_count"
|
||||
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!subIssueCount}
|
||||
>
|
||||
<Tooltip
|
||||
tooltipHeading={isEpic ? t("issues.label", { count: 2 }) : t("common.sub_work_items")}
|
||||
tooltipContent={`${subIssueCount}`}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={false}
|
||||
{!isEpic && (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey="sub_issue_count"
|
||||
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!subIssueCount}
|
||||
>
|
||||
<div
|
||||
onFocus={handleEventPropagation}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (subIssueCount) redirectToIssueDetail();
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1",
|
||||
{
|
||||
"hover:bg-custom-background-80 cursor-pointer": subIssueCount,
|
||||
}
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading={t("common.sub_work_items")}
|
||||
tooltipContent={`${subIssueCount}`}
|
||||
isMobile={isMobile}
|
||||
renderByDefault={false}
|
||||
>
|
||||
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-xs">{subIssueCount}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</WithDisplayPropertiesHOC>
|
||||
<div
|
||||
onFocus={handleEventPropagation}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (subIssueCount) redirectToIssueDetail();
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded border-[0.5px] border-custom-border-300 px-2.5 py-1",
|
||||
{
|
||||
"hover:bg-custom-background-80 cursor-pointer": subIssueCount,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
|
||||
<div className="text-xs">{subIssueCount}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
|
||||
{/* attachments */}
|
||||
<WithDisplayPropertiesHOC
|
||||
@@ -506,6 +510,9 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
</Tooltip>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
{/* Additional Properties */}
|
||||
<WorkItemLayoutAdditionalProperties displayProperties={displayProperties} issue={issue} />
|
||||
|
||||
{/* label */}
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
|
||||
<IssuePropertyLabels
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
"use client";
|
||||
import { FC } from "react";
|
||||
//ui
|
||||
import {
|
||||
ArrowDownWideNarrow,
|
||||
ArrowUpNarrowWide,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
Eraser,
|
||||
MoveRight,
|
||||
CalendarDays,
|
||||
Link2,
|
||||
Signal,
|
||||
Tag,
|
||||
Triangle,
|
||||
Paperclip,
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
|
||||
// constants
|
||||
import { SPREADSHEET_PROPERTY_DETAILS } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types";
|
||||
import { LayersIcon, DoubleCircleIcon, DiceIcon, ContrastIcon, CustomMenu, Row } from "@plane/ui";
|
||||
// ui
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
import { CustomMenu, Row } from "@plane/ui";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { SpreadSheetPropertyIcon } from "../../utils";
|
||||
|
||||
interface Props {
|
||||
property: keyof IIssueDisplayProperties;
|
||||
@@ -35,130 +19,6 @@ interface Props {
|
||||
isEpic?: boolean;
|
||||
}
|
||||
|
||||
export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
[key in keyof IIssueDisplayProperties]: {
|
||||
i18n_title: string;
|
||||
ascendingOrderKey: TIssueOrderByOptions;
|
||||
ascendingOrderTitle: string;
|
||||
descendingOrderKey: TIssueOrderByOptions;
|
||||
descendingOrderTitle: string;
|
||||
icon: FC<ISvgIcons>;
|
||||
};
|
||||
} = {
|
||||
assignee: {
|
||||
i18n_title: "common.assignees",
|
||||
ascendingOrderKey: "assignees__first_name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-assignees__first_name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: Users,
|
||||
},
|
||||
created_on: {
|
||||
i18n_title: "common.sort.created_on",
|
||||
ascendingOrderKey: "-created_at",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "created_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
},
|
||||
due_date: {
|
||||
i18n_title: "common.order_by.due_date",
|
||||
ascendingOrderKey: "-target_date",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "target_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarCheck2,
|
||||
},
|
||||
estimate: {
|
||||
i18n_title: "common.estimate",
|
||||
ascendingOrderKey: "estimate_point__key",
|
||||
ascendingOrderTitle: "Low",
|
||||
descendingOrderKey: "-estimate_point__key",
|
||||
descendingOrderTitle: "High",
|
||||
icon: Triangle,
|
||||
},
|
||||
labels: {
|
||||
i18n_title: "common.labels",
|
||||
ascendingOrderKey: "labels__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-labels__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: Tag,
|
||||
},
|
||||
modules: {
|
||||
i18n_title: "common.modules",
|
||||
ascendingOrderKey: "issue_module__module__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-issue_module__module__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: DiceIcon,
|
||||
},
|
||||
cycle: {
|
||||
i18n_title: "common.cycle",
|
||||
ascendingOrderKey: "issue_cycle__cycle__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-issue_cycle__cycle__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: ContrastIcon,
|
||||
},
|
||||
priority: {
|
||||
i18n_title: "common.priority",
|
||||
ascendingOrderKey: "priority",
|
||||
ascendingOrderTitle: "None",
|
||||
descendingOrderKey: "-priority",
|
||||
descendingOrderTitle: "Urgent",
|
||||
icon: Signal,
|
||||
},
|
||||
start_date: {
|
||||
i18n_title: "common.order_by.start_date",
|
||||
ascendingOrderKey: "-start_date",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "start_date",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarClock,
|
||||
},
|
||||
state: {
|
||||
i18n_title: "common.state",
|
||||
ascendingOrderKey: "state__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-state__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: DoubleCircleIcon,
|
||||
},
|
||||
updated_on: {
|
||||
i18n_title: "common.sort.updated_on",
|
||||
ascendingOrderKey: "-updated_at",
|
||||
ascendingOrderTitle: "New",
|
||||
descendingOrderKey: "updated_at",
|
||||
descendingOrderTitle: "Old",
|
||||
icon: CalendarDays,
|
||||
},
|
||||
link: {
|
||||
i18n_title: "common.link",
|
||||
ascendingOrderKey: "-link_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "link_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: Link2,
|
||||
},
|
||||
attachment_count: {
|
||||
i18n_title: "common.attachment",
|
||||
ascendingOrderKey: "-attachment_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "attachment_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: Paperclip,
|
||||
},
|
||||
sub_issue_count: {
|
||||
i18n_title: "issue.display.properties.sub_issue",
|
||||
ascendingOrderKey: "-sub_issues_count",
|
||||
ascendingOrderTitle: "Most",
|
||||
descendingOrderKey: "sub_issues_count",
|
||||
descendingOrderTitle: "Least",
|
||||
icon: LayersIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export const HeaderColumn = (props: Props) => {
|
||||
const { displayFilters, handleDisplayFilterUpdate, property, onClose, isEpic = false } = props;
|
||||
// i18n
|
||||
@@ -190,7 +50,7 @@ export const HeaderColumn = (props: Props) => {
|
||||
customButton={
|
||||
<Row className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{<propertyDetails.icon className="h-4 w-4 text-custom-text-400" />}
|
||||
{<SpreadSheetPropertyIcon iconKey={propertyDetails.icon} className="h-4 w-4 text-custom-text-400" />}
|
||||
{property === "sub_issue_count" && isEpic ? t("issue.label", { count: 2 }) : t(propertyDetails.i18n_title)}
|
||||
</div>
|
||||
<div className="ml-3 flex">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Row } from "@plane/ui";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-stats";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
@@ -18,30 +19,30 @@ export const SpreadsheetSubIssueColumn: React.FC<Props> = observer((props: Props
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// hooks
|
||||
const { workspaceSlug, epicId } = useParams();
|
||||
const { workspaceSlug } = useParams();
|
||||
// derived values
|
||||
const isEpic = issue?.is_epic;
|
||||
const subIssueCount = issue?.sub_issues_count ?? 0;
|
||||
|
||||
const redirectToIssueDetail = () => {
|
||||
router.push(
|
||||
`/${workspaceSlug?.toString()}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${epicId ? "epics" : "issues"}/${issue.id}#sub-issues`
|
||||
`/${workspaceSlug?.toString()}/projects/${issue.project_id}/${issue.archived_at ? "archives/" : ""}${isEpic ? "epics" : "issues"}/${issue.id}#sub-issues`
|
||||
);
|
||||
};
|
||||
|
||||
const issueLabel = epicId ? "work item" : "sub-work item";
|
||||
const label = `${subIssueCount} ${issueLabel}${subIssueCount !== 1 ? "s" : ""}`;
|
||||
const label = `${subIssueCount} sub-work item${subIssueCount !== 1 ? "s" : ""}`;
|
||||
|
||||
return (
|
||||
<Row
|
||||
onClick={subIssueCount ? redirectToIssueDetail : () => {}}
|
||||
className={cn(
|
||||
"flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 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",
|
||||
"flex h-11 w-full items-center border-b-[0.5px] border-custom-border-200 py-1 text-xs hover:bg-custom-background-90 group-[.selected-issue-row]:bg-custom-primary-100/5 group-[.selected-issue-row]:hover:bg-custom-primary-90",
|
||||
{
|
||||
"cursor-pointer": subIssueCount,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{isEpic ? <IssueStats issueId={issue.id} /> : label}
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,27 +3,12 @@ import { observer } from "mobx-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
// types
|
||||
import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetAttachmentColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetModuleColumn,
|
||||
SpreadsheetCycleColumn,
|
||||
SpreadsheetLinkColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetSubIssueColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "@/components/issues/issue-layouts/spreadsheet";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
// components
|
||||
import { SPREADSHEET_COLUMNS } from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
// utils
|
||||
|
||||
type Props = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
@@ -34,30 +19,6 @@ type Props = {
|
||||
isEstimateEnabled: boolean;
|
||||
};
|
||||
|
||||
type TSpreadsheetColumn = React.FC<{
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
||||
const SPREADSHEET_COLUMNS: { [key in keyof IIssueDisplayProperties]: TSpreadsheetColumn } = {
|
||||
assignee: SpreadsheetAssigneeColumn,
|
||||
created_on: SpreadsheetCreatedOnColumn,
|
||||
due_date: SpreadsheetDueDateColumn,
|
||||
estimate: SpreadsheetEstimateColumn,
|
||||
labels: SpreadsheetLabelColumn,
|
||||
modules: SpreadsheetModuleColumn,
|
||||
cycle: SpreadsheetCycleColumn,
|
||||
link: SpreadsheetLinkColumn,
|
||||
priority: SpreadsheetPriorityColumn,
|
||||
start_date: SpreadsheetStartDateColumn,
|
||||
state: SpreadsheetStateColumn,
|
||||
sub_issue_count: SpreadsheetSubIssueColumn,
|
||||
updated_on: SpreadsheetUpdatedOnColumn,
|
||||
attachment_count: SpreadsheetAttachmentColumn,
|
||||
};
|
||||
|
||||
export const IssueColumn = observer((props: Props) => {
|
||||
const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props;
|
||||
// router
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, SPREADSHEET_SELECT_GROUP } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, SPREADSHEET_SELECT_GROUP, SPREADSHEET_PROPERTY_LIST } from "@plane/constants";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
// components
|
||||
@@ -18,23 +18,6 @@ import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-sta
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { SpreadsheetTable } from "./spreadsheet-table";
|
||||
|
||||
const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
||||
"state",
|
||||
"priority",
|
||||
"assignee",
|
||||
"labels",
|
||||
"modules",
|
||||
"cycle",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"estimate",
|
||||
"created_on",
|
||||
"updated_on",
|
||||
"link",
|
||||
"attachment_count",
|
||||
"sub_issue_count",
|
||||
];
|
||||
|
||||
type Props = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { CSSProperties, FC } from "react";
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import clone from "lodash/clone";
|
||||
import concat from "lodash/concat";
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
IWorkspaceView,
|
||||
} from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
import { Avatar, CycleGroupIcon, DiceIcon, ISvgIcons, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||
// components
|
||||
import { Logo } from "@/components/common";
|
||||
// helpers
|
||||
@@ -36,7 +36,7 @@ import { getFileURL } from "@/helpers/file.helper";
|
||||
// store
|
||||
import { store } from "@/lib/store-context";
|
||||
// plane web store
|
||||
import { getTeamProjectColumns } from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
import { getTeamProjectColumns, SpreadSheetPropertyIconMap } from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
// store
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
|
||||
|
||||
@@ -708,3 +708,14 @@ export const getBlockViewDetails = (
|
||||
blockStyle,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This method returns the icon for Spreadsheet column headers
|
||||
* @param iconKey
|
||||
*/
|
||||
export const SpreadSheetPropertyIcon: FC<ISvgIcons & { iconKey: string }> = (props) => {
|
||||
const { iconKey } = props;
|
||||
const Icon = SpreadSheetPropertyIconMap[iconKey];
|
||||
if (!Icon) return null;
|
||||
return <Icon {...props} />;
|
||||
};
|
||||
|
||||
@@ -241,11 +241,11 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
setDescription("<p></p>");
|
||||
setChangesMade(null);
|
||||
return response;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: t(is_draft_issue ? "draft_creation_failed" : "issue_creation_failed"),
|
||||
message: error?.error ?? t(is_draft_issue ? "draft_creation_failed" : "issue_creation_failed"),
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
@@ -299,12 +299,12 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||
path: pathname,
|
||||
});
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("error"),
|
||||
message: t("issue_could_not_be_updated"),
|
||||
message: error?.error ?? t("issue_could_not_be_updated"),
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Control, Controller, FieldErrors } from "react-hook-form";
|
||||
import { Control, Controller, FormState } from "react-hook-form";
|
||||
// plane imports
|
||||
import { ETabIndices } from "@plane/constants";
|
||||
// types
|
||||
@@ -18,12 +18,17 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
type TIssueTitleInputProps = {
|
||||
control: Control<TIssue>;
|
||||
issueTitleRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||
errors: FieldErrors<TIssue>;
|
||||
formState: FormState<TIssue>;
|
||||
handleFormChange: () => void;
|
||||
};
|
||||
|
||||
export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props) => {
|
||||
const { control, issueTitleRef, errors, handleFormChange } = props;
|
||||
const {
|
||||
control,
|
||||
issueTitleRef,
|
||||
formState: { errors },
|
||||
handleFormChange,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { createContext } from "react";
|
||||
import { UseFormWatch } from "react-hook-form";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// plane web types
|
||||
import { TIssuePropertyValueErrors, TIssuePropertyValues } from "@/plane-web/types";
|
||||
import { createContext } from "react";
|
||||
// ce imports
|
||||
import { TIssueFields } from "ce/components/issues";
|
||||
// react-hook-form
|
||||
import { UseFormReset, UseFormWatch } from "react-hook-form";
|
||||
// plane imports
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
import { TIssuePropertyValues, TIssuePropertyValueErrors } from "@/plane-web/types/issue-types";
|
||||
|
||||
export type TPropertyValuesValidationProps = {
|
||||
projectId: string | null;
|
||||
workspaceSlug: string;
|
||||
watch: UseFormWatch<TIssue>;
|
||||
watch: UseFormWatch<TIssueFields>;
|
||||
};
|
||||
|
||||
export type TActiveAdditionalPropertiesProps = {
|
||||
projectId: string | null;
|
||||
workspaceSlug: string;
|
||||
watch: UseFormWatch<TIssue>;
|
||||
watch: UseFormWatch<TIssueFields>;
|
||||
};
|
||||
|
||||
export type TCreateUpdatePropertyValuesProps = {
|
||||
@@ -25,7 +28,31 @@ export type TCreateUpdatePropertyValuesProps = {
|
||||
isDraft?: boolean;
|
||||
};
|
||||
|
||||
export type THandleTemplateChangeProps = {
|
||||
workspaceSlug: string;
|
||||
reset: UseFormReset<TIssue>;
|
||||
editorRef: React.MutableRefObject<EditorRefApi | null>;
|
||||
};
|
||||
|
||||
export type THandleProjectEntitiesFetchProps = {
|
||||
workspaceSlug: string;
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
export type THandleParentWorkItemDetailsProps = {
|
||||
workspaceSlug: string;
|
||||
parentId: string | undefined;
|
||||
parentProjectId: string | undefined;
|
||||
isParentEpic: boolean;
|
||||
};
|
||||
|
||||
export type TIssueModalContext = {
|
||||
workItemTemplateId: string | null;
|
||||
setWorkItemTemplateId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
isApplyingTemplate: boolean;
|
||||
setIsApplyingTemplate: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
selectedParentIssue: ISearchIssueResponse | null;
|
||||
setSelectedParentIssue: React.Dispatch<React.SetStateAction<ISearchIssueResponse | null>>;
|
||||
issuePropertyValues: TIssuePropertyValues;
|
||||
setIssuePropertyValues: React.Dispatch<React.SetStateAction<TIssuePropertyValues>>;
|
||||
issuePropertyValueErrors: TIssuePropertyValueErrors;
|
||||
@@ -34,6 +61,9 @@ export type TIssueModalContext = {
|
||||
getActiveAdditionalPropertiesLength: (props: TActiveAdditionalPropertiesProps) => number;
|
||||
handlePropertyValuesValidation: (props: TPropertyValuesValidationProps) => boolean;
|
||||
handleCreateUpdatePropertyValues: (props: TCreateUpdatePropertyValuesProps) => Promise<void>;
|
||||
handleParentWorkItemDetails: (props: THandleParentWorkItemDetailsProps) => Promise<ISearchIssueResponse | undefined>;
|
||||
handleProjectEntitiesFetch: (props: THandleProjectEntitiesFetchProps) => Promise<void>;
|
||||
handleTemplateChange: (props: THandleTemplateChangeProps) => Promise<void>;
|
||||
};
|
||||
|
||||
export const IssueModalContext = createContext<TIssueModalContext | undefined>(undefined);
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
import React, { FC, useState, useRef, useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
// editor
|
||||
import { ETabIndices, EIssuesStoreType } from "@plane/constants";
|
||||
import { ETabIndices, EIssuesStoreType, DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants";
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import type { TIssue, ISearchIssueResponse, TWorkspaceDraftIssue } from "@plane/types";
|
||||
import type { TIssue, TWorkspaceDraftIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { Button, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { convertWorkItemDataToSearchResponse, getUpdateFormDataForReset } from "@plane/utils";
|
||||
import {
|
||||
IssueDefaultProperties,
|
||||
IssueDescriptionEditor,
|
||||
@@ -25,35 +26,22 @@ import { CreateLabelModal } from "@/components/labels";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getTextContent } from "@/helpers/editor.helper";
|
||||
import { getChangedIssuefields } from "@/helpers/issue.helper";
|
||||
import { getChangedIssuefields } from "@/helpers/issue-modal.helper";
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useIssueModal } from "@/hooks/context/use-issue-modal";
|
||||
import { useIssueDetail, useProject, useProjectState, useWorkspaceDraftIssues } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
|
||||
// plane web components
|
||||
// plane web imports
|
||||
import { DeDupeButtonRoot, DuplicateModalRoot } from "@/plane-web/components/de-dupe";
|
||||
import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal";
|
||||
import {
|
||||
IssueAdditionalProperties,
|
||||
IssueTypeSelect,
|
||||
WorkItemTemplateSelect,
|
||||
} from "@/plane-web/components/issues/issue-modal";
|
||||
import { useDebouncedDuplicateIssues } from "@/plane-web/hooks/use-debounced-duplicate-issues";
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
project_id: "",
|
||||
type_id: null,
|
||||
name: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
state_id: "",
|
||||
parent_id: null,
|
||||
priority: "none",
|
||||
assignee_ids: [],
|
||||
label_ids: [],
|
||||
cycle_id: null,
|
||||
module_ids: null,
|
||||
start_date: null,
|
||||
target_date: null,
|
||||
};
|
||||
|
||||
export interface IssueFormProps {
|
||||
data?: Partial<TIssue>;
|
||||
issueTitleRef: React.MutableRefObject<HTMLInputElement | null>;
|
||||
@@ -104,7 +92,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
// states
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
const [isMoving, setIsMoving] = useState<boolean>(false);
|
||||
|
||||
@@ -120,10 +107,16 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
workItemTemplateId,
|
||||
isApplyingTemplate,
|
||||
selectedParentIssue,
|
||||
setWorkItemTemplateId,
|
||||
setSelectedParentIssue,
|
||||
getIssueTypeIdOnProjectChange,
|
||||
getActiveAdditionalPropertiesLength,
|
||||
handlePropertyValuesValidation,
|
||||
handleCreateUpdatePropertyValues,
|
||||
handleTemplateChange,
|
||||
} = useIssueModal();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { moveIssue } = useWorkspaceDraftIssues();
|
||||
@@ -135,18 +128,20 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const { getStateById } = useProjectState();
|
||||
|
||||
// form info
|
||||
const methods = useForm<TIssue>({
|
||||
defaultValues: { ...DEFAULT_WORK_ITEM_FORM_VALUES, project_id: defaultProjectId, ...data },
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
const {
|
||||
formState: { errors, isDirty, isSubmitting, dirtyFields },
|
||||
formState,
|
||||
formState: { isDirty, isSubmitting, dirtyFields },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
control,
|
||||
getValues,
|
||||
setValue,
|
||||
} = useForm<TIssue>({
|
||||
defaultValues: { ...defaultValues, project_id: defaultProjectId, ...data },
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
} = methods;
|
||||
|
||||
const projectId = watch("project_id");
|
||||
const activeAdditionalPropertiesLength = getActiveAdditionalPropertiesLength({
|
||||
@@ -157,24 +152,21 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
// derived values
|
||||
const projectDetails = projectId ? getProjectById(projectId) : undefined;
|
||||
const isDisabled = isSubmitting || isApplyingTemplate;
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
|
||||
|
||||
//reset few fields on projectId change
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
const formData = getValues();
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
project_id: projectId,
|
||||
name: formData.name,
|
||||
description_html: formData.description_html,
|
||||
priority: formData.priority,
|
||||
start_date: formData.start_date,
|
||||
target_date: formData.target_date,
|
||||
parent_id: formData.parent_id,
|
||||
});
|
||||
if (workItemTemplateId) {
|
||||
// reset work item template id
|
||||
setWorkItemTemplateId(null);
|
||||
reset({ ...DEFAULT_WORK_ITEM_FORM_VALUES, project_id: projectId });
|
||||
editorRef.current?.clearEditor();
|
||||
} else {
|
||||
reset(getUpdateFormDataForReset(projectId, getValues()));
|
||||
}
|
||||
}
|
||||
if (projectId && routeProjectId !== projectId) fetchCycles(workspaceSlug?.toString(), projectId);
|
||||
|
||||
@@ -195,6 +187,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workItemTemplateId && editorRef.current) {
|
||||
handleTemplateChange({
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
reset,
|
||||
editorRef,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workItemTemplateId]);
|
||||
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>, is_draft_issue = false) => {
|
||||
// Check if the editor is ready to discard
|
||||
if (!editorRef.current?.isEditorReadyToDiscard()) {
|
||||
@@ -233,7 +236,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
.then(() => {
|
||||
setGptAssistantModal(false);
|
||||
reset({
|
||||
...defaultValues,
|
||||
...DEFAULT_WORK_ITEM_FORM_VALUES,
|
||||
...(isCreateMoreToggleEnabled ? { ...data } : {}),
|
||||
project_id: getValues<"project_id">("project_id"),
|
||||
type_id: getValues<"type_id">("type_id"),
|
||||
@@ -262,7 +265,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
...data,
|
||||
...getValues(),
|
||||
} as TWorkspaceDraftIssue);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
@@ -308,16 +311,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
|
||||
setSelectedParentIssue({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
project_id: issue.project_id,
|
||||
project__identifier: projectDetails.identifier,
|
||||
project__name: projectDetails.name,
|
||||
sequence_id: issue.sequence_id,
|
||||
type_id: issue.type_id,
|
||||
state__color: stateDetails?.color,
|
||||
} as ISearchIssueResponse);
|
||||
setSelectedParentIssue(
|
||||
convertWorkItemDataToSearchResponse(workspaceSlug?.toString(), issue, projectDetails, stateDetails)
|
||||
);
|
||||
}, [watch, getIssueById, getProjectById, selectedParentIssue, getStateById]);
|
||||
|
||||
// executing this useEffect when isDirty changes
|
||||
@@ -351,7 +347,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const shouldRenderDuplicateModal = isDuplicateModalOpen && duplicateIssues?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
{projectId && (
|
||||
<CreateLabelModal
|
||||
isOpen={labelModal}
|
||||
@@ -383,11 +379,20 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
<IssueTypeSelect
|
||||
control={control}
|
||||
projectId={projectId}
|
||||
editorRef={editorRef}
|
||||
disabled={!!data?.sourceIssueId}
|
||||
handleFormChange={handleFormChange}
|
||||
renderChevron
|
||||
/>
|
||||
)}
|
||||
{projectId && !data?.id && !data?.sourceIssueId && (
|
||||
<WorkItemTemplateSelect
|
||||
projectId={projectId}
|
||||
typeId={watch("type_id")}
|
||||
handleFormChange={handleFormChange}
|
||||
renderChevron
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{duplicateIssues.length > 0 && (
|
||||
<DeDupeButtonRoot
|
||||
@@ -416,7 +421,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
<IssueTitleInput
|
||||
control={control}
|
||||
issueTitleRef={issueTitleRef}
|
||||
errors={errors}
|
||||
formState={formState}
|
||||
handleFormChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -526,6 +531,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
size="sm"
|
||||
ref={submitBtnRef}
|
||||
loading={isSubmitting}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
|
||||
</Button>
|
||||
@@ -562,6 +568,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</FormProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
// plane imports
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import type { TIssue } from "@plane/types";
|
||||
// components
|
||||
import { CreateUpdateIssueModalBase } from "@/components/issues";
|
||||
// constants
|
||||
// plane web providers
|
||||
// plane web imports
|
||||
import { IssueModalProvider } from "@/plane-web/components/issues";
|
||||
|
||||
export interface IssuesModalProps {
|
||||
@@ -28,12 +27,13 @@ export interface IssuesModalProps {
|
||||
loading: string;
|
||||
};
|
||||
isProjectSelectionDisabled?: boolean;
|
||||
templateId?: string;
|
||||
}
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
(props) =>
|
||||
props.isOpen && (
|
||||
<IssueModalProvider>
|
||||
<IssueModalProvider templateId={props.templateId}>
|
||||
<CreateUpdateIssueModalBase {...props} />
|
||||
</IssueModalProvider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { usePopper } from "react-popper";
|
||||
@@ -24,7 +25,9 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
createLabelEnabled?: boolean;
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
placement?: Placement;
|
||||
};
|
||||
|
||||
export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
@@ -37,7 +40,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
disabled = false,
|
||||
tabIndex,
|
||||
createLabelEnabled = false,
|
||||
buttonContainerClassName,
|
||||
buttonClassName,
|
||||
placement,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
// router
|
||||
@@ -55,7 +60,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
placement: placement ?? "bottom-start",
|
||||
});
|
||||
|
||||
const projectLabels = getProjectLabels(projectId);
|
||||
@@ -115,13 +120,16 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
className={cn("h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200", buttonClassName)}
|
||||
className={cn(
|
||||
"h-full flex cursor-pointer items-center gap-2 text-xs text-custom-text-200",
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{label ? (
|
||||
label
|
||||
) : value && value.length > 0 ? (
|
||||
<span className="flex items-center justify-center gap-2 text-xs h-full">
|
||||
<span className={cn("flex items-center justify-center gap-2 text-xs h-full", buttonClassName)}>
|
||||
<IssueLabelsList
|
||||
labels={value.map((v) => projectLabels?.find((l) => l.id === v)) ?? []}
|
||||
length={3}
|
||||
@@ -129,7 +137,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80">
|
||||
<div className={cn("h-full flex items-center justify-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs hover:bg-custom-background-80", buttonClassName)}>
|
||||
<Tag className="h-3 w-3 flex-shrink-0" />
|
||||
<span>{t("labels")}</span>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,13 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 px-4 pb-4 pt-5 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Create Label
|
||||
|
||||
@@ -26,11 +26,9 @@ import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPer
|
||||
import { TProject } from "@/plane-web/types";
|
||||
|
||||
export const SidebarProjectsList: FC = observer(() => {
|
||||
// get local storage data for isFavoriteProjectsListOpen and isAllProjectsListOpen
|
||||
const isAllProjectsListOpenInLocalStorage = localStorage.getItem("isAllProjectsListOpen");
|
||||
// states
|
||||
|
||||
const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(isAllProjectsListOpenInLocalStorage === "true");
|
||||
const [isAllProjectsListOpen, setIsAllProjectsListOpen] = useState(true);
|
||||
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false); // scroll animation state
|
||||
// refs
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user