forked from github/plane
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:
parent
d933c73343
commit
63b6150b9c
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -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() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -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() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -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() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -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() ?? ""]}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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" />}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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) => {
|
||||||
|
@ -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";
|
||||||
|
24
web/components/labels/label-block/drag-handle.tsx
Normal file
24
web/components/labels/label-block/drag-handle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
80
web/components/labels/label-block/label-item-block.tsx
Normal file
80
web/components/labels/label-block/label-item-block.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
27
web/components/labels/label-block/label-name.tsx
Normal file
27
web/components/labels/label-block/label-name.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
91
web/components/labels/project-setting-label-item.tsx
Normal file
91
web/components/labels/project-setting-label-item.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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"
|
||||||
|
@ -24,7 +24,6 @@ export const ProjectMemberList: React.FC = observer(() => {
|
|||||||
|
|
||||||
// store
|
// store
|
||||||
const {
|
const {
|
||||||
project: projectStore,
|
|
||||||
projectMember: { projectMembers, fetchProjectMembers },
|
projectMember: { projectMembers, fetchProjectMembers },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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 ?? []}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
31
web/hooks/use-draggable-portal.ts
Normal file
31
web/hooks/use-draggable-portal.ts
Normal 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;
|
@ -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 },
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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 || [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
7
web/types/issues.d.ts
vendored
7
web/types/issues.d.ts
vendored
@ -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 {
|
||||||
|
4
web/types/pages.d.ts
vendored
4
web/types/pages.d.ts
vendored
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user