[WEB-4951] [WEB-4884] feat: work item filters revamp (#7810)
This commit is contained in:
parent
e6a7ca4c72
commit
9aef5d4aa9
160 changed files with 5879 additions and 4881 deletions
|
|
@ -1,53 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Layers } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import { ETabIndices, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmojiPicker, EmojiIconPickerTypes } from "@plane/propel/emoji-icon-picker";
|
||||
// types
|
||||
import {
|
||||
EViewAccess,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IProjectView,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
IIssueFilters,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { Logo } from "@/components/common/logo";
|
||||
import {
|
||||
AppliedFiltersList,
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
// plane web imports
|
||||
import { AccessController } from "@/plane-web/components/views/access-controller";
|
||||
// local imports
|
||||
import { LayoutDropDown } from "../dropdowns/layout";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "../work-item-filters/filters-hoc/project-level";
|
||||
|
||||
type Props = {
|
||||
data?: IProjectView | null;
|
||||
handleClose: () => void;
|
||||
handleFormSubmit: (values: IProjectView) => Promise<void>;
|
||||
preLoadedData?: Partial<IProjectView> | null;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IProjectView> = {
|
||||
const DEFAULT_VALUES: Partial<IProjectView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
access: EViewAccess.PUBLIC,
|
||||
|
|
@ -56,23 +50,24 @@ const defaultValues: Partial<IProjectView> = {
|
|||
};
|
||||
|
||||
export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData, projectId, workspaceSlug } = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const { getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// form info
|
||||
const defaultValues = {
|
||||
...DEFAULT_VALUES,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
};
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
|
|
@ -80,53 +75,23 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
} = useForm<IProjectView>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const logoValue = watch("logo_props");
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
|
||||
|
||||
const selectedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(watch("filters") ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
selectedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
// for removing filters from a key
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
// If value is null then remove all the filters of that key
|
||||
if (!value) {
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newValues = selectedFilters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (newValues.includes(val)) newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (selectedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
}
|
||||
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: newValues,
|
||||
});
|
||||
const workItemFilters: IIssueFilters = {
|
||||
richFilters: getValues("rich_filters"),
|
||||
displayFilters: getValues("display_filters"),
|
||||
displayProperties: getValues("display_properties"),
|
||||
kanbanFilters: undefined,
|
||||
};
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
|
||||
|
||||
const handleCreateUpdateView = async (formData: IProjectView) => {
|
||||
await handleFormSubmit({
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
logo_props: formData.logo_props,
|
||||
filters: formData.filters,
|
||||
rich_filters: formData.rich_filters,
|
||||
display_filters: formData.display_filters,
|
||||
display_properties: formData.display_properties,
|
||||
access: formData.access,
|
||||
|
|
@ -137,20 +102,6 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
if (!selectedFilters) return;
|
||||
|
||||
setValue("filters", {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
});
|
||||
}, [data, preLoadedData, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||
<div className="space-y-5 p-5">
|
||||
|
|
@ -263,43 +214,6 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
value={displayFilters.layout}
|
||||
/>
|
||||
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="filters"
|
||||
render={({ field: { onChange, value: filters } }) => (
|
||||
<FiltersDropdown title={t("common.filters")} tabIndex={getIndex("filters")}>
|
||||
<FilterSelection
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={(key, value) => {
|
||||
const newValues = filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...filters,
|
||||
[key]: newValues,
|
||||
});
|
||||
}}
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[displayFilters.layout]}
|
||||
labels={projectLabels ?? undefined}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* display filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -307,7 +221,9 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
render={({ field: { onChange: onDisplayPropertiesChange, value: displayProperties } }) => (
|
||||
<FiltersDropdown title={t("common.display")}>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[displayFilters.layout]}
|
||||
layoutDisplayFiltersOptions={
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[displayFilters.layout]
|
||||
}
|
||||
displayFilters={displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
onDisplayFiltersChange({
|
||||
|
|
@ -324,8 +240,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
...updatedDisplayProperties,
|
||||
});
|
||||
}}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
cycleViewDisabled={!projectDetails?.cycle_view}
|
||||
moduleViewDisabled={!projectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
|
|
@ -334,17 +250,31 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{selectedFilters && Object.keys(selectedFilters).length > 0 && (
|
||||
<div>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={selectedFilters}
|
||||
handleClearAllFilters={clearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="rich_filters"
|
||||
render={({ field: { onChange: onFiltersChange } }) => (
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
entityId={data?.id}
|
||||
entityType={EIssuesStoreType.PROJECT_VIEW}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectViewWorkItemsFilter }) =>
|
||||
projectViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="default" />
|
||||
)
|
||||
}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EIssueLayoutTypes } from "@plane/types";
|
||||
import { WorkspaceSpreadsheetRoot } from "@/components/issues/issue-layouts/spreadsheet/roots/workspace-root";
|
||||
import { WorkspaceAdditionalLayouts } from "@/plane-web/components/views/helper";
|
||||
import { WorkspaceSpreadsheetRoot } from "../issues/issue-layouts/spreadsheet/roots/workspace-root";
|
||||
|
||||
export type TWorkspaceLayoutProps = {
|
||||
activeLayout: EIssueLayoutTypes | undefined;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { PROJECT_VIEW_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { IProjectView } from "@plane/types";
|
||||
import { EIssuesStoreType, IProjectView } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
// local imports
|
||||
|
|
@ -30,6 +32,10 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { createView, updateView } = useProjectView();
|
||||
const {
|
||||
issuesFilter: { mutateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { resetExpression } = useWorkItemFilters();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
|
@ -66,7 +72,9 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
|
||||
const handleUpdateView = async (payload: IProjectView) => {
|
||||
await updateView(workspaceSlug, projectId, data?.id as string, payload)
|
||||
.then(() => {
|
||||
.then((viewDetails) => {
|
||||
mutateFilters(workspaceSlug, viewDetails.id, viewDetails);
|
||||
resetExpression(EIssuesStoreType.PROJECT_VIEW, viewDetails.id, viewDetails.rich_filters);
|
||||
handleClose();
|
||||
captureSuccess({
|
||||
eventName: PROJECT_VIEW_TRACKER_EVENTS.update,
|
||||
|
|
@ -106,6 +114,8 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
handleClose={handleClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
preLoadedData={preLoadedData}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { SetStateAction, useEffect, useState } from "react";
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isLocked: boolean;
|
||||
areFiltersEqual: boolean;
|
||||
isOwner: boolean;
|
||||
isAuthorizedUser: boolean;
|
||||
setIsModalOpen: (value: SetStateAction<boolean>) => void;
|
||||
handleUpdateView: () => void;
|
||||
lockedTooltipContent?: string;
|
||||
trackerElement: string;
|
||||
};
|
||||
|
||||
export const UpdateViewComponent = (props: Props) => {
|
||||
const { isLocked, areFiltersEqual, isOwner, isAuthorizedUser, setIsModalOpen, handleUpdateView, trackerElement } =
|
||||
props;
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (areFiltersEqual) {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [areFiltersEqual]);
|
||||
|
||||
// Change state while updating view to have a feedback
|
||||
const updateButton = isUpdating ? (
|
||||
<Button variant="primary" size="sm" className="flex-shrink-0">
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsUpdating(true);
|
||||
handleUpdateView();
|
||||
}}
|
||||
>
|
||||
Update view
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 h-fit">
|
||||
{!isLocked && !areFiltersEqual && isAuthorizedUser && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="md"
|
||||
className="flex-shrink-0"
|
||||
data-ph-element={trackerElement}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
Save as
|
||||
</Button>
|
||||
{isOwner && <>{updateButton}</>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import { useLocalStorage } from "@plane/hooks";
|
|||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { EViewAccess, IProjectView } from "@plane/types";
|
||||
import { FavoriteStar } from "@plane/ui";
|
||||
import { calculateTotalFilters, getPublishViewLink } from "@plane/utils";
|
||||
import { getPublishViewLink } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
|
|
@ -52,8 +52,6 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||
|
||||
const access = view.access;
|
||||
|
||||
const publishLink = getPublishViewLink(view?.anchor);
|
||||
|
|
@ -87,10 +85,6 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
/>
|
||||
)}
|
||||
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
<p className="hidden rounded bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200 group-hover:block">
|
||||
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
||||
</p>
|
||||
|
||||
<div className="cursor-default text-custom-text-300">
|
||||
<Tooltip tooltipContent={access === EViewAccess.PUBLIC ? "Public" : "Private"}>
|
||||
{access === EViewAccess.PUBLIC ? <Earth className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue