fix: Labels delete & reordering (#2729)

* fix: Labels reordering inconsistency

* fix: Delete child labels

* feat: multi-select while grouping labels

* refactor: label sorting in mobx computed function

* feat: drag & drop label grouping, un-grouping

* chore: removed label select modal

* fix: moving labels from project store to project label store

* fix: typo changes and build tree function added

* labels feature

* disable dropping group into a group

* fix build errors

* fix more issues

* chore: added combining state UI, fixed scroll issue for label groups

* chore: group icon for label groups

* fix: group cannot be dropped in another group

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
Lakhan Baheti 2023-11-19 01:46:11 +05:30 committed by sriram veeraghanta
parent d933c73343
commit 63b6150b9c
62 changed files with 862 additions and 520 deletions

View File

@ -30,12 +30,12 @@ export const CycleIssuesHeader: React.FC = observer(() => {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
cycle: cycleStore, cycle: cycleStore,
cycleIssueFilter: cycleIssueFilterStore, cycleIssueFilter: cycleIssueFilterStore,
project: projectStore, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = projectStore;
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
@ -178,7 +178,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />

View File

@ -30,13 +30,13 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
module: moduleStore, module: moduleStore,
moduleFilter: moduleFilterStore, moduleFilter: moduleFilterStore,
project: projectStore, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
commandPalette: commandPaletteStore, commandPalette: commandPaletteStore,
} = useMobxStore(); } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
const { currentProjectDetails } = projectStore;
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false"); const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
@ -177,7 +177,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />

View File

@ -21,14 +21,13 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { const {
project: projectStore, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
archivedIssueFilters: archivedIssueFiltersStore, archivedIssueFilters: archivedIssueFiltersStore,
projectState: projectStateStore, projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = projectStore;
// for archived issues list layout is the only option // for archived issues list layout is the only option
const activeLayout = "list"; const activeLayout = "list";
@ -119,7 +118,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />

View File

@ -25,7 +25,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
const { const {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
project: projectStore, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
inbox: inboxStore, inbox: inboxStore,
@ -92,7 +93,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
}, },
[issueFilterStore, projectId, workspaceSlug] [issueFilterStore, projectId, workspaceSlug]
); );
const { currentProjectDetails } = projectStore;
const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined; const inboxDetails = projectId ? inboxStore.inboxesList?.[projectId.toString()]?.[0] : undefined;
@ -178,7 +178,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />

View File

@ -22,14 +22,13 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const { const {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
projectViewFilters: projectViewFiltersStore, projectViewFilters: projectViewFiltersStore,
project: projectStore, project: { currentProjectDetails },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
projectViews: projectViewsStore, projectViews: projectViewsStore,
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = projectStore;
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined; const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
const activeLayout = issueFilterStore.userDisplayFilters.layout; const activeLayout = issueFilterStore.userDisplayFilters.layout;
@ -163,7 +162,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
layoutDisplayFiltersOptions={ layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
} }
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/> />

View File

@ -15,13 +15,13 @@ import { X } from "lucide-react";
// helpers // helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types // types
import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types"; import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
type Props = { type Props = {
appliedFilters: IIssueFilterOptions; appliedFilters: IIssueFilterOptions;
handleClearAllFilters: () => void; handleClearAllFilters: () => void;
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void; handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
labels?: IIssueLabels[] | undefined; labels?: IIssueLabel[] | undefined;
members?: IUserLite[] | undefined; members?: IUserLite[] | undefined;
projects?: IProject[] | undefined; projects?: IProject[] | undefined;
states?: IState[] | undefined; states?: IState[] | undefined;

View File

@ -3,11 +3,11 @@ import { observer } from "mobx-react-lite";
// icons // icons
import { X } from "lucide-react"; import { X } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
type Props = { type Props = {
handleRemove: (val: string) => void; handleRemove: (val: string) => void;
labels: IIssueLabels[] | undefined; labels: IIssueLabel[] | undefined;
values: string[]; values: string[];
}; };

View File

@ -14,7 +14,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const { const {
archivedIssueFilters: archivedIssueFiltersStore, archivedIssueFilters: archivedIssueFiltersStore,
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
@ -77,7 +77,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
appliedFilters={appliedFilters} appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/> />

View File

@ -12,7 +12,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
const { workspaceSlug, projectId, cycleId } = router.query; const { workspaceSlug, projectId, cycleId } = router.query;
const { const {
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
cycleIssueFilter: cycleIssueFilterStore, cycleIssueFilter: cycleIssueFilterStore,
projectState: projectStateStore, projectState: projectStateStore,
@ -72,7 +72,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
appliedFilters={appliedFilters} appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/> />

View File

@ -13,7 +13,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
const { workspaceSlug, projectId, moduleId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
const { const {
project: projectStore, projectLabel: { projectLabels },
moduleFilter: moduleFilterStore, moduleFilter: moduleFilterStore,
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
@ -73,7 +73,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
appliedFilters={appliedFilters} appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/> />

View File

@ -14,7 +14,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
const { const {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
project: projectStore, projectLabel: { projectLabels },
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
} = useMobxStore(); } = useMobxStore();
@ -77,7 +77,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
appliedFilters={appliedFilters} appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/> />

View File

@ -18,7 +18,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const { workspaceSlug, projectId, viewId } = router.query; const { workspaceSlug, projectId, viewId } = router.query;
const { const {
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
projectViews: projectViewsStore, projectViews: projectViewsStore,
@ -99,7 +99,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
appliedFilters={appliedFilters} appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters} handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
states={projectStateStore.states?.[projectId?.toString() ?? ""]} states={projectStateStore.states?.[projectId?.toString() ?? ""]}
/> />

View File

@ -15,7 +15,7 @@ import {
FilterTargetDate, FilterTargetDate,
} from "components/issues"; } from "components/issues";
// types // types
import { IIssueFilterOptions, IIssueLabels, IProject, IState, IUserLite } from "types"; import { IIssueFilterOptions, IIssueLabel, IProject, IState, IUserLite } from "types";
// constants // constants
import { ILayoutDisplayFiltersOptions } from "constants/issue"; import { ILayoutDisplayFiltersOptions } from "constants/issue";
@ -23,7 +23,7 @@ type Props = {
filters: IIssueFilterOptions; filters: IIssueFilterOptions;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
labels?: IIssueLabels[] | undefined; labels?: IIssueLabel[] | undefined;
members?: IUserLite[] | undefined; members?: IUserLite[] | undefined;
projects?: IProject[] | undefined; projects?: IProject[] | undefined;
states?: IState[] | undefined; states?: IState[] | undefined;

View File

@ -5,7 +5,7 @@ import { FilterHeader, FilterOption } from "components/issues";
// ui // ui
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
const LabelIcons = ({ color }: { color: string }) => ( const LabelIcons = ({ color }: { color: string }) => (
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} /> <span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: color }} />
@ -14,7 +14,7 @@ const LabelIcons = ({ color }: { color: string }) => (
type Props = { type Props = {
appliedFilters: string[] | null; appliedFilters: string[] | null;
handleUpdate: (val: string) => void; handleUpdate: (val: string) => void;
labels: IIssueLabels[] | undefined; labels: IIssueLabel[] | undefined;
searchQuery: string; searchQuery: string;
}; };

View File

@ -23,6 +23,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
// store // store
const { const {
project: projectStore, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
@ -99,7 +100,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
// const estimates = // const estimates =
@ -137,7 +137,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
@ -164,7 +164,7 @@ export const CycleKanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}

View File

@ -21,7 +21,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
const { workspaceSlug, moduleId } = router.query; const { workspaceSlug, moduleId } = router.query;
// store // store
const { const {
project: projectStore, project: { workspaceProjects },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
@ -97,9 +98,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
// const estimates = // const estimates =
// currentProjectDetails?.estimate !== null // currentProjectDetails?.estimate !== null
// ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
@ -135,9 +134,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={workspaceProjects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
/> />
@ -162,9 +161,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={workspaceProjects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
/> />

View File

@ -21,7 +21,8 @@ export const KanBanLayout: React.FC = observer(() => {
const { workspaceSlug } = router.query as { workspaceSlug: string }; const { workspaceSlug } = router.query as { workspaceSlug: string };
const { const {
project: projectStore, project: { workspaceProjects },
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
issue: issueStore, issue: issueStore,
@ -29,7 +30,6 @@ export const KanBanLayout: React.FC = observer(() => {
issueKanBanView: issueKanBanViewStore, issueKanBanView: issueKanBanViewStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
} = useMobxStore(); } = useMobxStore();
const { currentProjectDetails } = projectStore;
const issues = issueStore?.getIssues; const issues = issueStore?.getIssues;
@ -92,13 +92,11 @@ export const KanBanLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects?.[workspaceSlug] || null : null; // const estimates =
const estimates = // currentProjectDetails?.estimate !== null
currentProjectDetails?.estimate !== null // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null
? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null // : null;
: null;
return ( return (
<> <>
@ -129,9 +127,9 @@ export const KanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={workspaceProjects}
enableQuickIssueCreate enableQuickIssueCreate
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
@ -156,9 +154,9 @@ export const KanBanLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={workspaceProjects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true} showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
/> />

View File

@ -56,7 +56,7 @@ export const ProjectViewKanBanLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null; // const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = projectStateStore?.projectStates || null; const projects = projectStateStore?.projectStates || null;
const estimates = null; const estimates = null;

View File

@ -5,7 +5,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root";
import { KanBan } from "./default"; import { KanBan } from "./default";
// types // types
import { IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
// constants // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
@ -63,7 +63,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
states: IState[] | null; states: IState[] | null;
stateGroups: any; stateGroups: any;
priorities: any; priorities: any;
labels: IIssueLabels[] | null; labels: IIssueLabel[] | null;
members: IUserLite[] | null; members: IUserLite[] | null;
projects: IProject[] | null; projects: IProject[] | null;
issues: any; issues: any;
@ -181,7 +181,7 @@ export interface IKanBanSwimLanes {
states: IState[] | null; states: IState[] | null;
stateGroups: any; stateGroups: any;
priorities: any; priorities: any;
labels: IIssueLabels[] | null; labels: IIssueLabel[] | null;
members: IUserLite[] | null; members: IUserLite[] | null;
projects: IProject[] | null; projects: IProject[] | null;
isDragStarted?: boolean; isDragStarted?: boolean;

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { ListGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues";
// types // types
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types";
// constants // constants
import { getValueFromObject } from "constants/issue"; import { getValueFromObject } from "constants/issue";
@ -88,7 +88,7 @@ export interface IList {
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
states: IState[] | null; states: IState[] | null;
labels: IIssueLabels[] | null; labels: IIssueLabel[] | null;
members: IUserLite[] | null; members: IUserLite[] | null;
projects: IProject[] | null; projects: IProject[] | null;
stateGroups: any; stateGroups: any;

View File

@ -19,6 +19,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
const { const {
project: projectStore, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
archivedIssues: archivedIssueStore, archivedIssues: archivedIssueStore,
@ -42,7 +43,6 @@ export const ArchivedIssueListLayout: FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const estimates = const estimates =
@ -64,7 +64,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}

View File

@ -21,6 +21,7 @@ export const CycleListLayout: React.FC = observer(() => {
// store // store
const { const {
project: projectStore, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
@ -59,7 +60,6 @@ export const CycleListLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const estimates = const estimates =
@ -85,7 +85,7 @@ export const CycleListLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}

View File

@ -21,6 +21,7 @@ export const ModuleListLayout: React.FC = observer(() => {
const { const {
project: projectStore, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
@ -59,7 +60,6 @@ export const ModuleListLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const estimates = const estimates =
@ -85,7 +85,7 @@ export const ModuleListLayout: React.FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}

View File

@ -20,6 +20,7 @@ export const ListLayout: FC = observer(() => {
// store // store
const { const {
project: projectStore, project: projectStore,
projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
issue: issueStore, issue: issueStore,
@ -49,7 +50,6 @@ export const ListLayout: FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const estimates = const estimates =
@ -80,7 +80,7 @@ export const ListLayout: FC = observer(() => {
states={states} states={states}
stateGroups={stateGroups} stateGroups={stateGroups}
priorities={priorities} priorities={priorities}
labels={labels} labels={projectLabels}
members={projectMembers?.map((m) => m.member) ?? null} members={projectMembers?.map((m) => m.member) ?? null}
projects={projects} projects={projects}
enableQuickIssueCreate enableQuickIssueCreate

View File

@ -30,7 +30,7 @@ export const ProjectViewListLayout: React.FC = observer(() => {
const states = projectStateStore?.projectStates || null; const states = projectStateStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null; const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null; // const labels = projectStore?.projectLabels || null;
const stateGroups = ISSUE_STATE_GROUPS || null; const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = projectStateStore?.projectStates || null; const projects = projectStateStore?.projectStates || null;
const estimates = null; const estimates = null;

View File

@ -1,8 +1,6 @@
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
// components // components
@ -44,7 +42,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
noLabelBorder = false, noLabelBorder = false,
} = props; } = props;
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore(); const {
workspace: workspaceStore,
projectLabel: { fetchProjectLabels, projectLabels },
}: RootStore = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug; const workspaceSlug = workspaceStore?.workspaceSlug;
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -53,12 +54,9 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [isLoading, setIsLoading] = useState<Boolean>(false); const [isLoading, setIsLoading] = useState<Boolean>(false);
const projectLabels = projectId && projectStore?.labels?.[projectId]; const fetchLabels = () => {
const fetchProjectLabels = () => {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug && projectId) if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
projectStore.fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}; };
const options = (projectLabels ? projectLabels : []).map((label) => ({ const options = (projectLabels ? projectLabels : []).map((label) => ({
@ -169,7 +167,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer" ? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !projectLabels && fetchProjectLabels()} onClick={() => !projectLabels && fetchLabels()}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}

View File

@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
// components // components
import { SpreadsheetColumn } from "components/issues"; import { SpreadsheetColumn } from "components/issues";
// types // types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
type Props = { type Props = {
displayFilters: IIssueDisplayFilterOptions; displayFilters: IIssueDisplayFilterOptions;
@ -13,7 +13,7 @@ type Props = {
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void; handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
issues: IIssue[] | undefined; issues: IIssue[] | undefined;
members?: IUserLite[] | undefined; members?: IUserLite[] | undefined;
labels?: IIssueLabels[] | undefined; labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined; states?: IState[] | undefined;
}; };

View File

@ -5,12 +5,12 @@ import { IssuePropertyLabels } from "../../properties";
// hooks // hooks
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // types
import { IIssue, IIssueLabels } from "types"; import { IIssue, IIssueLabel } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
onChange: (formData: Partial<IIssue>) => void; onChange: (formData: Partial<IIssue>) => void;
labels: IIssueLabels[] | undefined; labels: IIssueLabel[] | undefined;
expandedIssues: string[]; expandedIssues: string[];
disabled: boolean; disabled: boolean;
}; };

View File

@ -1,7 +1,6 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
@ -19,7 +18,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
cycleIssue: cycleIssueStore, cycleIssue: cycleIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
@ -61,7 +60,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues as IIssueUnGroupedStructure} issues={issues as IIssueUnGroupedStructure}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined} labels={projectLabels || undefined}
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
handleIssueAction={() => {}} handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue} handleUpdateIssue={handleUpdateIssue}

View File

@ -19,7 +19,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
moduleIssue: moduleIssueStore, moduleIssue: moduleIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
@ -61,7 +61,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues as IIssueUnGroupedStructure} issues={issues as IIssueUnGroupedStructure}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined} labels={projectLabels ?? undefined}
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
handleIssueAction={() => {}} handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue} handleUpdateIssue={handleUpdateIssue}

View File

@ -19,7 +19,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
issue: issueStore, issue: issueStore,
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
user: userStore, user: userStore,
@ -63,7 +63,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues as IIssueUnGroupedStructure} issues={issues as IIssueUnGroupedStructure}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined} labels={projectLabels || undefined}
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
handleIssueAction={() => {}} handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue} handleUpdateIssue={handleUpdateIssue}

View File

@ -1,7 +1,6 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
@ -19,7 +18,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
issueFilter: issueFilterStore, issueFilter: issueFilterStore,
projectViewIssues: projectViewIssueStore, projectViewIssues: projectViewIssueStore,
issueDetail: issueDetailStore, issueDetail: issueDetailStore,
project: projectStore, projectLabel: { projectLabels },
projectMember: { projectMembers }, projectMember: { projectMembers },
projectState: projectStateStore, projectState: projectStateStore,
} = useMobxStore(); } = useMobxStore();
@ -61,7 +60,7 @@ export const ProjectViewSpreadsheetLayout: React.FC = observer(() => {
handleDisplayFilterUpdate={handleDisplayFiltersUpdate} handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues as IIssueUnGroupedStructure} issues={issues as IIssueUnGroupedStructure}
members={projectMembers?.map((m) => m.member)} members={projectMembers?.map((m) => m.member)}
labels={projectId ? projectStore.labels?.[projectId.toString()] ?? undefined : undefined} labels={projectLabels || undefined}
states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined}
handleIssueAction={() => {}} handleIssueAction={() => {}}
handleUpdateIssue={handleUpdateIssue} handleUpdateIssue={handleUpdateIssue}

View File

@ -27,7 +27,7 @@ import {
// ui // ui
import { CustomMenu } from "@plane/ui"; import { CustomMenu } from "@plane/ui";
// types // types
import { IIssue, IIssueDisplayFilterOptions, IIssueLabels, IState, IUserLite, TIssueOrderByOptions } from "types"; import { IIssue, IIssueDisplayFilterOptions, IIssueLabel, IState, IUserLite, TIssueOrderByOptions } from "types";
// constants // constants
import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet"; import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
@ -40,7 +40,7 @@ type Props = {
issues: IIssue[] | undefined; issues: IIssue[] | undefined;
property: string; property: string;
members?: IUserLite[] | undefined; members?: IUserLite[] | undefined;
labels?: IIssueLabels[] | undefined; labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined; states?: IState[] | undefined;
}; };

View File

@ -6,7 +6,7 @@ import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreat
import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { IssuePeekOverview } from "components/issues/issue-peek-overview";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// types // types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueLabel, IState, IUserLite } from "types";
type Props = { type Props = {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
@ -14,7 +14,7 @@ type Props = {
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void; handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: IIssue[] | undefined; issues: IIssue[] | undefined;
members?: IUserLite[] | undefined; members?: IUserLite[] | undefined;
labels?: IIssueLabels[] | undefined; labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined; states?: IState[] | undefined;
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void; handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;

View File

@ -29,7 +29,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
const { const {
project: { labels, fetchProjectLabels }, projectLabel: { labels, fetchProjectLabels },
} = useMobxStore(); } = useMobxStore();
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);

View File

@ -15,7 +15,7 @@ import { IssueLabelSelect } from "../select";
// icons // icons
import { Plus, X } from "lucide-react"; import { Plus, X } from "lucide-react";
// types // types
import { IIssue, IIssueLabels } from "types"; import { IIssue, IIssueLabel } from "types";
// fetch-keys // fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -28,7 +28,7 @@ type Props = {
uneditable: boolean; uneditable: boolean;
}; };
const defaultValues: Partial<IIssueLabels> = { const defaultValues: Partial<IIssueLabel> = {
name: "", name: "",
color: "#ff0000", color: "#ff0000",
}; };
@ -57,20 +57,20 @@ export const SidebarLabelSelect: React.FC<Props> = ({
watch, watch,
control, control,
setFocus, setFocus,
} = useForm<Partial<IIssueLabels>>({ } = useForm<Partial<IIssueLabel>>({
defaultValues, defaultValues,
}); });
const { user } = useUser(); const { user } = useUser();
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>( const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabel[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string) ? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)
: null : null
); );
const handleNewLabel = async (formData: Partial<IIssueLabels>) => { const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await issueLabelService await issueLabelService

View File

@ -12,7 +12,7 @@ import { Button, Input } from "@plane/ui";
// icons // icons
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
// types // types
import type { IIssueLabels, IState } from "types"; import type { IIssueLabel, IState } from "types";
// constants // constants
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label"; import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "constants/label";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -22,7 +22,7 @@ type Props = {
isOpen: boolean; isOpen: boolean;
projectId: string; projectId: string;
handleClose: () => void; handleClose: () => void;
onSuccess?: (response: IIssueLabels) => void; onSuccess?: (response: IIssueLabel) => void;
}; };
const defaultValues: Partial<IState> = { const defaultValues: Partial<IState> = {
@ -47,7 +47,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
reset, reset,
setValue, setValue,
setFocus, setFocus,
} = useForm<IIssueLabels>({ } = useForm<IIssueLabel>({
defaultValues, defaultValues,
}); });
@ -69,7 +69,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const onSubmit = async (formData: IIssueLabels) => { const onSubmit = async (formData: IIssueLabel) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
await projectLabelStore await projectLabelStore

View File

@ -11,7 +11,7 @@ import { Popover, Transition } from "@headlessui/react";
// ui // ui
import { Button, Input } from "@plane/ui"; import { Button, Input } from "@plane/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
// fetch-keys // fetch-keys
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label"; import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "constants/label";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -20,11 +20,11 @@ type Props = {
labelForm: boolean; labelForm: boolean;
setLabelForm: React.Dispatch<React.SetStateAction<boolean>>; setLabelForm: React.Dispatch<React.SetStateAction<boolean>>;
isUpdating: boolean; isUpdating: boolean;
labelToUpdate: IIssueLabels | null; labelToUpdate?: IIssueLabel;
onClose?: () => void; onClose?: () => void;
}; };
const defaultValues: Partial<IIssueLabels> = { const defaultValues: Partial<IIssueLabel> = {
name: "", name: "",
color: "rgb(var(--color-text-200))", color: "rgb(var(--color-text-200))",
}; };
@ -50,7 +50,7 @@ export const CreateUpdateLabelInline = observer(
watch, watch,
setValue, setValue,
setFocus, setFocus,
} = useForm<IIssueLabels>({ } = useForm<IIssueLabel>({
defaultValues, defaultValues,
}); });
@ -60,7 +60,7 @@ export const CreateUpdateLabelInline = observer(
if (onClose) onClose(); if (onClose) onClose();
}; };
const handleLabelCreate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelCreate: SubmitHandler<IIssueLabel> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await projectLabelStore await projectLabelStore
@ -79,7 +79,7 @@ export const CreateUpdateLabelInline = observer(
}); });
}; };
const handleLabelUpdate: SubmitHandler<IIssueLabels> = async (formData) => { const handleLabelUpdate: SubmitHandler<IIssueLabel> = async (formData) => {
if (!workspaceSlug || !projectId || isSubmitting) return; if (!workspaceSlug || !projectId || isSubmitting) return;
await projectLabelStore await projectLabelStore
@ -128,9 +128,7 @@ export const CreateUpdateLabelInline = observer(
e.preventDefault(); e.preventDefault();
handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)(); handleSubmit(isUpdating ? handleLabelUpdate : handleLabelCreate)();
}} }}
className={`flex scroll-m-8 items-center gap-2 rounded border border-custom-border-200 bg-custom-background-100 px-3.5 py-2 ${ className={`flex scroll-m-8 items-center gap-2 bg-custom-background-100 w-full ${labelForm ? "" : "hidden"}`}
labelForm ? "" : "hidden"
}`}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Popover className="relative z-10 flex h-full w-full items-center justify-center"> <Popover className="relative z-10 flex h-full w-full items-center justify-center">
@ -198,10 +196,10 @@ export const CreateUpdateLabelInline = observer(
)} )}
/> />
</div> </div>
<Button variant="neutral-primary" onClick={() => handleClose()}> <Button variant="neutral-primary" onClick={() => handleClose()} size="sm">
Cancel Cancel
</Button> </Button>
<Button variant="primary" type="submit" loading={isSubmitting}> <Button variant="primary" type="submit" size="sm" loading={isSubmitting}>
{isUpdating ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} {isUpdating ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"}
</Button> </Button>
</form> </form>

View File

@ -12,12 +12,12 @@ import useToast from "hooks/use-toast";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { IIssueLabels } from "types"; import type { IIssueLabel } from "types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
data: IIssueLabels | null; data: IIssueLabel | null;
}; };
export const DeleteLabelModal: React.FC<Props> = observer((props) => { export const DeleteLabelModal: React.FC<Props> = observer((props) => {

View File

@ -4,5 +4,5 @@ export * from "./delete-label-modal";
export * from "./label-select"; export * from "./label-select";
export * from "./labels-list-modal"; export * from "./labels-list-modal";
export * from "./project-setting-label-group"; export * from "./project-setting-label-group";
export * from "./project-setting-label-list-item"; export * from "./project-setting-label-item";
export * from "./project-setting-label-list"; export * from "./project-setting-label-list";

View File

@ -0,0 +1,24 @@
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
import { MoreVertical } from "lucide-react";
interface IDragHandle {
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps;
}
export const DragHandle = (props: IDragHandle) => {
const { isDragging, dragHandleProps } = props;
return (
<button
type="button"
className={`rounded text-custom-sidebar-text-200 flex flex-shrink-0 mr-1 group-hover:opacity-100 ${
isDragging ? "opacity-100" : "opacity-0"
}`}
{...dragHandleProps}
>
<MoreVertical className="h-3.5 w-3.5 stroke-custom-text-400" />
<MoreVertical className="h-3.5 w-3.5 stroke-custom-text-400 -ml-5" />
</button>
);
};

View File

@ -0,0 +1,80 @@
import { useRef, useState } from "react";
import { LucideIcon, X } from "lucide-react";
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
//ui
import { CustomMenu } from "@plane/ui";
//types
import { IIssueLabel } from "types";
//hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
//components
import { DragHandle } from "./drag-handle";
import { LabelName } from "./label-name";
//types
export interface ICustomMenuItem {
CustomIcon: LucideIcon;
onClick: (label: IIssueLabel) => void;
isVisible: boolean;
text: string;
}
interface ILabelItemBlock {
label: IIssueLabel;
isDragging: boolean;
customMenuItems: ICustomMenuItem[];
dragHandleProps: DraggableProvidedDragHandleProps;
handleLabelDelete: (label: IIssueLabel) => void;
isLabelGroup?: boolean;
}
export const LabelItemBlock = (props: ILabelItemBlock) => {
const { label, isDragging, customMenuItems, dragHandleProps, handleLabelDelete, isLabelGroup } = props;
//state
const [isMenuActive, setIsMenuActive] = useState(false);
//refs
const actionSectionRef = useRef<HTMLDivElement | null>(null);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
return (
<div className="flex items-center group">
<div className="flex items-center">
<DragHandle isDragging={isDragging} dragHandleProps={dragHandleProps} />
<LabelName color={label.color} name={label.name} isGroup={isLabelGroup ?? false} />
</div>
<div
ref={actionSectionRef}
className={`absolute right-3 flex items-start gap-3.5 px-4 ${
isMenuActive || isLabelGroup
? "opacity-100"
: "opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
} ${isLabelGroup && "-top-0.5"}`}
>
<CustomMenu ellipsis buttonClassName="h-4 w-4 leading-4 text-custom-sidebar-text-400">
{customMenuItems.map(
({ isVisible, onClick, CustomIcon, text }) =>
isVisible && (
<CustomMenu.MenuItem onClick={() => onClick(label)}>
<span className="flex items-center justify-start gap-2">
<CustomIcon className="h-4 w-4" />
<span>{text}</span>
</span>
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
{!isLabelGroup && (
<div className="py-0.5">
<button className="flex h-4 w-4 items-center justify-start gap-2" onClick={() => handleLabelDelete(label)}>
<X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
</button>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,27 @@
import { Component } from "lucide-react";
interface ILabelName {
name: string;
color: string;
isGroup: boolean;
}
export const LabelName = (props: ILabelName) => {
const { name, color, isGroup } = props;
return (
<div className="flex items-center gap-3">
{isGroup ? (
<Component className="h-3.5 w-3.5" color={color} />
) : (
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: color && color !== "" ? color : "#000",
}}
/>
)}
<h6 className="text-sm">{name}</h6>
</div>
);
};

View File

@ -6,12 +6,12 @@ import { Check, ChevronDown, Search } from "lucide-react";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
type Props = { type Props = {
value: string[]; value: string[];
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
labels: IIssueLabels[] | undefined; labels: IIssueLabel[] | undefined;
className?: string; className?: string;
buttonClassName?: string; buttonClassName?: string;
optionsClassName?: string; optionsClassName?: string;

View File

@ -11,12 +11,12 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { LayerStackIcon } from "@plane/ui"; import { LayerStackIcon } from "@plane/ui";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
handleClose: () => void; handleClose: () => void;
parent: IIssueLabels | undefined; parent: IIssueLabel | undefined;
}; };
export const LabelsListModal: React.FC<Props> = observer((props) => { export const LabelsListModal: React.FC<Props> = observer((props) => {
@ -27,7 +27,9 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store // store
const { projectLabel: projectLabelStore, project: projectStore } = useMobxStore(); const {
projectLabel: { projectLabels, fetchProjectLabels, updateLabel },
} = useMobxStore();
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -35,28 +37,24 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
// api call to fetch project details // api call to fetch project details
useSWR( useSWR(
workspaceSlug && projectId ? "PROJECT_LABELS" : null, workspaceSlug && projectId ? "PROJECT_LABELS" : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
: null
); );
// derived values // derived values
const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null; const filteredLabels: IIssueLabel[] =
const filteredLabels: IIssueLabels[] =
query === "" query === ""
? issueLabels ?? [] ? projectLabels ?? []
: issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? []; : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? [];
const handleModalClose = () => { const handleModalClose = () => {
handleClose(); handleClose();
setQuery(""); setQuery("");
}; };
const addChildLabel = async (label: IIssueLabels) => { const addChildLabel = async (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
await projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { await updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
parent: parent?.id!, parent: parent?.id!,
}); });
}; };
@ -108,7 +106,7 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
)} )}
<ul className="text-sm text-gray-700"> <ul className="text-sm text-gray-700">
{filteredLabels.map((label) => { {filteredLabels.map((label) => {
const children = issueLabels?.filter((l) => l.parent === label.id); const children = projectLabels?.filter((l) => l.parent === label.id);
if ( if (
(label.parent === "" || label.parent === null) && // issue does not have any other parent (label.parent === "" || label.parent === null) && // issue does not have any other parent
@ -128,7 +126,6 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
} }
onClick={() => { onClick={() => {
addChildLabel(label); addChildLabel(label);
handleClose();
}} }}
> >
<span <span

View File

@ -1,146 +1,164 @@
import React from "react"; import React, { Dispatch, SetStateAction, useState } from "react";
import { useRouter } from "next/router";
import { Disclosure, Transition } from "@headlessui/react"; import { Disclosure, Transition } from "@headlessui/react";
// store // store
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { CustomMenu } from "@plane/ui";
// icons // icons
import { ChevronDown, Component, Pencil, Plus, Trash2, X } from "lucide-react"; import { ChevronDown, Pencil, Trash2 } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
import {
Draggable,
DraggableProvided,
DraggableProvidedDragHandleProps,
DraggableStateSnapshot,
Droppable,
} from "@hello-pangea/dnd";
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
import { CreateUpdateLabelInline } from "./create-update-label-inline";
import { ProjectSettingLabelItem } from "./project-setting-label-item";
import useDraggableInPortal from "hooks/use-draggable-portal";
type Props = { type Props = {
label: IIssueLabels; label: IIssueLabel;
labelChildren: IIssueLabels[]; labelChildren: IIssueLabel[];
handleLabelDelete: () => void; handleLabelDelete: (label: IIssueLabel) => void;
editLabel: (label: IIssueLabels) => void; dragHandleProps: DraggableProvidedDragHandleProps;
addLabelToGroup: (parentLabel: IIssueLabels) => void; draggableSnapshot: DraggableStateSnapshot;
isUpdating: boolean;
setIsUpdating: Dispatch<SetStateAction<boolean>>;
isDropDisabled: boolean;
}; };
export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => { export const ProjectSettingLabelGroup: React.FC<Props> = observer((props) => {
const { label, labelChildren, addLabelToGroup, editLabel, handleLabelDelete } = props; const {
label,
labelChildren,
handleLabelDelete,
draggableSnapshot: groupDragSnapshot,
dragHandleProps,
isUpdating,
setIsUpdating,
isDropDisabled,
} = props;
// router const [isEditLabelForm, setEditLabelForm] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store const renderDraggable = useDraggableInPortal();
const { projectLabel: projectLabelStore } = useMobxStore();
const removeFromGroup = (label: IIssueLabels) => { const customMenuItems: ICustomMenuItem[] = [
if (!workspaceSlug || !projectId) return; {
CustomIcon: Pencil,
projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { onClick: () => {
parent: null, setEditLabelForm(true);
}); setIsUpdating(true);
}; },
isVisible: true,
text: "Edit label",
},
{
CustomIcon: Trash2,
onClick: handleLabelDelete,
isVisible: true,
text: "Delete label",
},
];
return ( return (
<Disclosure <Disclosure
as="div" as="div"
className="rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3.5 py-3 text-custom-text-100" className={`rounded border-[0.5px] border-custom-border-200 text-custom-text-100 ${
groupDragSnapshot.combineTargetFor ? "bg-custom-background-80" : "bg-custom-background-100"
}`}
defaultOpen defaultOpen
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className="flex cursor-pointer items-center justify-between gap-2"> <Droppable
<div className="flex items-center gap-2"> key={`label.group.droppable.${label.id}`}
<Component className="h-4 w-4 text-custom-text-100 flex-shrink-0" /> droppableId={`label.group.droppable.${label.id}`}
<h6>{label.name}</h6> isCombineEnabled={!groupDragSnapshot.isDragging && !isUpdating}
</div> isDropDisabled={groupDragSnapshot.isDragging || isUpdating || isDropDisabled}
<div className="flex items-center gap-2">
<CustomMenu ellipsis buttonClassName="!text-custom-sidebar-text-400">
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2">
<Plus className="h-4 w-4" />
<span>Add more labels</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleLabelDelete}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-4 w-4" />
<span>Delete label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
<Disclosure.Button>
<span>
<ChevronDown
className={`h-4 w-4 text-custom-sidebar-text-400 ${!open ? "rotate-90 transform" : ""}`}
/>
</span>
</Disclosure.Button>
</div>
</div>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
> >
<Disclosure.Panel> {(droppableProvided) => (
<div className="mt-2.5 ml-6"> <div
{labelChildren.map((child) => ( className={`py-3 pl-1 pr-3 ${!isUpdating && "max-h-full overflow-y-hidden"}`}
<div ref={droppableProvided.innerRef}
key={child.id} {...droppableProvided.droppableProps}
className="group flex items-center justify-between border-b-[0.5px] border-custom-border-200 px-4 py-2.5 text-sm last:border-0" >
> <>
<h5 className="flex items-center gap-3"> <div className="relative flex cursor-pointer items-center justify-between gap-2">
<span {isEditLabelForm ? (
className="h-3.5 w-3.5 flex-shrink-0 rounded-full" <CreateUpdateLabelInline
style={{ labelForm={isEditLabelForm}
backgroundColor: child.color && child.color !== "" ? child.color : "#000000", setLabelForm={setEditLabelForm}
isUpdating={true}
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}} }}
/> />
{child.name} ) : (
</h5> <LabelItemBlock
<div className="flex items-center gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"> label={label}
<div className="h-4 w-4"> isDragging={groupDragSnapshot.isDragging}
<CustomMenu customMenuItems={customMenuItems}
customButton={ dragHandleProps={dragHandleProps}
<div className="h-4 w-4"> handleLabelDelete={handleLabelDelete}
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" /> isLabelGroup={true}
</div> />
} )}
>
<CustomMenu.MenuItem onClick={() => removeFromGroup(child)}>
<span className="flex items-center justify-start gap-2">
<X className="h-4 w-4" />
<span>Remove from group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(child)}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
</div>
<div className="flex items-center"> <Disclosure.Button>
<button className="flex items-center justify-start gap-2" onClick={handleLabelDelete}> <span>
<X className="h-[18px] w-[18px] text-custom-sidebar-text-400 flex-shrink-0" /> <ChevronDown
</button> className={`h-4 w-4 text-custom-sidebar-text-400 ${!open ? "rotate-90 transform" : ""}`}
</div> />
</div> </span>
</Disclosure.Button>
</div> </div>
))} <Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"
enterTo="transform opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Disclosure.Panel>
<div className="mt-2.5 ml-6">
{labelChildren.map((child, index) => (
<div key={child.id} className={`group w-full flex items-center text-sm`}>
<Draggable
draggableId={`label.draggable.${child.id}`}
index={index}
isDragDisabled={groupDragSnapshot.isDragging || isUpdating}
>
{renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
<div className="w-full py-1" ref={provided.innerRef} {...provided.draggableProps}>
<ProjectSettingLabelItem
label={child}
handleLabelDelete={() => handleLabelDelete(child)}
draggableSnapshot={snapshot}
dragHandleProps={provided.dragHandleProps!}
setIsUpdating={setIsUpdating}
isChild
/>
</div>
))}
</Draggable>
</div>
))}
</div>
</Disclosure.Panel>
</Transition>
{droppableProvided.placeholder}
</>
</div> </div>
</Disclosure.Panel> )}
</Transition> </Droppable>
</> </>
)} )}
</Disclosure> </Disclosure>

View File

@ -0,0 +1,91 @@
import React, { Dispatch, SetStateAction, useState } from "react";
import { useRouter } from "next/router";
import { useMobxStore } from "lib/mobx/store-provider";
import { DraggableProvidedDragHandleProps, DraggableStateSnapshot } from "@hello-pangea/dnd";
// types
import { IIssueLabel } from "types";
//icons
import { X, Pencil } from "lucide-react";
//components
import { ICustomMenuItem, LabelItemBlock } from "./label-block/label-item-block";
import { CreateUpdateLabelInline } from "./create-update-label-inline";
type Props = {
label: IIssueLabel;
handleLabelDelete: (label: IIssueLabel) => void;
draggableSnapshot: DraggableStateSnapshot;
dragHandleProps: DraggableProvidedDragHandleProps;
setIsUpdating: Dispatch<SetStateAction<boolean>>;
isChild: boolean;
};
export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const { label, setIsUpdating, handleLabelDelete, draggableSnapshot, dragHandleProps, isChild } = props;
const { combineTargetFor, isDragging } = draggableSnapshot;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store
const { projectLabel: projectLabelStore } = useMobxStore();
//state
const [isEditLabelForm, setEditLabelForm] = useState(false);
const removeFromGroup = (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return;
projectLabelStore.updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, {
parent: null,
});
};
const customMenuItems: ICustomMenuItem[] = [
{
CustomIcon: X,
onClick: removeFromGroup,
isVisible: !!label.parent,
text: "Remove from group",
},
{
CustomIcon: Pencil,
onClick: () => {
setEditLabelForm(true);
setIsUpdating(true);
},
isVisible: true,
text: "Edit label",
},
];
return (
<div
className={`relative group flex items-center justify-between gap-2 space-y-3 rounded border-[0.5px] border-custom-border-200 ${
!isChild && combineTargetFor ? "bg-custom-background-80" : ""
} ${isDragging ? "shadow-custom-shadow-xs bg-custom-background-80" : ""} bg-custom-background-100 px-1 py-2.5`}
>
{isEditLabelForm ? (
<CreateUpdateLabelInline
labelForm={isEditLabelForm}
setLabelForm={setEditLabelForm}
isUpdating={true}
labelToUpdate={label}
onClose={() => {
setEditLabelForm(false);
setIsUpdating(false);
}}
/>
) : (
<LabelItemBlock
label={label}
isDragging={isDragging}
customMenuItems={customMenuItems}
dragHandleProps={dragHandleProps}
handleLabelDelete={handleLabelDelete}
/>
)}
</div>
);
};

View File

@ -1,73 +0,0 @@
import React, { useRef, useState } from "react";
//hook
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui
import { CustomMenu } from "@plane/ui";
// types
import { IIssueLabels } from "types";
//icons
import { Component, X, Pencil } from "lucide-react";
type Props = {
label: IIssueLabels;
addLabelToGroup: (parentLabel: IIssueLabels) => void;
editLabel: (label: IIssueLabels) => void;
handleLabelDelete: () => void;
};
export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const { label, addLabelToGroup, editLabel, handleLabelDelete } = props;
const [isMenuActive, setIsMenuActive] = useState(false);
const actionSectionRef = useRef<HTMLDivElement | null>(null);
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
return (
<div className="relative group flex items-center justify-between gap-2 space-y-3 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-4 py-2.5">
<div className="flex items-center gap-3">
<span
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
}}
/>
<h6 className="text-sm">{label.name}</h6>
</div>
<div
ref={actionSectionRef}
className={`absolute -top-0.5 right-3 flex items-start gap-3.5 pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 ${
isMenuActive ? "opacity-100" : ""
}`}
>
<CustomMenu
customButton={
<div className="h-4 w-4" onClick={() => setIsMenuActive(!isMenuActive)}>
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
</div>
}
>
<CustomMenu.MenuItem onClick={() => addLabelToGroup(label)}>
<span className="flex items-center justify-start gap-2">
<Component className="h-4 w-4 leading-4 text-custom-sidebar-text-400 flex-shrink-0" />
<span>Convert to group</span>
</span>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => editLabel(label)}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-4 w-4" />
<span>Edit label</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>
<div className="py-0.5">
<button className="flex h-4 w-4 items-center justify-start gap-2" onClick={handleLabelDelete}>
<X className="h-4 w-4 text-custom-sidebar-text-400 flex-shrink-0" />
</button>
</div>
</div>
</div>
);
};

View File

@ -1,75 +1,96 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite";
import {
DragDropContext,
Draggable,
DraggableProvided,
DraggableStateSnapshot,
DropResult,
Droppable,
} from "@hello-pangea/dnd";
// store // store
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup } from "components/labels";
CreateUpdateLabelInline,
DeleteLabelModal,
LabelsListModal,
ProjectSettingLabelItem,
ProjectSettingLabelGroup,
} from "components/labels";
// ui // ui
import { Button, Loader } from "@plane/ui"; import { Button, Loader } from "@plane/ui";
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
// images // images
import emptyLabel from "public/empty-state/label.svg"; import emptyLabel from "public/empty-state/label.svg";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
//component
import { ProjectSettingLabelItem } from "./project-setting-label-item";
import useDraggableInPortal from "hooks/use-draggable-portal";
const LABELS_ROOT = "labels.root";
export const ProjectSettingsLabelList: React.FC = observer(() => { export const ProjectSettingsLabelList: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const renderDraggable = useDraggableInPortal();
// store // store
const { project: projectStore } = useMobxStore(); const {
projectLabel: { fetchProjectLabels, projectLabels, updateLabelPosition, projectLabelsTree },
} = useMobxStore();
// states // states
const [labelForm, setLabelForm] = useState(false); const [showLabelForm, setLabelForm] = useState(false);
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [labelsListModal, setLabelsListModal] = useState(false); const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabel | null>(null);
const [labelToUpdate, setLabelToUpdate] = useState<IIssueLabels | null>(null); const [isDraggingGroup, setIsDraggingGroup] = useState(false);
const [parentLabel, setParentLabel] = useState<IIssueLabels | undefined>(undefined);
const [selectDeleteLabel, setSelectDeleteLabel] = useState<IIssueLabels | null>(null);
// ref // ref
const scrollToRef = useRef<HTMLFormElement>(null); const scrollToRef = useRef<HTMLFormElement>(null);
// api call to fetch project details // api call to fetch project details
useSWR( useSWR(
workspaceSlug && projectId ? "PROJECT_LABELS" : null, workspaceSlug && projectId ? "PROJECT_LABELS" : null,
workspaceSlug && projectId workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null
? () => projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString())
: null
); );
// derived values
const issueLabels = projectStore.labels?.[projectId?.toString()!] ?? null;
const newLabel = () => { const newLabel = () => {
setIsUpdating(false); setIsUpdating(false);
setLabelForm(true); setLabelForm(true);
}; };
const addLabelToGroup = (parentLabel: IIssueLabels) => { const onDragEnd = (result: DropResult) => {
setLabelsListModal(true); const { combine, draggableId, destination, source } = result;
setParentLabel(parentLabel);
};
const editLabel = (label: IIssueLabels) => { // return if dropped outside the DragDropContext
setLabelForm(true); if (!combine && !destination) return;
setIsUpdating(true);
setLabelToUpdate(label); const childLabel = draggableId.split(".")[2];
let parentLabel: string | undefined | null = destination?.droppableId?.split(".")[3];
const index = destination?.index || 0;
const prevParentLabel: string | undefined | null = source?.droppableId?.split(".")[3];
const prevIndex = source?.index;
if (combine && combine.draggableId) parentLabel = combine?.draggableId?.split(".")[2];
if (destination?.droppableId === LABELS_ROOT) parentLabel = null;
if (result.reason == "DROP" && childLabel != parentLabel) {
updateLabelPosition(
workspaceSlug?.toString()!,
projectId?.toString()!,
childLabel,
parentLabel,
index,
prevParentLabel == parentLabel,
prevIndex
);
return;
}
}; };
return ( return (
<> <>
<LabelsListModal isOpen={labelsListModal} parent={parentLabel} handleClose={() => setLabelsListModal(false)} />
<DeleteLabelModal <DeleteLabelModal
isOpen={!!selectDeleteLabel} isOpen={!!selectDeleteLabel}
data={selectDeleteLabel ?? null} data={selectDeleteLabel ?? null}
@ -82,64 +103,105 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
Add label Add label
</Button> </Button>
</div> </div>
<div className="space-y-3 py-6 h-full w-full"> <div className="w-full">
{labelForm && ( {showLabelForm && (
<CreateUpdateLabelInline <div className="w-full rounded border border-custom-border-200 px-3.5 py-2">
labelForm={labelForm} <CreateUpdateLabelInline
setLabelForm={setLabelForm} labelForm={showLabelForm}
isUpdating={isUpdating} setLabelForm={setLabelForm}
labelToUpdate={labelToUpdate} isUpdating={isUpdating}
ref={scrollToRef} ref={scrollToRef}
onClose={() => { onClose={() => {
setLabelForm(false); setLabelForm(false);
setIsUpdating(false); setIsUpdating(false);
setLabelToUpdate(null); }}
}} />
/> </div>
)} )}
{/* labels */} {/* labels */}
{issueLabels && <>
issueLabels.map((label) => { {projectLabelsTree && (
const children = issueLabels?.filter((l) => l.parent === label.id); <DragDropContext
onDragEnd={onDragEnd}
autoScrollerOptions={{
startFromPercentage: 1,
disabled: false,
maxScrollAtPercentage: 0,
maxPixelScroll: 2,
}}
>
<Droppable
droppableId={LABELS_ROOT}
isCombineEnabled={!isDraggingGroup}
ignoreContainerClipping={true}
isDropDisabled={isUpdating}
>
{(droppableProvided, droppableSnapshot) => (
<div className={`mt-3`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
{projectLabelsTree.map((label, index) => {
if (label.children && label.children.length) {
return (
<Draggable
key={`label.draggable.${label.id}`}
draggableId={`label.draggable.${label.id}.group`}
index={index}
isDragDisabled={isUpdating}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const isGroup = droppableSnapshot.draggingFromThisWith?.split(".")[3] === "group";
setIsDraggingGroup(isGroup);
if (children && children.length === 0) { return (
if (!label.parent) <div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
return ( <ProjectSettingLabelGroup
<ProjectSettingLabelItem key={label.id}
key={label.id} label={label}
label={label} labelChildren={label.children || []}
addLabelToGroup={() => addLabelToGroup(label)} isDropDisabled={isGroup}
editLabel={(label) => { dragHandleProps={provided.dragHandleProps!}
editLabel(label); handleLabelDelete={(label: IIssueLabel) => setSelectDeleteLabel(label)}
scrollToRef.current?.scrollIntoView({ draggableSnapshot={snapshot}
behavior: "smooth", isUpdating={isUpdating}
}); setIsUpdating={setIsUpdating}
}} />
handleLabelDelete={() => setSelectDeleteLabel(label)} </div>
/> );
); }}
} else { </Draggable>
return ( );
<ProjectSettingLabelGroup }
key={label.id} return (
label={label} <Draggable
labelChildren={children} key={`label.draggable.${label.id}`}
addLabelToGroup={addLabelToGroup} draggableId={`label.draggable.${label.id}`}
editLabel={(label) => { index={index}
editLabel(label); isDragDisabled={isUpdating}
scrollToRef.current?.scrollIntoView({ >
behavior: "smooth", {renderDraggable((provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
}); <div ref={provided.innerRef} {...provided.draggableProps} className="mt-3">
}} <ProjectSettingLabelItem
handleLabelDelete={() => setSelectDeleteLabel(label)} dragHandleProps={provided.dragHandleProps!}
/> draggableSnapshot={snapshot}
); label={label}
} setIsUpdating={setIsUpdating}
})} handleLabelDelete={(label) => setSelectDeleteLabel(label)}
isChild={false}
/>
</div>
))}
</Draggable>
);
})}
{droppableProvided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
</>
{/* loading state */} {/* loading state */}
{!issueLabels && ( {!projectLabels && (
<Loader className="space-y-5"> <Loader className="space-y-5">
<Loader.Item height="42px" /> <Loader.Item height="42px" />
<Loader.Item height="42px" /> <Loader.Item height="42px" />
@ -149,7 +211,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
)} )}
{/* empty state */} {/* empty state */}
{issueLabels && issueLabels.length === 0 && ( {projectLabels && projectLabels.length === 0 && (
<EmptyState <EmptyState
title="No labels yet" title="No labels yet"
description="Create labels to help organize and filter issues in you project" description="Create labels to help organize and filter issues in you project"

View File

@ -24,7 +24,6 @@ export const ProjectMemberList: React.FC = observer(() => {
// store // store
const { const {
project: projectStore,
projectMember: { projectMembers, fetchProjectMembers }, projectMember: { projectMembers, fetchProjectMembers },
} = useMobxStore(); } = useMobxStore();

View File

@ -2,10 +2,10 @@ import { FC } from "react";
// ui // ui
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// types // types
import { IIssueLabels } from "types"; import { IIssueLabel } from "types";
type IssueLabelsListProps = { type IssueLabelsListProps = {
labels?: (IIssueLabels | undefined)[]; labels?: (IIssueLabel | undefined)[];
length?: number; length?: number;
showLength?: boolean; showLength?: boolean;
}; };

View File

@ -27,7 +27,7 @@ const defaultValues: Partial<IProjectView> = {
export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, handleClose, data, preLoadedData }) => { export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, handleClose, data, preLoadedData }) => {
const { const {
project: projectStore, projectLabel: { projectLabels },
projectState: projectStateStore, projectState: projectStateStore,
projectMember: { projectMembers }, projectMember: { projectMembers },
} = useMobxStore(); } = useMobxStore();
@ -167,7 +167,7 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
}); });
}} }}
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.list} layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.list}
labels={projectStore.projectLabels ?? undefined} labels={projectLabels ?? undefined}
members={projectMembers?.map((m) => m.member) ?? undefined} members={projectMembers?.map((m) => m.member) ?? undefined}
states={projectStateStore.projectStates ?? undefined} states={projectStateStore.projectStates ?? undefined}
/> />
@ -181,7 +181,7 @@ export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, ha
appliedFilters={selectedFilters} appliedFilters={selectedFilters}
handleClearAllFilters={clearAllFilters} handleClearAllFilters={clearAllFilters}
handleRemoveFilter={handleRemoveFilter} handleRemoveFilter={handleRemoveFilter}
labels={projectStore.projectLabels ?? []} labels={projectLabels ?? []}
members={projectMembers?.map((m) => m.member) ?? []} members={projectMembers?.map((m) => m.member) ?? []}
states={projectStateStore.projectStates ?? []} states={projectStateStore.projectStates ?? []}
/> />

View File

@ -1,3 +1,5 @@
import { IIssueLabelTree } from "types";
export const groupBy = (array: any[], key: string) => { export const groupBy = (array: any[], key: string) => {
const innerKey = key.split("."); // split the key by dot const innerKey = key.split("."); // split the key by dot
return array.reduce((result, currentValue) => { return array.reduce((result, currentValue) => {
@ -74,3 +76,17 @@ export const orderGroupedDataByField = <T>(groupedData: GroupedItems<T>, orderBy
} }
return groupedData; return groupedData;
}; };
export const buildTree = (array: any[], parent = null) => {
const tree: IIssueLabelTree[] = [];
array.forEach((item: any) => {
if (item.parent === parent) {
const children = buildTree(array, item.id);
item.children = children;
tree.push(item);
}
});
return tree;
};

View File

@ -0,0 +1,31 @@
import { createPortal } from "react-dom";
import { useEffect, useRef } from "react";
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
const useDraggableInPortal = () => {
const self = useRef<Element>();
useEffect(() => {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.pointerEvents = "none";
div.style.top = "0";
div.style.width = "100%";
div.style.height = "100%";
self.current = div;
document.body.appendChild(div);
return () => {
document.body.removeChild(div);
};
}, [self.current]);
return (render: any) => (provided: DraggableProvided, snapshot: DraggableStateSnapshot) => {
const element = render(provided, snapshot);
if (self.current && snapshot?.isDragging) {
return createPortal(element, self.current);
}
return element;
};
};
export default useDraggableInPortal;

View File

@ -20,7 +20,8 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
// store // store
const { const {
user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject }, user: { fetchUserProjectInfo, projectMemberInfo, hasPermissionToProject },
project: { fetchProjectDetails, fetchProjectLabels, fetchProjectEstimates, workspaceProjects }, project: { fetchProjectDetails, fetchProjectEstimates, workspaceProjects },
projectLabel: { fetchProjectLabels },
projectMember: { fetchProjectMembers }, projectMember: { fetchProjectMembers },
projectState: { fetchProjectStates }, projectState: { fetchProjectStates },
cycle: { fetchCycles }, cycle: { fetchCycles },

View File

@ -33,7 +33,7 @@ import { copyTextToClipboard } from "helpers/string.helper";
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types"; import { IIssueLabel, IPage, IPageBlock, IProjectMember } from "types";
// fetch-keys // fetch-keys
import { import {
PAGE_BLOCKS_LIST, PAGE_BLOCKS_LIST,
@ -86,7 +86,7 @@ const PageDetailsPage: NextPageWithLayout = () => {
: null : null
); );
const { data: labels } = useSWR<IIssueLabels[]>( const { data: labels } = useSWR<IIssueLabel[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId
? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string) ? () => issueLabelService.getProjectIssueLabels(workspaceSlug as string, projectId as string)

View File

@ -3,7 +3,7 @@ import { API_BASE_URL } from "helpers/common.helper";
import { APIService } from "services/api.service"; import { APIService } from "services/api.service";
import { TrackEventService } from "services/track_event.service"; import { TrackEventService } from "services/track_event.service";
// types // types
import { IIssueLabels, IUser } from "types"; import { IIssueLabel, IUser } from "types";
const trackEventServices = new TrackEventService(); const trackEventServices = new TrackEventService();
@ -12,7 +12,7 @@ export class IssueLabelService extends APIService {
super(API_BASE_URL); super(API_BASE_URL);
} }
async getWorkspaceIssueLabels(workspaceSlug: string): Promise<IIssueLabels[]> { async getWorkspaceIssueLabels(workspaceSlug: string): Promise<IIssueLabel[]> {
return this.get(`/api/workspaces/${workspaceSlug}/labels/`) return this.get(`/api/workspaces/${workspaceSlug}/labels/`)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -20,7 +20,7 @@ export class IssueLabelService extends APIService {
}); });
} }
async getProjectIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabels[]> { async getProjectIssueLabels(workspaceSlug: string, projectId: string): Promise<IIssueLabel[]> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`) return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
@ -33,9 +33,9 @@ export class IssueLabelService extends APIService {
projectId: string, projectId: string,
data: any, data: any,
user: IUser | undefined user: IUser | undefined
): Promise<IIssueLabels> { ): Promise<IIssueLabel> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-labels/`, data)
.then((response: { data: IIssueLabels; [key: string]: any }) => { .then((response: { data: IIssueLabel; [key: string]: any }) => {
trackEventServices.trackIssueLabelEvent( trackEventServices.trackIssueLabelEvent(
{ {
workSpaceId: response?.data?.workspace_detail?.id, workSpaceId: response?.data?.workspace_detail?.id,

View File

@ -1,30 +1,49 @@
import { observable, action, makeObservable, runInAction } from "mobx"; import { observable, action, makeObservable, runInAction, computed } from "mobx";
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IIssueLabels } from "types"; import { IIssueLabel, IIssueLabelTree } from "types";
// services // services
import { IssueLabelService } from "services/issue"; import { IssueLabelService } from "services/issue";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
import { buildTree } from "helpers/array.helper";
export interface IProjectLabelStore { export interface IProjectLabelStore {
loader: boolean; loader: boolean;
error: any | null; error: any | null;
labels: {
// labels [projectId: string]: IIssueLabel[] | null; // project_id: labels
createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabels>) => Promise<IIssueLabels>; } | null;
// computed
projectLabels: IIssueLabel[] | null;
projectLabelsTree: IIssueLabelTree[] | null;
// actions
getProjectLabelById: (labelId: string) => IIssueLabel | null;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<void>;
createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
updateLabel: ( updateLabel: (
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
labelId: string, labelId: string,
data: Partial<IIssueLabels> data: Partial<IIssueLabel>
) => Promise<IIssueLabels>; ) => Promise<IIssueLabel>;
updateLabelPosition: (
workspaceSlug: string,
projectId: string,
labelId: string,
parentId: string | null | undefined,
index: number,
isSameParent: boolean,
prevIndex: number | undefined
) => Promise<IIssueLabel>;
deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>; deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>;
} }
export class ProjectLabelStore implements IProjectLabelStore { export class ProjectLabelStore implements IProjectLabelStore {
loader: boolean = false; loader: boolean = false;
error: any | null = null; error: any | null = null;
labels: {
[projectId: string]: IIssueLabel[]; // projectId: labels
} | null = {};
// root store // root store
rootStore; rootStore;
// service // service
@ -34,12 +53,18 @@ export class ProjectLabelStore implements IProjectLabelStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// observable // observable
loader: observable, loader: observable.ref,
error: observable, error: observable.ref,
labels: observable.ref,
// labels // computed
projectLabels: computed,
projectLabelsTree: computed,
// actions
getProjectLabelById: action,
fetchProjectLabels: action,
createLabel: action, createLabel: action,
updateLabel: action, updateLabel: action,
updateLabelPosition: action,
deleteLabel: action, deleteLabel: action,
}); });
@ -48,7 +73,51 @@ export class ProjectLabelStore implements IProjectLabelStore {
this.issueLabelService = new IssueLabelService(); this.issueLabelService = new IssueLabelService();
} }
createLabel = async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabels>) => { get projectLabels() {
if (!this.rootStore.project.projectId) return null;
return this.labels?.[this.rootStore.project.projectId]?.sort((a, b) => a.name.localeCompare(b.name)) || null;
}
get projectLabelsTree() {
if (!this.rootStore.project.projectId) return null;
const currentProjectLabels = this.labels?.[this.rootStore.project.projectId];
if (!currentProjectLabels) return null;
currentProjectLabels.sort((labelA: IIssueLabel, labelB: IIssueLabel) => labelB.sort_order - labelA.sort_order);
return buildTree(currentProjectLabels);
}
getProjectLabelById = (labelId: string) => {
if (!this.rootStore.project.projectId) return null;
const labels = this.projectLabels;
if (!labels) return null;
const labelInfo: IIssueLabel | null = labels.find((label) => label.id === labelId) || null;
return labelInfo;
};
fetchProjectLabels = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const labelResponse = await this.issueLabelService.getProjectIssueLabels(workspaceSlug, projectId);
runInAction(() => {
this.labels = {
...this.labels,
[projectId]: labelResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error(error);
this.loader = false;
this.error = error;
}
};
createLabel = async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
try { try {
const response = await this.issueLabelService.createIssueLabel( const response = await this.issueLabelService.createIssueLabel(
workspaceSlug, workspaceSlug,
@ -58,9 +127,9 @@ export class ProjectLabelStore implements IProjectLabelStore {
); );
runInAction(() => { runInAction(() => {
this.rootStore.project.labels = { this.labels = {
...this.rootStore.project.labels, ...this.labels,
[projectId]: [response, ...(this.rootStore.project.labels?.[projectId] || [])], [projectId]: [response, ...(this.labels?.[projectId] || [])],
}; };
}); });
@ -71,16 +140,70 @@ export class ProjectLabelStore implements IProjectLabelStore {
} }
}; };
updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial<IIssueLabels>) => { updateLabelPosition = async (
const originalLabel = this.rootStore.project.getProjectLabelById(labelId); workspaceSlug: string,
projectId: string,
labelId: string,
parentId: string | null | undefined,
index: number,
isSameParent: boolean,
prevIndex: number | undefined
) => {
const labels = this.labels;
const currLabel = labels?.[projectId]?.find((label) => label.id === labelId);
const labelTree = this.projectLabelsTree;
let currentArray: IIssueLabel[];
if (!currLabel || !labelTree) return;
const data: Partial<IIssueLabel> = { parent: parentId };
//find array in which the label is to be added
if (!parentId) currentArray = labelTree;
else currentArray = labelTree?.find((label) => label.id === parentId)?.children || [];
//Add the array at the destination
if (isSameParent && prevIndex !== undefined) currentArray.splice(prevIndex, 1);
currentArray.splice(index, 0, currLabel);
//if currently adding to a new array, then let backend assign a sort order
if (currentArray.length > 1) {
let prevSortOrder: number | undefined, nextSortOrder: number | undefined;
if (typeof currentArray[index - 1] !== "undefined") {
prevSortOrder = currentArray[index - 1].sort_order;
}
if (typeof currentArray[index + 1] !== "undefined") {
nextSortOrder = currentArray[index + 1].sort_order;
}
let sortOrder: number;
//based on the next and previous labels calculate current sort order
if (prevSortOrder && nextSortOrder) {
sortOrder = (prevSortOrder + nextSortOrder) / 2;
} else if (nextSortOrder) {
sortOrder = nextSortOrder + 10000;
} else {
sortOrder = prevSortOrder! / 2;
}
data.sort_order = sortOrder;
}
return this.updateLabel(workspaceSlug, projectId, labelId, data);
};
updateLabel = async (workspaceSlug: string, projectId: string, labelId: string, data: Partial<IIssueLabel>) => {
const originalLabel = this.getProjectLabelById(labelId);
runInAction(() => { runInAction(() => {
this.rootStore.project.labels = { this.labels = {
...this.rootStore.project.labels, ...this.labels,
[projectId]: [projectId]:
this.rootStore.project.labels?.[projectId]?.map((label) => this.labels?.[projectId]?.map((label) => (label.id === labelId ? { ...label, ...data } : label)) || [],
label.id === labelId ? { ...label, ...data } : label
) || [],
}; };
}); });
@ -97,9 +220,9 @@ export class ProjectLabelStore implements IProjectLabelStore {
} catch (error) { } catch (error) {
console.log("Failed to update label from project store"); console.log("Failed to update label from project store");
runInAction(() => { runInAction(() => {
this.rootStore.project.labels = { this.labels = {
...this.rootStore.project.labels, ...this.labels,
[projectId]: (this.rootStore.project.labels?.[projectId] || [])?.map((label) => [projectId]: (this.labels?.[projectId] || [])?.map((label) =>
label.id === labelId ? { ...label, ...originalLabel } : label label.id === labelId ? { ...label, ...originalLabel } : label
), ),
}; };
@ -109,12 +232,12 @@ export class ProjectLabelStore implements IProjectLabelStore {
}; };
deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => { deleteLabel = async (workspaceSlug: string, projectId: string, labelId: string) => {
const originalLabelList = this.rootStore.project.projectLabels; const originalLabelList = this.projectLabels;
runInAction(() => { runInAction(() => {
this.rootStore.project.labels = { this.labels = {
...this.rootStore.project.labels, ...this.labels,
[projectId]: (this.rootStore.project.labels?.[projectId] || [])?.filter((label) => label.id !== labelId), [projectId]: (this.labels?.[projectId] || [])?.filter((label) => label.id !== labelId),
}; };
}); });
@ -130,8 +253,8 @@ export class ProjectLabelStore implements IProjectLabelStore {
console.log("Failed to delete label from project store"); console.log("Failed to delete label from project store");
// reverting back to original label list // reverting back to original label list
runInAction(() => { runInAction(() => {
this.rootStore.project.labels = { this.labels = {
...this.rootStore.project.labels, ...this.labels,
[projectId]: originalLabelList || [], [projectId]: originalLabelList || [],
}; };
}); });

View File

@ -1,7 +1,7 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { observable, action, computed, makeObservable, runInAction } from "mobx";
// types // types
import { RootStore } from "../root"; import { RootStore } from "../root";
import { IProject, IIssueLabels, IEstimate } from "types"; import { IProject, IEstimate } from "types";
// services // services
import { ProjectService, ProjectStateService, ProjectEstimateService } from "services/project"; import { ProjectService, ProjectStateService, ProjectEstimateService } from "services/project";
import { IssueService, IssueLabelService } from "services/issue"; import { IssueService, IssueLabelService } from "services/issue";
@ -16,9 +16,6 @@ export interface IProjectStore {
project_details: { project_details: {
[projectId: string]: IProject; // projectId: project Info [projectId: string]: IProject; // projectId: project Info
}; };
labels: {
[projectId: string]: IIssueLabels[] | null; // project_id: labels
} | null;
estimates: { estimates: {
[projectId: string]: IEstimate[] | null; // project_id: members [projectId: string]: IEstimate[] | null; // project_id: members
} | null; } | null;
@ -26,12 +23,9 @@ export interface IProjectStore {
// computed // computed
searchedProjects: IProject[]; searchedProjects: IProject[];
workspaceProjects: IProject[] | null; workspaceProjects: IProject[] | null;
projectLabels: IIssueLabels[] | null;
projectEstimates: IEstimate[] | null; projectEstimates: IEstimate[] | null;
joinedProjects: IProject[]; joinedProjects: IProject[];
favoriteProjects: IProject[]; favoriteProjects: IProject[];
currentProjectDetails: IProject | undefined; currentProjectDetails: IProject | undefined;
// actions // actions
@ -39,12 +33,10 @@ export interface IProjectStore {
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
getProjectById: (workspaceSlug: string, projectId: string) => IProject | null; getProjectById: (workspaceSlug: string, projectId: string) => IProject | null;
getProjectLabelById: (labelId: string) => IIssueLabels | null;
getProjectEstimateById: (estimateId: string) => IEstimate | null;
getProjectEstimateById: (estimateId: string) => IEstimate | null;
fetchProjects: (workspaceSlug: string) => Promise<void>; fetchProjects: (workspaceSlug: string) => Promise<void>;
fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise<any>;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<void>;
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<any>; fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<any>;
addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>; addProjectToFavorites: (workspaceSlug: string, projectId: string) => Promise<any>;
@ -70,9 +62,6 @@ export class ProjectStore implements IProjectStore {
project_details: { project_details: {
[projectId: string]: IProject; // projectId: project [projectId: string]: IProject; // projectId: project
} = {}; } = {};
labels: {
[projectId: string]: IIssueLabels[]; // projectId: labels
} | null = {};
estimates: { estimates: {
[projectId: string]: IEstimate[]; // projectId: estimates [projectId: string]: IEstimate[]; // projectId: estimates
} | null = {}; } | null = {};
@ -96,13 +85,13 @@ export class ProjectStore implements IProjectStore {
projectId: observable.ref, projectId: observable.ref,
projects: observable.ref, projects: observable.ref,
project_details: observable.ref, project_details: observable.ref,
labels: observable.ref,
estimates: observable.ref, estimates: observable.ref,
// computed // computed
searchedProjects: computed, searchedProjects: computed,
workspaceProjects: computed, workspaceProjects: computed,
projectLabels: computed,
projectEstimates: computed, projectEstimates: computed,
currentProjectDetails: computed, currentProjectDetails: computed,
@ -117,10 +106,8 @@ export class ProjectStore implements IProjectStore {
fetchProjectDetails: action, fetchProjectDetails: action,
getProjectById: action, getProjectById: action,
getProjectLabelById: action,
getProjectEstimateById: action, getProjectEstimateById: action,
fetchProjectLabels: action,
fetchProjectEstimates: action, fetchProjectEstimates: action,
addProjectToFavorites: action, addProjectToFavorites: action,
@ -177,11 +164,6 @@ export class ProjectStore implements IProjectStore {
return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_favorite); return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_favorite);
} }
get projectLabels() {
if (!this.projectId) return null;
return this.labels?.[this.projectId] || null;
}
get projectEstimates() { get projectEstimates() {
if (!this.projectId) return null; if (!this.projectId) return null;
return this.estimates?.[this.projectId] || null; return this.estimates?.[this.projectId] || null;
@ -241,14 +223,6 @@ export class ProjectStore implements IProjectStore {
return projectInfo; return projectInfo;
}; };
getProjectLabelById = (labelId: string) => {
if (!this.projectId) return null;
const labels = this.projectLabels;
if (!labels) return null;
const labelInfo: IIssueLabels | null = labels.find((label) => label.id === labelId) || null;
return labelInfo;
};
getProjectEstimateById = (estimateId: string) => { getProjectEstimateById = (estimateId: string) => {
if (!this.projectId) return null; if (!this.projectId) return null;
const estimates = this.projectEstimates; const estimates = this.projectEstimates;
@ -257,28 +231,6 @@ export class ProjectStore implements IProjectStore {
return estimateInfo; return estimateInfo;
}; };
fetchProjectLabels = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
const labelResponse = await this.issueLabelService.getProjectIssueLabels(workspaceSlug, projectId);
runInAction(() => {
this.labels = {
...this.labels,
[projectId]: labelResponse,
};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error(error);
this.loader = false;
this.error = error;
}
};
fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => { fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => {
try { try {
this.loader = true; this.loader = true;

View File

@ -1,7 +1,7 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { RootStore } from "../root"; import { RootStore } from "../root";
// types // types
import { IIssueLabels, IProject, IWorkspace, IWorkspaceMember } from "types"; import { IIssueLabel, IProject, IWorkspace, IWorkspaceMember } from "types";
// services // services
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
import { ProjectService } from "services/project"; import { ProjectService } from "services/project";
@ -15,12 +15,12 @@ export interface IWorkspaceStore {
// observables // observables
workspaceSlug: string | null; workspaceSlug: string | null;
workspaces: IWorkspace[] | undefined; workspaces: IWorkspace[] | undefined;
labels: { [workspaceSlug: string]: IIssueLabels[] }; // workspaceSlug: labels[] labels: { [workspaceSlug: string]: IIssueLabel[] }; // workspaceSlug: labels[]
// actions // actions
setWorkspaceSlug: (workspaceSlug: string) => void; setWorkspaceSlug: (workspaceSlug: string) => void;
getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null; getWorkspaceBySlug: (workspaceSlug: string) => IWorkspace | null;
getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabels | null; getWorkspaceLabelById: (workspaceSlug: string, labelId: string) => IIssueLabel | null;
fetchWorkspaces: () => Promise<IWorkspace[]>; fetchWorkspaces: () => Promise<IWorkspace[]>;
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>; fetchWorkspaceLabels: (workspaceSlug: string) => Promise<void>;
@ -32,7 +32,7 @@ export interface IWorkspaceStore {
// computed // computed
currentWorkspace: IWorkspace | null; currentWorkspace: IWorkspace | null;
workspacesCreateByCurrentUser: IWorkspace[] | null; workspacesCreateByCurrentUser: IWorkspace[] | null;
workspaceLabels: IIssueLabels[] | null; workspaceLabels: IIssueLabel[] | null;
} }
export class WorkspaceStore implements IWorkspaceStore { export class WorkspaceStore implements IWorkspaceStore {
@ -44,7 +44,7 @@ export class WorkspaceStore implements IWorkspaceStore {
workspaceSlug: string | null = null; workspaceSlug: string | null = null;
workspaces: IWorkspace[] | undefined = []; workspaces: IWorkspace[] | undefined = [];
projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[] projects: { [workspaceSlug: string]: IProject[] } = {}; // workspaceSlug: project[]
labels: { [workspaceSlug: string]: IIssueLabels[] } = {}; labels: { [workspaceSlug: string]: IIssueLabel[] } = {};
members: { [workspaceSlug: string]: IWorkspaceMember[] } = {}; members: { [workspaceSlug: string]: IWorkspaceMember[] } = {};
// services // services

View File

@ -159,7 +159,7 @@ export type IssuePriorities = {
user: string; user: string;
}; };
export interface IIssueLabels { export interface IIssueLabel {
id: string; id: string;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
@ -173,6 +173,11 @@ export interface IIssueLabels {
workspace: string; workspace: string;
workspace_detail: IWorkspaceLite; workspace_detail: IWorkspaceLite;
parent: string | null; parent: string | null;
sort_order: number;
}
export interface IIssueLabelTree extends IIssueLabel {
children: IIssueLabel[] | undefined;
} }
export interface IIssueActivity { export interface IIssueActivity {

View File

@ -1,5 +1,5 @@
// types // types
import { IIssue, IIssueLabels, IWorkspaceLite, IProjectLite } from "types"; import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types";
export interface IPage { export interface IPage {
access: number; access: number;
@ -12,7 +12,7 @@ export interface IPage {
description_stripped: string | null; description_stripped: string | null;
id: string; id: string;
is_favorite: boolean; is_favorite: boolean;
label_details: IIssueLabels[]; label_details: IIssueLabel[];
labels: string[]; labels: string[];
name: string; name: string;
owned_by: string; owned_by: string;