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:
Vamsi Kurama 2023-01-10 23:55:47 +05:30 committed by GitHub
parent a960ddedf7
commit bef166a65f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
395 changed files with 20119 additions and 18322 deletions

View file

@ -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}
/>
</>
);
};

View file

@ -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)}

View file

@ -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

View file

@ -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

View file

@ -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>
)

View file

@ -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;

View file

@ -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>

View file

@ -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>
))}

View file

@ -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>
)}