diff --git a/web/components/custom-attributes/attribute-display/file.tsx b/web/components/custom-attributes/attribute-display/file.tsx index e2929131b..023902637 100644 --- a/web/components/custom-attributes/attribute-display/file.tsx +++ b/web/components/custom-attributes/attribute-display/file.tsx @@ -1,17 +1,129 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +// react-dropzone +import { useDropzone } from "react-dropzone"; +// services +import fileService from "services/file.service"; +// hooks +import useToast from "hooks/use-toast"; +// icons +import { getFileIcon } from "components/icons"; +// helpers +import { getFileExtension, getFileName } from "helpers/attachment.helper"; // types import { Props } from "./types"; +import useWorkspaceDetails from "hooks/use-workspace-details"; -export const CustomFileAttribute: React.FC = ({ - attributeDetails, - onChange, - value, -}) => ( -
- -
-); +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB + +export const CustomFileAttribute: React.FC = (props) => { + const { attributeDetails, onChange, value } = props; + + const [isUploading, setIsUploading] = useState(false); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { workspaceDetails } = useWorkspaceDetails(); + + const { setToastAlert } = useToast(); + + const onDrop = useCallback( + (acceptedFiles: File[]) => { + if (!acceptedFiles[0] || !workspaceSlug) return; + + const extension = getFileExtension(acceptedFiles[0].name); + + if (!attributeDetails.extra_settings?.file_formats?.includes(`.${extension}`)) { + setToastAlert({ + type: "error", + title: "Error!", + message: `File format not accepted. Accepted file formats- ${attributeDetails.extra_settings?.file_formats?.join( + ", " + )}`, + }); + return; + } + + const formData = new FormData(); + formData.append("asset", acceptedFiles[0]); + formData.append( + "attributes", + JSON.stringify({ + name: acceptedFiles[0].name, + size: acceptedFiles[0].size, + }) + ); + setIsUploading(true); + + fileService + .uploadFile(workspaceSlug.toString(), formData) + .then((res) => { + const imageUrl = res.asset; + + onChange(imageUrl); + + if (value && value !== "" && workspaceDetails) + fileService.deleteFile(workspaceDetails.id, value); + }) + .finally(() => setIsUploading(false)); + }, + [ + attributeDetails.extra_settings?.file_formats, + onChange, + setToastAlert, + value, + workspaceDetails, + workspaceSlug, + ] + ); + + const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({ + onDrop, + maxSize: MAX_FILE_SIZE, + multiple: false, + disabled: isUploading, + }); + + const fileError = + fileRejections.length > 0 + ? `Invalid file type or size (max ${MAX_FILE_SIZE / 1024 / 1024} MB)` + : null; + + return ( +
+ {value && value !== "" && ( + + {getFileIcon(getFileExtension(value))} + {getFileName(value)} + + )} +
+ + + {isDragActive ? ( +

Drop here...

+ ) : fileError ? ( +

{fileError}

+ ) : isUploading ? ( +

Uploading...

+ ) : ( +

Upload {value && value !== "" ? "new " : ""}file

+ )} +
+
+
+ ); +}; diff --git a/web/components/custom-attributes/attributes-list/index.ts b/web/components/custom-attributes/attributes-list/index.ts new file mode 100644 index 000000000..1906cd1f2 --- /dev/null +++ b/web/components/custom-attributes/attributes-list/index.ts @@ -0,0 +1,3 @@ +export * from "./issue-modal-attributes-list"; +export * from "./peek-overview-custom-attributes-list"; +export * from "./sidebar-custom-attributes-list"; diff --git a/web/components/issues/custom-attributes-list.tsx b/web/components/custom-attributes/attributes-list/issue-modal-attributes-list.tsx similarity index 97% rename from web/components/issues/custom-attributes-list.tsx rename to web/components/custom-attributes/attributes-list/issue-modal-attributes-list.tsx index fdcf2f46a..cf19c77cb 100644 --- a/web/components/issues/custom-attributes-list.tsx +++ b/web/components/custom-attributes/attributes-list/issue-modal-attributes-list.tsx @@ -22,7 +22,7 @@ type Props = { projectId: string; }; -export const CustomAttributesList: React.FC = observer( +export const IssueModalCustomAttributesList: React.FC = observer( ({ entityId, issueId, onSubmit, projectId }) => { const router = useRouter(); const { workspaceSlug } = router.query; diff --git a/web/components/custom-attributes/attributes-list/peek-overview-custom-attributes-list.tsx b/web/components/custom-attributes/attributes-list/peek-overview-custom-attributes-list.tsx new file mode 100644 index 000000000..bf9028581 --- /dev/null +++ b/web/components/custom-attributes/attributes-list/peek-overview-custom-attributes-list.tsx @@ -0,0 +1,189 @@ +import { useEffect } from "react"; + +import { useRouter } from "next/router"; + +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { observer } from "mobx-react-lite"; +// components +import { + CustomCheckboxAttribute, + CustomDateTimeAttribute, + CustomEmailAttribute, + CustomNumberAttribute, + CustomRelationAttribute, + CustomSelectAttribute, + CustomTextAttribute, + CustomUrlAttribute, +} from "components/custom-attributes"; +// ui +import { Loader } from "components/ui"; +// types +import { ICustomAttributeValueFormData, IIssue } from "types"; +// constants +import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes"; + +type Props = { + issue: IIssue | undefined; +}; + +export const PeekOverviewCustomAttributesList: React.FC = observer(({ issue }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + customAttributes: customAttributesStore, + customAttributeValues: customAttributeValuesStore, + } = useMobxStore(); + const { entityAttributes, fetchEntityDetails } = customAttributesStore; + const { issueAttributeValues, fetchIssueAttributeValues } = customAttributeValuesStore; + + const handleAttributeUpdate = (attributeId: string, value: string) => { + if (!issue || !workspaceSlug) return; + + const payload: ICustomAttributeValueFormData = { + issue_properties: { + [attributeId]: value, + }, + }; + + customAttributeValuesStore.createAttributeValue( + workspaceSlug.toString(), + issue.project, + issue.id, + payload + ); + }; + + // fetch the object details if object state has id + useEffect(() => { + if (!issue?.entity) return; + + if (!entityAttributes[issue.entity]) { + if (!workspaceSlug) return; + + fetchEntityDetails(workspaceSlug.toString(), issue.entity); + } + }, [issue?.entity, entityAttributes, fetchEntityDetails, workspaceSlug]); + + // fetch issue attribute values + useEffect(() => { + if (!issue) return; + + if (!issueAttributeValues || !issueAttributeValues[issue.id]) { + if (!workspaceSlug) return; + + fetchIssueAttributeValues(workspaceSlug.toString(), issue.project, issue.id); + } + }, [fetchIssueAttributeValues, issue, issueAttributeValues, workspaceSlug]); + + if (!issue || !issue?.entity) return null; + + if (!entityAttributes[issue.entity] || !issueAttributeValues?.[issue.id]) + return ( + + + + + + + ); + + return ( + <> + {Object.values(entityAttributes?.[issue.entity] ?? {}).map((attribute) => { + const typeMetaData = CUSTOM_ATTRIBUTES_LIST[attribute.type]; + const attributeValue = issueAttributeValues?.[issue.id].find( + (a) => a.id === attribute.id + )?.prop_value; + + return ( +
+
+ +

{attribute.display_name}

+
+
+ {attribute.type === "checkbox" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={ + attributeValue ? (attributeValue?.[0]?.value === "true" ? true : false) : false + } + /> + )} + {attribute.type === "datetime" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? new Date(attributeValue?.[0]?.value ?? "") : undefined} + /> + )} + {attribute.type === "email" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0]?.value : undefined} + /> + )} + {attribute.type === "number" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={ + attributeValue ? parseInt(attributeValue?.[0]?.value ?? "0", 10) : undefined + } + /> + )} + {attribute.type === "relation" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0]?.value : undefined} + /> + )} + {attribute.type === "select" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0]?.value : undefined} + /> + )} + {attribute.type === "text" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0].value : undefined} + /> + )} + {attribute.type === "url" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0]?.value : undefined} + /> + )} +
+
+ ); + })} + + ); +}); diff --git a/web/components/issues/sidebar-custom-attributes-list.tsx b/web/components/custom-attributes/attributes-list/sidebar-custom-attributes-list.tsx similarity index 93% rename from web/components/issues/sidebar-custom-attributes-list.tsx rename to web/components/custom-attributes/attributes-list/sidebar-custom-attributes-list.tsx index 683aeb5cd..5dd243a23 100644 --- a/web/components/issues/sidebar-custom-attributes-list.tsx +++ b/web/components/custom-attributes/attributes-list/sidebar-custom-attributes-list.tsx @@ -10,17 +10,19 @@ import { CustomCheckboxAttribute, CustomDateTimeAttribute, CustomEmailAttribute, + CustomFileAttribute, CustomNumberAttribute, CustomRelationAttribute, CustomSelectAttribute, CustomTextAttribute, CustomUrlAttribute, } from "components/custom-attributes"; +// ui +import { Loader } from "components/ui"; // types import { ICustomAttributeValueFormData, IIssue } from "types"; // constants import { CUSTOM_ATTRIBUTES_LIST } from "constants/custom-attributes"; -import { Loader } from "components/ui"; type Props = { issue: IIssue | undefined; @@ -132,6 +134,15 @@ export const SidebarCustomAttributesList: React.FC = observer(({ issue }) value={attributeValue ? attributeValue?.[0]?.value : undefined} /> )} + {attribute.type === "file" && ( + handleAttributeUpdate(attribute.id, val)} + projectId={issue.project} + value={attributeValue ? attributeValue?.[0]?.value : undefined} + /> + )} {attribute.type === "number" && ( = ({ onChange, value }) => { { - console.log(val); - onChange(val); - }} + onChange={(val) => onChange(val)} className="relative flex-shrink-0 text-left" multiple > {({ open }: { open: boolean }) => ( <> - All Formats + {value.length > 0 ? value.join(", ") : "Select file formats"} = ({ )} ) : ( - {}} projectId={projectId} /> )} diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 614292e54..a21afbbc4 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -6,14 +6,12 @@ export * from "./peek-overview"; export * from "./sidebar-select"; export * from "./view-select"; export * from "./activity"; -export * from "./custom-attributes-list"; export * from "./delete-issue-modal"; export * from "./description-form"; export * from "./form"; export * from "./main-content"; export * from "./modal"; export * from "./parent-issues-list-modal"; -export * from "./sidebar-custom-attributes-list" export * from "./sidebar"; export * from "./sub-issues-list"; export * from "./label"; diff --git a/web/components/issues/peek-overview/issue-properties.tsx b/web/components/issues/peek-overview/issue-properties.tsx index d001634fc..f17579247 100644 --- a/web/components/issues/peek-overview/issue-properties.tsx +++ b/web/components/issues/peek-overview/issue-properties.tsx @@ -1,5 +1,3 @@ -// mobx -import { observer } from "mobx-react-lite"; // headless ui import { Disclosure } from "@headlessui/react"; import { StateGroupIcon } from "components/icons"; @@ -14,12 +12,13 @@ import { SidebarStateSelect, TPeekOverviewModes, } from "components/issues"; +import { PeekOverviewCustomAttributesList } from "components/custom-attributes"; // ui import { CustomDatePicker, Icon } from "components/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IIssue, TIssuePriorities } from "types"; +import { IIssue } from "types"; type Props = { handleDeleteIssue: () => void; @@ -96,72 +95,77 @@ export const PeekOverviewIssueProperties: React.FC = ({ /> -
-
- - Assignees -
-
- handleUpdateIssue({ assignees_list: val })} - disabled={readOnly} - /> -
-
-
-
- - Priority -
-
- handleUpdateIssue({ priority: val })} - disabled={readOnly} - /> -
-
-
-
- - Start date -
-
- - handleUpdateIssue({ - start_date: val, - }) - } - className="bg-custom-background-80 border-none" - maxDate={maxDate ?? undefined} - disabled={readOnly} - /> -
-
-
-
- - Due date -
-
- - handleUpdateIssue({ - target_date: val, - }) - } - className="bg-custom-background-80 border-none" - minDate={minDate ?? undefined} - disabled={readOnly} - /> -
-
+ {issue.entity === null && ( + <> +
+
+ + Assignees +
+
+ handleUpdateIssue({ assignees_list: val })} + disabled={readOnly} + /> +
+
+
+
+ + Priority +
+
+ handleUpdateIssue({ priority: val })} + disabled={readOnly} + /> +
+
+
+
+ + Start date +
+
+ + handleUpdateIssue({ + start_date: val, + }) + } + className="bg-custom-background-80 border-none" + maxDate={maxDate ?? undefined} + disabled={readOnly} + /> +
+
+
+
+ + Due date +
+
+ + handleUpdateIssue({ + target_date: val, + }) + } + className="bg-custom-background-80 border-none" + minDate={minDate ?? undefined} + disabled={readOnly} + /> +
+
+ + )} + {issue.entity !== null && } {/*
diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index b4427fe95..6f50edc89 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -32,8 +32,8 @@ import { SidebarLabelSelect, SidebarDuplicateSelect, SidebarRelatesSelect, - SidebarCustomAttributesList, } from "components/issues"; +import { SidebarCustomAttributesList } from "components/custom-attributes"; // ui import { CustomDatePicker, Icon } from "components/ui"; // icons @@ -56,7 +56,6 @@ import { copyTextToClipboard } from "helpers/string.helper"; import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types"; // fetch-keys import { ISSUE_DETAILS } from "constants/fetch-keys"; -import { ObjectsSelect } from "components/custom-attributes"; type Props = { control: any; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index e4a1c37b2..b2f931f17 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -180,7 +180,7 @@ const ArchivedIssueDetailsPage: NextPage = () => {
)} -
+
{ /> ) : issueDetails && projectId ? (
-
+