dev: promote stage release to production (#155)
* refractor: removed modules from user.context * refractor: removed cycles from user context * refractor: removed state from user context * feat: implement channel protocol for tracking issue-activites * refactor: remove blocking code and add todo * refactor: refactor the consumer with function modules * feat: add columns for identifiers for easier redirection * style: minor padding, coloring and consistency changes * feat: track blocker issues * feat: track issue after creation * feat: add runworker in procfile * refractor: moved all context provider to _app for more clarity * dev: added our icons * refractor: removed issues from user context * refactor: rename db names to plural and remove admin register file * refactor: integrate permission layer in endpoints * feat: create product email html templates * refractor: changed to getServerSide from getInitialProps, removed unused component imports and minor refractoring * feat: remirror added * feat: workspace member user details endpoint * fix: resolved build issue * refactor: remove www * feat: workspace details on user endpoint * feat: added authorization in project settings refractor: improved code readability * fix: removed hard-coded workspace slug value, and added workspace in user interface * refactor: invitation workflow for already existing users * feat: modified remirror, fix: issue details sidebar * fix: merge conflicts * fix: merge conflicts * fix: added missing dependencies * refactor: remove user dependency from invitations * refactor: issue description context is updated with manager * dev: redis instance rewrite for ssl settings and remove REDIS_TLS env variable * chore: upgrade python package requirements * dev: added new migrations for changes * dev: ssl config for django channels redis connection * chore: upgrade channels requirements * refactor: better function for connecting with redis ssl django channels * chore: cleanup on manifest file * revert: user endpoint changes * build: setup asgi * refactor: update invitation endpoint to do bulk operations * style: cycles page, custom listbox, issue details page * refractor: removed folder that were moved to workspaceSlug * dev: uvicorn in requirements * Update index.tsx * refactor: get workspace slug on user endpoint * fix: workspace slug redirections and slug value in user context * fix: user context bugs, drag and drop in cycles and modules * fix: merge conflicts * fix: user context and create issue modal * refactor: add extra columns for json and html description and script for back migrating old issues * refactor: move all 500 errors to 400 * refractor: removed active project, active workspace, projects, and workspaces from user context * refractor: change from /home to /, added home page redirection logic added explict GET method on fetch request, and fixed invitation page not fetching all invitations * fix: passing project id in command palette * style: home page, feat: image in remirror * fix: bugs * chore: remove test_runner workflow from github actions * dev: update Procfile worker count and python runtime upgrade * refactor: update response from 404 to 403 * feat: filtering using both name and issue identifier in command palette showing my issues instead of project issue in command palette, hiding again according to route in command palette * fix: mutation on different CRUD operations * fix: redirection in my issues pages * feat: added authorization in workspace settings, moved command palette to app-layout * feat: endpoint and column to store my issue props * style: authorization new design, fix: made whole button on authorization page clickable, lib/auth on unsuccessful api call redirecting to error page * feat: return project details on modules and cycles * fix: create cycle and state coming below issue modal, showing loader for rich text editor refractor: changed from sprint to cycle in issue type * fix: issue delete mustation and some code refractor * fix: mutation bugs, remirror bugs, style: consistent droopdowns and buttons * feat: user role in model * dev: added new migrations * fix: add url for workspace availability check * feat: onboarding screens * fix: update url for workspace name check and add authentication layer and fix invitation endpoint * refactor: bulk invitations message * refactor: response on workspace invitarions * refactor: update identifier endpoint * refactor: invitations endpoint * feat: onboarding logic and validations * fix: email striep * dev: added workspace space member unique_together * chore: back populate neccesary data for description field * feat: emoji-picker gets close on select, public will be default option in create project * fix: update error in project creation * fix: mutation error on issue count in kanban view some minor code refractoring * fix: module bugs * fix: issue activities and issue comments mutation handled at issue detail * fix: error message for creating updates without permissions * fix: showing no user left to invite in project invite fix: - mutation in project settings control, style: - showing loader in project settings controller, - showing request pending for user that hasn't accepted invitation * refactor: file asset upload directory * fix: update last workspace id on user invitation accept * style: onboarding screens * style: cycles, issue activity * feat: add json and html column in issue comments * fix: submitting create issue modal on enter click, project not getting deselected * feat: file size validator * fix: emoji picker not closing on all emoji select * feat: added validation in identifier such that it only accept uppercase text * dev: commenting is now richer * fix: shortcuts not getting opened in settings layouts * style: showing sidebar on unauthorized pages * fix: error code on exception * fix: add issue button is working on my issues pages * feat: new way of assets * fix: updated activity content for description field * fix: mutation on project settings control style: blocker and blocked changed to outline button * fix: description activity logging * refactor: check for workspace slug on workspace creation * fix: typo on workspace url check * fix: workspace name uniqueness * fix: remove workspace from read only field * fix: file upload endpoint, workspace slug check * chore: drop unique_together constraint for name and workspace * chore: settings files cleanup and use PubSub backend on django channels * chore: change in channels backend * refactor: issue activity api to combine comments * fix: instance created at key * fix: result list * style: create project, cycle modal, view dropdown * feat: merged issue activities and issue comments into a single section * fix: remirror dynamic update of issue description * fix: removed commented code * fix: issue acitivties mutation * fix: empty comments cant be submitted * fix: workspace avatar has been updated while loading * refactor: update docker-compose to run redis and database in heroku and docker environment * refactor: removesingle docker file configuration * refactor: update take off script to run in asgi * docs: added workspace, quickstart documentation * fix: reading editor values on focus out * refactor: cleanup environment variables and create .env.example * refactor: add extra variables in example env * fix: warning and erros on console lazy loading images with low priority, added validation on onboarding for user to either join or create workspace, on onboarding user can't click button while form is getting submitted, profile page going into loading state when updated, refractor: made some state local, removed unnecessary console logs and comments, changed some variable and function name to make more sence * feat: env examples * fix: workspace member does not exist * fi: remove pagination from issue list api * refactor: remove env example from root * feat: documentation for projects on plane * feat: create code of conduct and contributing guidelines * fix: update docker setup to check handle redis * revert: bring back pagination to avoid breaking * feat: made image uploader modal, used it in profile page and workspace page, delete project from project settings page, join project modal in project list page * feat: create workspace page, style: made ui consistent * style: updated onboarding and create workspace page design * style: responsive sidebar * fix: updated ui imports
This commit is contained in:
parent
a960ddedf7
commit
bef166a65f
395 changed files with 20119 additions and 18322 deletions
|
|
@ -1,30 +1,33 @@
|
|||
import React, { useState } from "react";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
import dynamic from "next/dynamic";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// react hook form
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { useForm, Controller, UseFormWatch } from "react-hook-form";
|
||||
|
||||
import { TwitterPicker } from "react-color";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// fetching keys
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
// commons
|
||||
import { copyTextToClipboard } from "constants/common";
|
||||
// components
|
||||
import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion";
|
||||
import SelectState from "components/project/issues/issue-detail/issue-detail-sidebar/select-state";
|
||||
import SelectPriority from "components/project/issues/issue-detail/issue-detail-sidebar/select-priority";
|
||||
import SelectParent from "components/project/issues/issue-detail/issue-detail-sidebar/select-parent";
|
||||
import SelectCycle from "components/project/issues/issue-detail/issue-detail-sidebar/select-cycle";
|
||||
import SelectAssignee from "components/project/issues/issue-detail/issue-detail-sidebar/select-assignee";
|
||||
import SelectBlocker from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocker";
|
||||
import SelectBlocked from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocked";
|
||||
// headless ui
|
||||
import { Popover, Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Input, Button, Spinner } from "ui";
|
||||
import { Popover } from "@headlessui/react";
|
||||
// icons
|
||||
import {
|
||||
TagIcon,
|
||||
ChevronDownIcon,
|
||||
ClipboardDocumentIcon,
|
||||
LinkIcon,
|
||||
CalendarDaysIcon,
|
||||
TrashIcon,
|
||||
|
|
@ -33,16 +36,11 @@ import {
|
|||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { Control } from "react-hook-form";
|
||||
import type { ICycle, IIssue, IIssueLabels, NestedKeyOf } from "types";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import { positionEditorElement } from "components/lexical/helpers/editor";
|
||||
import SelectState from "./select-state";
|
||||
import SelectPriority from "./select-priority";
|
||||
import SelectParent from "./select-parent";
|
||||
import SelectCycle from "./select-cycle";
|
||||
import SelectAssignee from "./select-assignee";
|
||||
import SelectBlocker from "./select-blocker";
|
||||
import SelectBlocked from "./select-blocked";
|
||||
import type { ICycle, IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// common
|
||||
import { copyTextToClipboard } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
|
|
@ -63,19 +61,28 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
watch: watchIssue,
|
||||
}) => {
|
||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const { activeWorkspace, activeProject, issues } = useUser();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||
activeProject && activeWorkspace
|
||||
? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id)
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
|
|
@ -89,49 +96,51 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
});
|
||||
|
||||
const handleNewLabel = (formData: any) => {
|
||||
if (!activeWorkspace || !activeProject || isSubmitting) return;
|
||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||
issuesServices
|
||||
.createIssueLabel(activeWorkspace.slug, activeProject.id, formData)
|
||||
.createIssueLabel(workspaceSlug as string, projectId as string, formData)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
reset(defaultValues);
|
||||
issueLabelMutate((prevData) => [...(prevData ?? []), res], false);
|
||||
submitChanges({ labels_list: [res.id] });
|
||||
submitChanges({ labels_list: [...(issueDetail?.labels ?? []), res.id] });
|
||||
setCreateLabelForm(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCycleChange = (cycleDetail: ICycle) => {
|
||||
if (activeWorkspace && activeProject && issueDetail) {
|
||||
submitChanges({ cycle: cycleDetail.id, cycle_detail: cycleDetail });
|
||||
issuesServices
|
||||
.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleDetail.id, {
|
||||
issues: [issueDetail.id],
|
||||
})
|
||||
.then(() => {
|
||||
submitChanges({});
|
||||
});
|
||||
}
|
||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
|
||||
issuesServices.addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetail.id, {
|
||||
issues: [issueDetail.id],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmIssueDeletion
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueDetail}
|
||||
/>
|
||||
<div className="h-full w-full divide-y-2 divide-gray-100">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<div className="flex items-center justify-between pb-3">
|
||||
<h4 className="text-sm font-medium">
|
||||
{activeProject?.identifier}-{issueDetail?.sequence_id}
|
||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||
className="rounded-md border p-2 shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
|
||||
`https://app.plane.so/${workspaceSlug}/projects/${issueDetail?.project_detail?.id}/issues/${issueDetail?.id}`
|
||||
)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Copied to clipboard",
|
||||
title: "Issue link copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -146,28 +155,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||
onClick={() =>
|
||||
copyTextToClipboard(issueDetail?.id ?? "")
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Some error occurred",
|
||||
});
|
||||
})
|
||||
}
|
||||
>
|
||||
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-red-50 text-red-500 border border-red-500 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-50 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() => setDeleteIssueModal(true)}
|
||||
>
|
||||
<TrashIcon className="h-3.5 w-3.5" />
|
||||
|
|
@ -196,14 +184,14 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
issueDetail?.parent_detail ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 bg-gray-100 px-3 py-2 text-xs rounded"
|
||||
className="flex items-center gap-2 rounded bg-gray-100 px-3 py-2 text-xs"
|
||||
onClick={() => submitChanges({ parent: null })}
|
||||
>
|
||||
{issueDetail.parent_detail?.name}
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="inline-block bg-gray-100 px-3 py-2 text-xs rounded">
|
||||
<div className="inline-block rounded bg-gray-100 px-3 py-2 text-xs">
|
||||
No parent selected
|
||||
</div>
|
||||
)
|
||||
|
|
@ -216,13 +204,13 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
watch={watchIssue}
|
||||
/>
|
||||
<SelectBlocked
|
||||
issueDetail={issueDetail}
|
||||
submitChanges={submitChanges}
|
||||
issuesList={issues?.results.filter((i) => i.id !== issueDetail?.id) ?? []}
|
||||
watch={watchIssue}
|
||||
/>
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<CalendarDaysIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<CalendarDaysIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
|
|
@ -238,7 +226,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
submitChanges({ target_date: e.target.value });
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
className="w-full cursor-pointer rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -246,17 +234,22 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<SelectCycle control={control} handleCycleChange={handleCycleChange} />
|
||||
<SelectCycle
|
||||
issueDetail={issueDetail}
|
||||
control={control}
|
||||
handleCycleChange={handleCycleChange}
|
||||
watch={watchIssue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-3 space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-x-2 text-sm basis-1/2">
|
||||
<TagIcon className="w-4 h-4" />
|
||||
<div className="space-y-3 pt-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex basis-1/2 items-center gap-x-2 text-sm">
|
||||
<TagIcon className="h-4 w-4" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div className="basis-1/2">
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{watchIssue("labels_list")?.map((label) => {
|
||||
const singleLabel = issueLabels?.find((l) => l.id === label);
|
||||
|
||||
|
|
@ -265,7 +258,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
return (
|
||||
<span
|
||||
key={singleLabel.id}
|
||||
className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer"
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border px-1 py-0.5 text-xs hover:border-red-500 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
const updatedLabels = watchIssue("labels_list")?.filter((l) => l !== label);
|
||||
submitChanges({
|
||||
|
|
@ -274,7 +267,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: singleLabel.colour ?? "green" }}
|
||||
></span>
|
||||
{singleLabel.name}
|
||||
|
|
@ -297,7 +290,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
<>
|
||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="flex items-center gap-2 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer">
|
||||
<Listbox.Button className="flex cursor-pointer items-center gap-2 rounded-2xl border px-2 py-0.5 text-xs hover:bg-gray-100">
|
||||
Select Label
|
||||
</Listbox.Button>
|
||||
|
||||
|
|
@ -308,7 +301,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{issueLabels ? (
|
||||
issueLabels.length > 0 ? (
|
||||
|
|
@ -318,12 +311,12 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "bg-indigo-50" : ""
|
||||
} flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate`
|
||||
} relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: label.colour ?? "green" }}
|
||||
></span>
|
||||
{label.name}
|
||||
|
|
@ -346,7 +339,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 border rounded-2xl text-xs px-2 py-0.5 hover:bg-gray-100 cursor-pointer"
|
||||
className="flex cursor-pointer items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs hover:bg-gray-100"
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
|
|
@ -369,11 +362,11 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`bg-white flex items-center gap-1 rounded-md p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
||||
className={`flex items-center gap-1 rounded-md bg-white p-1 outline-none focus:ring-2 focus:ring-indigo-500`}
|
||||
>
|
||||
{watch("colour") && watch("colour") !== "" && (
|
||||
<span
|
||||
className="w-5 h-5 rounded"
|
||||
className="h-5 w-5 rounded"
|
||||
style={{
|
||||
backgroundColor: watch("colour") ?? "green",
|
||||
}}
|
||||
|
|
@ -391,7 +384,7 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute z-10 transform right-0 mt-1 px-2 max-w-xs sm:px-0">
|
||||
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
|
||||
<Controller
|
||||
name="colour"
|
||||
control={controlLabel}
|
||||
|
|
@ -418,18 +411,16 @@ const IssueDetailSidebar: React.FC<Props> = ({
|
|||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button type="submit" theme="danger" onClick={() => setCreateLabelForm(false)}>
|
||||
<XMarkIcon className="h-4 w-4 text-white" />
|
||||
</Button>
|
||||
<Button type="submit" theme="success" disabled={isSubmitting}>
|
||||
+
|
||||
<PlusIcon className="h-4 w-4 text-white" />
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmIssueDeletion
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issueDetail}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// react
|
||||
import React from "react";
|
||||
// next
|
||||
|
||||
import Image from "next/image";
|
||||
// swr
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
// react-hook-form
|
||||
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// services
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
|
|
@ -29,17 +29,18 @@ type Props = {
|
|||
};
|
||||
|
||||
const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const { activeWorkspace } = useUser();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: people } = useSWR(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
|
||||
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<UserGroupIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
|
|
@ -58,14 +59,14 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button className="w-full flex items-center gap-1 text-xs cursor-pointer">
|
||||
<Listbox.Button className="flex w-full cursor-pointer items-center gap-1 text-xs">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block text-left"
|
||||
"hidden truncate text-left sm:block"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs cursor-pointer">
|
||||
<div className="flex cursor-pointer items-center gap-1 text-xs">
|
||||
{value && Array.isArray(value) ? (
|
||||
<>
|
||||
{value.length > 0 ? (
|
||||
|
|
@ -82,7 +83,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
}`}
|
||||
>
|
||||
{person && person.avatar && person.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={person.avatar}
|
||||
height="100%"
|
||||
|
|
@ -93,7 +94,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
className={`grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 text-white`}
|
||||
>
|
||||
{person?.first_name.charAt(0)}
|
||||
</div>
|
||||
|
|
@ -102,7 +103,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
|
||||
<Image
|
||||
src={User}
|
||||
height="100%"
|
||||
|
|
@ -128,7 +129,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 left-0 mt-1 w-auto bg-white shadow-lg max-h-48 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 max-h-48 w-auto overflow-auto rounded-md bg-white py-1 text-xs shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{people ? (
|
||||
people.length > 0 ? (
|
||||
|
|
@ -138,7 +139,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "bg-indigo-50" : ""
|
||||
} flex items-center gap-2 text-gray-900 cursor-pointer select-none p-2 truncate`
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900`
|
||||
}
|
||||
value={option.member.id}
|
||||
>
|
||||
|
|
@ -153,7 +154,7 @@ const SelectAssignee: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0 h-4 w-4 bg-gray-700 text-white grid place-items-center capitalize rounded-full">
|
||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 capitalize text-white">
|
||||
{option.member.first_name && option.member.first_name !== ""
|
||||
? option.member.first_name.charAt(0)
|
||||
: option.member.email.charAt(0)}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,58 @@
|
|||
// react
|
||||
import React, { useState } from "react";
|
||||
// react-hook-form
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
|
||||
// services
|
||||
import issuesService from "lib/services/issues.service";
|
||||
// constants
|
||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import { FolderIcon, MagnifyingGlassIcon, FlagIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { BlockedIcon } from "ui/icons";
|
||||
// types
|
||||
import { IIssue, IssueResponse } from "types";
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
import { classNames } from "constants/common";
|
||||
import issuesService from "lib/services/issues.service";
|
||||
|
||||
type FormInput = {
|
||||
issue_ids: string[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
issuesList: IIssue[];
|
||||
watch: UseFormWatch<IIssue>;
|
||||
};
|
||||
|
||||
const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
||||
const SelectBlocked: React.FC<Props> = ({ submitChanges, issuesList, watch }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
|
||||
const { activeWorkspace, activeProject, issues, mutateIssues } = useUser();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { register, handleSubmit, reset, watch: watchIssues } = useForm<FormInput>();
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { register, handleSubmit, reset, watch: watchBlocked } = useForm<FormInput>();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsBlockedModalOpen(false);
|
||||
|
|
@ -51,86 +69,50 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
data.issue_ids.map((issue) => {
|
||||
if (!activeWorkspace || !activeProject || !issueDetail) return;
|
||||
|
||||
const currentBlockers =
|
||||
issues?.results
|
||||
.find((i) => i.id === issue)
|
||||
?.blocker_issues.map((b) => b.blocker_issue_detail?.id ?? "") ?? [];
|
||||
|
||||
issuesService
|
||||
.patchIssue(activeWorkspace.slug, activeProject.id, issue, {
|
||||
blockers_list: [...currentBlockers, issueDetail.id],
|
||||
})
|
||||
.then((response) => {
|
||||
mutateIssues((prevData) => ({
|
||||
...(prevData as IssueResponse),
|
||||
results: (prevData?.results ?? []).map((issue) => {
|
||||
if (issue.id === issueDetail.id) {
|
||||
return { ...issue, ...response };
|
||||
}
|
||||
return issue;
|
||||
}),
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
});
|
||||
if (!Array.isArray(data.issue_ids)) data.issue_ids = [data.issue_ids];
|
||||
|
||||
const newBlocked = [...watch("blocked_list"), ...data.issue_ids];
|
||||
submitChanges({ blocks_list: newBlocked });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const removeBlocked = (issueId: string) => {
|
||||
if (!activeWorkspace || !activeProject || !issueDetail) return;
|
||||
|
||||
const currentBlockers =
|
||||
issues?.results
|
||||
.find((i) => i.id === issueId)
|
||||
?.blocker_issues.map((b) => b.blocker_issue_detail?.id ?? "") ?? [];
|
||||
|
||||
const updatedBlockers = currentBlockers.filter((b) => b !== issueDetail.id);
|
||||
|
||||
issuesService
|
||||
.patchIssue(activeWorkspace.slug, activeProject.id, issueId, {
|
||||
blockers_list: updatedBlockers,
|
||||
})
|
||||
.then((response) => {
|
||||
mutateIssues((prevData) => ({
|
||||
...(prevData as IssueResponse),
|
||||
results: (prevData?.results ?? []).map((issue) => {
|
||||
if (issue.id === issueDetail.id) {
|
||||
return { ...issue, ...response };
|
||||
}
|
||||
return issue;
|
||||
}),
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-start py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<FlagIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<BlockedIcon height={16} width={16} />
|
||||
<p>Blocked by</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2 space-y-1">
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<div className="space-y-1 sm:basis-1/2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{watch("blocked_list") && watch("blocked_list").length > 0
|
||||
? watch("blocked_list").map((issue) => (
|
||||
<span
|
||||
key={issue}
|
||||
className="group flex items-center gap-1 border rounded-2xl text-xs px-1.5 py-0.5 text-red-500 hover:bg-red-50 border-red-500 cursor-pointer"
|
||||
onClick={() => removeBlocked(issue)}
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-white px-1.5 py-0.5 text-xs text-red-500 duration-300 hover:border-red-500 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
const updatedBlocked: string[] = watch("blocked_list").filter(
|
||||
(i) => i !== issue
|
||||
);
|
||||
submitChanges({
|
||||
blocks_list: updatedBlocked,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{`${activeProject?.identifier}-${
|
||||
issues?.results.find((i) => i.id === issue)?.sequence_id
|
||||
}`}
|
||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${
|
||||
issues?.results.find((i) => i.id === issue)?.id
|
||||
}`}
|
||||
>
|
||||
<a className="flex items-center gap-1">
|
||||
<BlockedIcon height={10} width={10} />
|
||||
{`${
|
||||
issues?.results.find((i) => i.id === issue)?.project_detail?.identifier
|
||||
}-${issues?.results.find((i) => i.id === issue)?.sequence_id}`}
|
||||
</a>
|
||||
</Link>
|
||||
<span className="opacity-0 duration-300 group-hover:opacity-100">
|
||||
<XMarkIcon className="h-2 w-2" />
|
||||
</span>
|
||||
</span>
|
||||
))
|
||||
: null}
|
||||
|
|
@ -173,7 +155,7 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
|
|
@ -201,40 +183,40 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
htmlFor={`blocked-issue-${issue.id}`}
|
||||
value={{
|
||||
name: issue.name,
|
||||
url: `/projects/${issue.project}/issues/${issue.id}`,
|
||||
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
|
||||
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("issue_ids")}
|
||||
id={`issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
/>
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span>{issue.name}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("issue_ids")}
|
||||
id={`blocked-issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
/>
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{
|
||||
issues?.results.find((i) => i.id === issue.id)
|
||||
?.project_detail?.identifier
|
||||
}
|
||||
-{issue.sequence_id}
|
||||
</span>
|
||||
<span>{issue.name}</span>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
}
|
||||
|
|
@ -258,15 +240,15 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||
)}
|
||||
</Combobox>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 p-3">
|
||||
<Button onClick={handleSubmit(onSubmit)} size="sm">
|
||||
Add selected issues
|
||||
</Button>
|
||||
<div className="flex items-center justify-end gap-2 p-3">
|
||||
<div>
|
||||
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleSubmit(onSubmit)} size="sm">
|
||||
Add selected issues
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
|
|
@ -276,7 +258,7 @@ const SelectBlocked: React.FC<Props> = ({ issueDetail, issuesList, watch }) => {
|
|||
</Transition.Root>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() => setIsBlockedModalOpen(true)}
|
||||
>
|
||||
Select issues
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
// react
|
||||
import React, { useState } from "react";
|
||||
// react-hook-form
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form";
|
||||
// constants
|
||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import { FlagIcon, FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { BlockerIcon, LayerDiagonalIcon } from "ui/icons";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
|
|
@ -30,9 +38,20 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
const [query, setQuery] = useState("");
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
|
||||
const { activeProject, issues } = useUser();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const { register, handleSubmit, reset } = useForm<FormInput>();
|
||||
|
||||
const handleClose = () => {
|
||||
|
|
@ -49,36 +68,54 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.issue_ids)) data.issue_ids = [data.issue_ids];
|
||||
|
||||
const newBlockers = [...watch("blockers_list"), ...data.issue_ids];
|
||||
submitChanges({ blockers_list: newBlockers });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-start py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<FlagIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<BlockerIcon height={16} width={16} />
|
||||
<p>Blocking</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2 space-y-1">
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<div className="space-y-1 sm:basis-1/2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{watch("blockers_list") && watch("blockers_list").length > 0
|
||||
? watch("blockers_list").map((issue) => (
|
||||
<span
|
||||
<div
|
||||
key={issue}
|
||||
className="group flex items-center gap-1 border rounded-2xl text-xs px-1.5 py-0.5 text-yellow-500 hover:bg-yellow-50 border-yellow-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
const updatedBlockers = watch("blockers_list").filter((i) => i !== issue);
|
||||
submitChanges({
|
||||
blockers_list: updatedBlockers,
|
||||
});
|
||||
}}
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-white px-1.5 py-0.5 text-xs text-yellow-500 duration-300 hover:border-yellow-500 hover:bg-yellow-50"
|
||||
>
|
||||
{`${activeProject?.identifier}-${
|
||||
issues?.results.find((i) => i.id === issue)?.sequence_id
|
||||
}`}
|
||||
<XMarkIcon className="h-2 w-2 group-hover:text-red-500" />
|
||||
</span>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${
|
||||
issues?.results.find((i) => i.id === issue)?.id
|
||||
}`}
|
||||
>
|
||||
<a className="flex items-center gap-1">
|
||||
<BlockerIcon height={10} width={10} />
|
||||
{`${
|
||||
issues?.results.find((i) => i.id === issue)?.project_detail?.identifier
|
||||
}-${issues?.results.find((i) => i.id === issue)?.sequence_id}`}
|
||||
</a>
|
||||
</Link>
|
||||
<span
|
||||
className="opacity-0 duration-300 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
const updatedBlockers: string[] = watch("blockers_list").filter(
|
||||
(i) => i !== issue
|
||||
);
|
||||
submitChanges({
|
||||
blockers_list: updatedBlockers,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<XMarkIcon className="h-2 w-2" />
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
|
|
@ -120,7 +157,7 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
aria-hidden="true"
|
||||
/>
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none"
|
||||
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
|
|
@ -130,65 +167,73 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
static
|
||||
className="max-h-80 scroll-py-2 divide-y divide-gray-500 divide-opacity-10 overflow-y-auto"
|
||||
>
|
||||
{issuesList.length > 0 && (
|
||||
<>
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||
Select blocker issues
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{issuesList.map((issue) => {
|
||||
if (
|
||||
!watch("blockers_list").includes(issue.id) &&
|
||||
!watch("blocked_list").includes(issue.id)
|
||||
) {
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
value={{
|
||||
name: issue.name,
|
||||
url: `/projects/${issue.project}/issues/${issue.id}`,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2",
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("issue_ids")}
|
||||
id={`issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
/>
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
<span>{issue.name}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
{issuesList.length > 0 ? (
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||
Select blocker issues
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
{issuesList.map((issue) => {
|
||||
if (
|
||||
!watch("blockers_list").includes(issue.id) &&
|
||||
!watch("blocked_list").includes(issue.id)
|
||||
)
|
||||
return (
|
||||
<Combobox.Option
|
||||
key={issue.id}
|
||||
as="label"
|
||||
htmlFor={`blocker-issue-${issue.id}`}
|
||||
value={{
|
||||
name: issue.name,
|
||||
url: `/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`,
|
||||
}}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
"flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2",
|
||||
active ? "bg-gray-900 bg-opacity-5 text-gray-900" : ""
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...register("issue_ids")}
|
||||
id={`blocker-issue-${issue.id}`}
|
||||
value={issue.id}
|
||||
/>
|
||||
<span
|
||||
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{
|
||||
issues?.results.find((i) => i.id === issue.id)
|
||||
?.project_detail?.identifier
|
||||
}
|
||||
-{issue.sequence_id}
|
||||
</span>
|
||||
<span>{issue.name}</span>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-4 px-3 py-8 text-center">
|
||||
<LayerDiagonalIcon height="56" width="56" />
|
||||
<h3 className="text-gray-500">
|
||||
No issues found. Create a new issue with{" "}
|
||||
<pre className="inline rounded bg-gray-100 px-2 py-1">
|
||||
Ctrl/Command + I
|
||||
</pre>
|
||||
.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
|
||||
|
|
@ -205,15 +250,15 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
)}
|
||||
</Combobox>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 p-3">
|
||||
<Button onClick={handleSubmit(onSubmit)} size="sm">
|
||||
Add selected issues
|
||||
</Button>
|
||||
<div className="flex items-center justify-end gap-2 p-3">
|
||||
<div>
|
||||
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||
<Button type="button" theme="secondary" size="sm" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleSubmit(onSubmit)} size="sm">
|
||||
Add selected issues
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
|
|
@ -223,7 +268,7 @@ const SelectBlocker: React.FC<Props> = ({ submitChanges, issuesList, watch }) =>
|
|||
</Transition.Root>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() => setIsBlockerModalOpen(true)}
|
||||
>
|
||||
Select issues
|
||||
|
|
|
|||
|
|
@ -1,33 +1,67 @@
|
|||
// react
|
||||
import React from "react";
|
||||
// react-hook-form
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// constants
|
||||
import { CYCLE_ISSUES, CYCLE_LIST, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// services
|
||||
import issuesService from "lib/services/issues.service";
|
||||
import cyclesService from "lib/services/cycles.service";
|
||||
// ui
|
||||
import { Spinner, CustomSelect } from "ui";
|
||||
// icons
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { ICycle, IIssue } from "types";
|
||||
import { CycleIssueResponse, ICycle, IIssue, IssueResponse } from "types";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
control: Control<IIssue, any>;
|
||||
handleCycleChange: (cycle: ICycle) => void;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
};
|
||||
|
||||
const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
|
||||
const { cycles } = useUser();
|
||||
const SelectCycle: React.FC<Props> = ({ issueDetail, control, handleCycleChange }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
|
||||
const { data: cycles } = useSWR(
|
||||
workspaceSlug && projectId ? CYCLE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => cyclesService.getCycles(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const removeIssueFromCycle = (bridgeId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string));
|
||||
|
||||
issuesService
|
||||
.removeIssueFromCycle(workspaceSlug as string, projectId as string, cycleId, bridgeId)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
|
||||
mutate(CYCLE_ISSUES(cycleId));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ArrowPathIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<ArrowPathIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Cycle</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<div className="space-y-1 sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="issue_cycle"
|
||||
|
|
@ -38,24 +72,34 @@ const SelectCycle: React.FC<Props> = ({ control, handleCycleChange }) => {
|
|||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block text-left"
|
||||
"hidden truncate text-left sm:block"
|
||||
)}
|
||||
>
|
||||
{value ? cycles?.find((c) => c.id === value.cycle_detail.id)?.name : "None"}
|
||||
{value ? value?.cycle_detail?.name : "None"}
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
handleCycleChange(cycles?.find((c) => c.id === value) as any);
|
||||
value === null
|
||||
? removeIssueFromCycle(
|
||||
issueDetail?.issue_cycle?.id ?? "",
|
||||
issueDetail?.issue_cycle?.cycle ?? ""
|
||||
)
|
||||
: handleCycleChange(cycles?.find((c) => c.id === value) as any);
|
||||
}}
|
||||
>
|
||||
{cycles ? (
|
||||
cycles.length > 0 ? (
|
||||
cycles.map((option) => (
|
||||
<CustomSelect.Option key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
<>
|
||||
<CustomSelect.Option value={null} className="capitalize">
|
||||
<>None</>
|
||||
</CustomSelect.Option>
|
||||
))
|
||||
{cycles.map((option) => (
|
||||
<CustomSelect.Option key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center">No cycles found</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// constants
|
||||
import { MODULE_LIST } from "constants/fetch-keys";
|
||||
// services
|
||||
import modulesService from "lib/services/modules.service";
|
||||
// ui
|
||||
import { Spinner, CustomSelect } from "ui";
|
||||
// icons
|
||||
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IModule } from "types";
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
handleModuleChange: (module: IModule) => void;
|
||||
};
|
||||
|
||||
const SelectModule: React.FC<Props> = ({ control, handleModuleChange }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: modules } = useSWR(
|
||||
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => modulesService.getModules(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<RectangleGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Module</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="issue_module"
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
label={
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate text-left sm:block"
|
||||
)}
|
||||
>
|
||||
{value ? modules?.find((m) => m.id === value?.module_detail.id)?.name : "None"}
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: any) => {
|
||||
handleModuleChange(modules?.find((m) => m.id === value) as any);
|
||||
}}
|
||||
>
|
||||
{modules ? (
|
||||
modules.length > 0 ? (
|
||||
modules.map((option) => (
|
||||
<CustomSelect.Option key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</CustomSelect.Option>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No cycles found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectModule;
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
// react
|
||||
import React, { useState } from "react";
|
||||
// react-hook-form
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// fetch keys
|
||||
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.service";
|
||||
// components
|
||||
import IssuesListModal from "components/project/issues/issues-list-modal";
|
||||
// icons
|
||||
|
|
@ -28,12 +33,22 @@ const SelectParent: React.FC<Props> = ({
|
|||
}) => {
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
|
||||
const { activeProject, issues } = useUser();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: issues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)
|
||||
: null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesServices.getIssues(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<UserIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Parent</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
|
|
@ -57,13 +72,13 @@ const SelectParent: React.FC<Props> = ({
|
|||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full"
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded-md border px-2 py-1 text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
onClick={() => setIsParentModalOpen(true)}
|
||||
>
|
||||
{watch("parent") && watch("parent") !== ""
|
||||
? `${activeProject?.identifier}-${
|
||||
issues?.results.find((i) => i.id === watch("parent"))?.sequence_id
|
||||
}`
|
||||
? `${
|
||||
issues?.results.find((i) => i.id === watch("parent"))?.project_detail?.identifier
|
||||
}-${issues?.results.find((i) => i.id === watch("parent"))?.sequence_id}`
|
||||
: "Select issue"}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
import React from "react";
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormWatch } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomSelect } from "ui";
|
||||
// icons
|
||||
import { ChevronDownIcon, ChartBarIcon } from "@heroicons/react/24/outline";
|
||||
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// constants
|
||||
// common
|
||||
import { classNames } from "constants/common";
|
||||
import { PRIORITIES } from "constants/";
|
||||
import CustomSelect from "ui/custom-select";
|
||||
// constants
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
import { PRIORITIES } from "constants/";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
|
|
@ -22,9 +22,9 @@ type Props = {
|
|||
|
||||
const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<ChartBarIcon className="flex-shrink-0 h-4 w-4" />
|
||||
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
|
|
@ -37,7 +37,7 @@ const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
|
|||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"text-left capitalize flex items-center gap-2"
|
||||
"flex items-center gap-2 text-left capitalize"
|
||||
)}
|
||||
>
|
||||
{getPriorityIcon(
|
||||
|
|
@ -58,7 +58,7 @@ const SelectPriority: React.FC<Props> = ({ control, submitChanges, watch }) => {
|
|||
<CustomSelect.Option key={option} value={option} className="capitalize">
|
||||
<>
|
||||
{getPriorityIcon(option, "text-sm")}
|
||||
{option}
|
||||
{option ?? "None"}
|
||||
</>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
// react-hook-form
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import stateService from "lib/services/state.service";
|
||||
// icons
|
||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
// constants
|
||||
import { classNames } from "constants/common";
|
||||
import { STATE_LIST } from "constants/fetch-keys";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { classNames } from "constants/common";
|
||||
import { CustomMenu, Spinner } from "ui";
|
||||
import React from "react";
|
||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import CustomSelect from "ui/custom-select";
|
||||
// ui
|
||||
import { Spinner, CustomSelect } from "ui";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
|
|
@ -18,12 +23,20 @@ type Props = {
|
|||
};
|
||||
|
||||
const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const { states } = useUser();
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: states } = useSWR(
|
||||
workspaceSlug && projectId ? STATE_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<Squares2X2Icon className="flex-shrink-0 h-4 w-4" />
|
||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
|
|
@ -42,7 +55,7 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
{value ? (
|
||||
<>
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: states?.find((option) => option.id === value)?.color,
|
||||
}}
|
||||
|
|
@ -66,7 +79,7 @@ const SelectState: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
<>
|
||||
{option.color && (
|
||||
<span
|
||||
className="h-2 w-2 rounded-full flex-shrink-0"
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: option.color }}
|
||||
></span>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue