Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: Added Module and Cycle Selection to Issue Creation Modal #2602

Merged
merged 1 commit into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions web/components/issues/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
IssuePrioritySelect,
IssueProjectSelect,
IssueStateSelect,
IssueModuleSelect,
IssueCycleSelect,
} from "components/issues/select";
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
Expand Down Expand Up @@ -70,6 +72,8 @@ export interface IssueFormProps {
| "estimate"
| "parent"
| "all"
| "module"
| "cycle"
)[];
}

Expand Down Expand Up @@ -108,7 +112,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {

const user = userStore.currentUser;

const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId)
const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId);

const { setToastAlert } = useToast();

Expand Down Expand Up @@ -488,6 +492,38 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
/>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && (
<Controller
control={control}
name="module"
render={({ field: { value, onChange } }) => (
<IssueModuleSelect
workspaceSlug={workspaceSlug as string}
projectId={projectId}
value={value}
onChange={(val: string) => {
onChange(val);
}}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && (
<Controller
control={control}
name="cycle"
render={({ field: { value, onChange } }) => (
<IssueCycleSelect
workspaceSlug={workspaceSlug as string}
projectId={projectId}
value={value}
onChange={(val: string) => {
onChange(val);
}}
/>
)}
/>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
<>
<Controller
Expand All @@ -503,7 +539,10 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
<>
<CustomMenu
customButton={
<button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200">
<button
type="button"
className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200"
>
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80">
{watch("parent") ? (
<>
Expand Down
2 changes: 2 additions & 0 deletions web/components/issues/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface IssuesModalProps {
| "estimate"
| "parent"
| "all"
| "module"
| "cycle"
)[];
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
}
Expand Down
143 changes: 143 additions & 0 deletions web/components/issues/select/cycle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// popper js
import { usePopper } from "react-popper";
// ui
import { Combobox } from "@headlessui/react";
// icons
import { ContrastIcon } from "@plane/ui";
// icons
import { Check, Search } from "lucide-react";

export interface IssueCycleSelectProps {
workspaceSlug: string;
projectId: string;
value: string | null;
onChange: (value: string) => void;
}

export const IssueCycleSelect: React.FC<IssueCycleSelectProps> = observer((props) => {
const { workspaceSlug, projectId, value, onChange } = props;
const [query, setQuery] = useState("");

const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);

const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
});

const { cycle: cycleStore } = useMobxStore();

const fetchCycles = () => {
if (workspaceSlug && projectId) cycleStore.fetchCycles(workspaceSlug, projectId, "all");
};

const cycles = projectId ? cycleStore.cycles[projectId] : undefined;

const selectedCycle = cycles ? cycles?.find((i) => i.id === value) : undefined;

const options = cycles?.map((cycle) => ({
value: cycle.id,
query: cycle.name,
content: (
<div className="flex items-center gap-1.5 truncate">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<ContrastIcon />
</span>
<span className="truncate flex-grow">{cycle.name}</span>
</div>
),
}));

const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));

const label = selectedCycle ? (
<div className="flex items-center gap-1.5">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<ContrastIcon />
</span>
<div className="truncate">{selectedCycle.name}</div>
</div>
) : (
<>
<ContrastIcon className="h-3 w-3" />
<span>Select Cycle</span>
</>
);

return (
<Combobox
as="div"
className={`flex-shrink-0 text-left`}
value={value}
onChange={(val: string) => onChange(val)}
disabled={false}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs px-2 py-1 rounded-md shadow-sm text-custom-text-200 border border-custom-border-300 duration-300 focus:outline-none ${
false ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
}`}
onClick={fetchCycles}
>
{label}
</button>
</Combobox.Button>
<Combobox.Options>
<div
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active && !selected ? "bg-custom-background-80" : ""
} w-full truncate ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
});
2 changes: 2 additions & 0 deletions web/components/issues/select/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from "./label";
export * from "./priority";
export * from "./project";
export * from "./state";
export * from "./module";
export * from "./cycle";
143 changes: 143 additions & 0 deletions web/components/issues/select/module.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// popper js
import { usePopper } from "react-popper";
// ui
import { Combobox } from "@headlessui/react";
// icons
import { DiceIcon } from "@plane/ui";
// icons
import { Check, Search } from "lucide-react";

export interface IssueModuleSelectProps {
workspaceSlug: string;
projectId: string;
value: string | null;
onChange: (value: string) => void;
}

export const IssueModuleSelect: React.FC<IssueModuleSelectProps> = observer((props) => {
const { workspaceSlug, projectId, value, onChange } = props;
const [query, setQuery] = useState("");

const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);

const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: "bottom-start",
});

const { module: moduleStore } = useMobxStore();

const fetchModules = () => {
if (workspaceSlug && projectId) moduleStore.fetchModules(workspaceSlug, projectId);
};

const modules = projectId ? moduleStore.modules[projectId] : undefined;

const selectedModule = modules ? modules?.find((i) => i.id === value) : undefined;

const options = modules?.map((module) => ({
value: module.id,
query: module.name,
content: (
<div className="flex items-center gap-1.5 truncate">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<DiceIcon />
</span>
<span className="truncate flex-grow">{module.name}</span>
</div>
),
}));

const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));

const label = selectedModule ? (
<div className="flex items-center gap-1.5">
<span className="flex justify-center items-center flex-shrink-0 w-3.5 h-3.5">
<DiceIcon />
</span>
<div className="truncate">{selectedModule.name}</div>
</div>
) : (
<>
<DiceIcon className="h-3 w-3" />
<span>Select Module</span>
</>
);

return (
<Combobox
as="div"
className={`flex-shrink-0 text-left`}
value={value}
onChange={(val: string) => onChange(val)}
disabled={false}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs px-2 py-1 rounded-md shadow-sm text-custom-text-200 border border-custom-border-300 duration-300 focus:outline-none ${
false ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
}`}
onClick={fetchModules}
>
{label}
</button>
</Combobox.Button>
<Combobox.Options>
<div
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active && !selected ? "bg-custom-background-80" : ""
} w-full truncate ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
});
Loading