[WEB-1255] feat: filters for project views and changes required for advanced views (#4949)
* View with filters and changes required for advanced views * minor refactoring of views * minor name change
This commit is contained in:
parent
adaf3b15de
commit
82661589fb
52 changed files with 922 additions and 146 deletions
18
packages/types/src/views.d.ts
vendored
18
packages/types/src/views.d.ts
vendored
|
|
@ -27,3 +27,21 @@ export interface IProjectView {
|
|||
is_locked: boolean;
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export type TViewFiltersSortKey = "name" | "created_at" | "updated_at";
|
||||
|
||||
export type TViewFiltersSortBy = "asc" | "desc";
|
||||
|
||||
export type TViewFilterProps = {
|
||||
created_at?: string[] | null;
|
||||
owned_by?: string[] | null;
|
||||
favorites?: boolean;
|
||||
accessTypes?: EViewAccess[];
|
||||
};
|
||||
|
||||
export type TViewFilters = {
|
||||
searchQuery: string;
|
||||
sortKey: TViewFiltersSortKey;
|
||||
sortBy: TViewFiltersSortBy;
|
||||
filters?: TViewFilterProps;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { TViewFilterProps } from "@plane/types";
|
||||
import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||
import { ViewListHeader } from "@/components/views";
|
||||
import { ViewAppliedFiltersList } from "@/components/views/filters/applied-filters";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useProject, useUser } from "@/hooks/store";
|
||||
import { useCommandPalette, useProject, useProjectView, useUser } from "@/hooks/store";
|
||||
|
||||
export const ProjectViewsHeader = observer(() => {
|
||||
// router
|
||||
|
|
@ -21,8 +26,28 @@ export const ProjectViewsHeader = observer(() => {
|
|||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { filters, updateFilters, clearAllFilters } = useProjectView();
|
||||
|
||||
const canUserCreateIssue =
|
||||
const handleRemoveFilter = useCallback(
|
||||
(key: keyof TViewFilterProps, value: string | null) => {
|
||||
let newValues = filters.filters?.[key];
|
||||
|
||||
if (key === "favorites") {
|
||||
newValues = !!value;
|
||||
}
|
||||
if (Array.isArray(newValues)) {
|
||||
if (!value) newValues = [];
|
||||
else newValues = newValues.filter((val) => val !== value) as string[];
|
||||
}
|
||||
|
||||
updateFilters("filters", { [key]: newValues });
|
||||
},
|
||||
[filters.filters, updateFilters]
|
||||
);
|
||||
|
||||
const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0;
|
||||
|
||||
const canUserCreateView =
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
return (
|
||||
|
|
@ -56,17 +81,27 @@ export const ProjectViewsHeader = observer(() => {
|
|||
</Breadcrumbs>
|
||||
</div>
|
||||
</div>
|
||||
{canUserCreateIssue && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<ViewListHeader />
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<ViewListHeader />
|
||||
{canUserCreateView && (
|
||||
<div>
|
||||
<Button variant="primary" size="sm" onClick={() => toggleCreateViewModal(true)}>
|
||||
Add View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isFiltersApplied && (
|
||||
<div className="border-t border-custom-border-200 px-5 py-3">
|
||||
<ViewAppliedFiltersList
|
||||
appliedFilters={filters.filters ?? {}}
|
||||
handleClearAllFilters={clearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import { useEventTracker, useUser, useUserProfile, useWorkspace } from "@/hooks/
|
|||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// images
|
||||
import emptyInvitation from "@/public/empty-state/invitation.svg";
|
||||
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
|
||||
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import { useUser, useWorkspace, useUserProfile, useEventTracker, useUserSettings
|
|||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
enum EOnboardingSteps {
|
||||
PROFILE_SETUP = "PROFILE_SETUP",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import { useUser } from "@/hooks/store";
|
|||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
// service initialization
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from "./project";
|
||||
export * from "./workspace.service";
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from "./estimate.service";
|
||||
export * from "./view.service";
|
||||
24
web/ce/services/project/view.service.ts
Normal file
24
web/ce/services/project/view.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { EViewAccess } from "@/constants/views";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { ViewService as CoreViewService } from "@/services/view.service";
|
||||
|
||||
export class ViewService extends CoreViewService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async updateViewAccess(workspaceSlug: string, projectId: string, viewId: string, access: EViewAccess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async lockView(workspaceSlug: string, projectId: string, viewId: string) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async unLockView(workspaceSlug: string, projectId: string, viewId: string) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
24
web/ce/services/workspace.service.ts
Normal file
24
web/ce/services/workspace.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { EViewAccess } from "@/constants/views";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { WorkspaceService as CoreWorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
export class WorkspaceService extends CoreWorkspaceService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async updateViewAccess(workspaceSlug: string, viewId: string, access: EViewAccess) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async lockView(workspaceSlug: string, viewId: string) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async unLockView(workspaceSlug: string, viewId: string) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import { LogoSpinner } from "@/components/common";
|
|||
import { WorkspaceLogo } from "@/components/workspace/logo";
|
||||
// helpers
|
||||
import { EAuthModes, EAuthSteps } from "@/helpers/authentication.helper";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
type TAuthHeader = {
|
||||
workspaceSlug: string | undefined;
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ import { useAppRouter } from "@/hooks/use-app-router";
|
|||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { IssueService } from "@/services/issue";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
// ui
|
||||
// components
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// helpers
|
||||
// constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "@/helpers/string.helper";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
editable: boolean | undefined;
|
||||
2
web/core/components/common/applied-filters/index.ts
Normal file
2
web/core/components/common/applied-filters/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./date";
|
||||
export * from "./members";
|
||||
2
web/core/components/common/filters/index.ts
Normal file
2
web/core/components/common/filters/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./created-at";
|
||||
export * from "./created-by";
|
||||
|
|
@ -11,7 +11,7 @@ import { MarkdownRenderer } from "@/components/ui";
|
|||
// icons
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { IGithubRepoCollaborator } from "@plane/types";
|
|||
// services
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input } from "@plane/ui";
|
||||
import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// ui
|
||||
// types
|
||||
import { IUserDetails } from "./root";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { IJiraImporterForm } from "@plane/types";
|
|||
// services
|
||||
import { Avatar, CustomSelect, CustomSearchSelect, Input, ToggleSwitch } from "@plane/ui";
|
||||
import { WORKSPACE_MEMBERS } from "@/constants/fetch-keys";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// ui
|
||||
// types
|
||||
// fetch keys
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { UpdateViewComponent } from "@/components/views/update-view-component";
|
|||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useProjectView, useUser } from "@/hooks/store";
|
||||
import { useIssues, useLabel, useProjectState, useProjectView, useUser } from "@/hooks/store";
|
||||
import { getAreFiltersEqual } from "../../../utils";
|
||||
|
||||
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
|
|
@ -26,6 +26,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
const { viewMap, updateView } = useProjectView();
|
||||
const {
|
||||
data,
|
||||
|
|
@ -129,6 +130,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
disableEditing={isLocked}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
|
|||
// hooks
|
||||
import { useEventTracker, useUserProfile, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
stepChange: (steps: Partial<TOnboardingSteps>) => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { getUserRole } from "@/helpers/user.helper";
|
|||
// hooks
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
handleNextStep: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ import { getUserRole } from "@/helpers/user.helper";
|
|||
import { useEventTracker } from "@/hooks/store";
|
||||
import useDynamicDropdownPosition from "@/hooks/use-dynamic-dropdown";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// assets
|
||||
import InviteMembersDark from "@/public/onboarding/invite-members-dark.svg";
|
||||
import InviteMembersLight from "@/public/onboarding/invite-members-light.svg";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// components
|
||||
import { OnboardingHeader } from "./header";
|
||||
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from "./date";
|
||||
export * from "./members";
|
||||
export * from "./root";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { X } from "lucide-react";
|
||||
import { TPageFilterProps } from "@plane/types";
|
||||
// components
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/pages";
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from "./created-at";
|
||||
export * from "./created-by";
|
||||
export * from "./root";
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { observer } from "mobx-react";
|
|||
import { Search, X } from "lucide-react";
|
||||
import { TPageFilterProps, TPageFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/common/filters";
|
||||
import { FilterOption } from "@/components/issues";
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/pages";
|
||||
|
||||
type Props = {
|
||||
filters: TPageFilters;
|
||||
|
|
|
|||
81
web/core/components/views/filters/applied-filters.tsx
Normal file
81
web/core/components/views/filters/applied-filters.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { X } from "lucide-react";
|
||||
import { TViewFilterProps } from "@plane/types";
|
||||
// components
|
||||
import { AppliedDateFilters, AppliedMembersFilters } from "@/components/common/applied-filters";
|
||||
// helpers
|
||||
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
appliedFilters: TViewFilterProps;
|
||||
handleClearAllFilters: () => void;
|
||||
handleRemoveFilter: (key: keyof TViewFilterProps, value: string | null) => void;
|
||||
alwaysAllowEditing?: boolean;
|
||||
};
|
||||
|
||||
const MEMBERS_FILTERS = ["owned_by"];
|
||||
const DATE_FILTERS = ["created_at"];
|
||||
|
||||
export const ViewAppliedFiltersList: React.FC<Props> = (props) => {
|
||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const isEditingAllowed = alwaysAllowEditing;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof TViewFilterProps;
|
||||
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={filterKey}
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||
{DATE_FILTERS.includes(filterKey) && (
|
||||
<AppliedDateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={Array.isArray(value) ? (value as string[]) : []}
|
||||
/>
|
||||
)}
|
||||
{MEMBERS_FILTERS.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={Array.isArray(value) ? (value as string[]) : []}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAllFilters}
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 text-xs text-custom-text-300 hover:text-custom-text-200"
|
||||
>
|
||||
Clear all
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
101
web/core/components/views/filters/filter-selection.tsx
Normal file
101
web/core/components/views/filters/filter-selection.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { TViewFilterProps, TViewFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterCreatedBy, FilterCreatedDate } from "@/components/common/filters";
|
||||
import { FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
|
||||
type Props = {
|
||||
filters: TViewFilters;
|
||||
handleFiltersUpdate: <T extends keyof TViewFilters>(filterKey: T, filterValue: TViewFilters[T]) => void;
|
||||
memberIds?: string[] | undefined;
|
||||
};
|
||||
|
||||
export const ViewFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFiltersUpdate, memberIds } = props;
|
||||
// states
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
// handles filter update
|
||||
const handleFilters = (key: keyof TViewFilterProps, value: boolean | string | EViewAccess | string[]) => {
|
||||
const currValues = (filters.filters?.[key] ?? []) as (string | EViewAccess)[];
|
||||
|
||||
if (typeof currValues === "boolean" && typeof value === "boolean") return;
|
||||
|
||||
if (Array.isArray(currValues)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!currValues.includes(val)) currValues.push(val);
|
||||
else currValues.splice(currValues.indexOf(val), 1);
|
||||
});
|
||||
} else if (typeof value !== "boolean") {
|
||||
if (currValues?.includes(value)) currValues.splice(currValues.indexOf(value), 1);
|
||||
else currValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
handleFiltersUpdate("filters", {
|
||||
...filters.filters,
|
||||
[key]: currValues,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="bg-custom-background-100 p-2.5 pb-0">
|
||||
<div className="flex items-center gap-1.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 py-1 text-xs">
|
||||
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
|
||||
placeholder="Search"
|
||||
value={filtersSearchQuery}
|
||||
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
{filtersSearchQuery !== "" && (
|
||||
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5 vertical-scrollbar scrollbar-sm">
|
||||
<div className="py-2">
|
||||
<FilterOption
|
||||
isChecked={!!filters.filters?.favorites}
|
||||
onClick={() =>
|
||||
handleFiltersUpdate("filters", {
|
||||
...filters.filters,
|
||||
favorites: !filters.filters?.favorites,
|
||||
})
|
||||
}
|
||||
title="Favorites"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created date */}
|
||||
<div className="py-2">
|
||||
<FilterCreatedDate
|
||||
appliedFilters={filters.filters?.created_at ?? null}
|
||||
handleUpdate={(val: string | string[]) => handleFilters("created_at", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created by */}
|
||||
<div className="py-2">
|
||||
<FilterCreatedBy
|
||||
appliedFilters={filters.filters?.owned_by ?? null}
|
||||
handleUpdate={(val) => handleFilters("owned_by", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
memberIds={memberIds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
75
web/core/components/views/filters/order-by.tsx
Normal file
75
web/core/components/views/filters/order-by.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"use client";
|
||||
|
||||
import { ArrowDownWideNarrow, Check, ChevronDown } from "lucide-react";
|
||||
// types
|
||||
import { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu, getButtonStyling } from "@plane/ui";
|
||||
// constants
|
||||
import { VIEW_SORTING_KEY_OPTIONS } from "@/constants/views";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type Props = {
|
||||
onChange: (value: { key?: TViewFiltersSortKey; order?: TViewFiltersSortBy }) => void;
|
||||
sortBy: TViewFiltersSortBy;
|
||||
sortKey: TViewFiltersSortKey;
|
||||
};
|
||||
|
||||
export const ViewOrderByDropdown: React.FC<Props> = (props) => {
|
||||
const { onChange, sortBy, sortKey } = props;
|
||||
|
||||
const orderByDetails = VIEW_SORTING_KEY_OPTIONS.find((option) => sortKey === option.key);
|
||||
const isDescending = sortBy === "desc";
|
||||
|
||||
const sortByOptions = [
|
||||
{ key: "asc", label: "Ascending", isSelected: !isDescending },
|
||||
{ key: "desc", label: "Descending", isSelected: isDescending },
|
||||
];
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<div className={cn(getButtonStyling("neutral-primary", "sm"), "px-2 text-custom-text-300")}>
|
||||
<ArrowDownWideNarrow className="h-3 w-3" />
|
||||
{orderByDetails?.label}
|
||||
<ChevronDown className="h-3 w-3" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
placement="bottom-end"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{VIEW_SORTING_KEY_OPTIONS.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
key: option.key,
|
||||
})
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
{sortKey === option.key && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
<hr className="my-2 border-custom-border-200" />
|
||||
{sortByOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
className="flex items-center justify-between gap-2"
|
||||
onClick={() => {
|
||||
if (!option.isSelected)
|
||||
onChange({
|
||||
order: option.key as TViewFiltersSortBy,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{option.isSelected && <Check className="h-3 w-3" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
};
|
||||
|
|
@ -14,6 +14,7 @@ import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/componen
|
|||
import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { LayoutDropDown } from "../dropdowns/layout";
|
||||
|
|
@ -28,6 +29,8 @@ type Props = {
|
|||
const defaultValues: Partial<IProjectView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
display_properties: getComputedDisplayProperties(),
|
||||
display_filters: getComputedDisplayFilters(),
|
||||
};
|
||||
|
||||
export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -51,14 +51,14 @@ export const UpdateViewComponent = (props: Props) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 h-fit">
|
||||
{isLocked ? (
|
||||
<LockedComponent toolTipContent={lockedTooltipContent} />
|
||||
) : (
|
||||
!areFiltersEqual &&
|
||||
isAuthorizedUser && (
|
||||
<>
|
||||
<Button variant="outline-primary" size="sm" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
|
||||
<Button variant="outline-primary" size="md" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
|
||||
Save as
|
||||
</Button>
|
||||
{isOwner && <>{updateButton}</>}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { Search, X } from "lucide-react";
|
||||
import { ListFilter, Search, X } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProjectView } from "@/hooks/store";
|
||||
import { useMember, useProjectView } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { FiltersDropdown } from "../issues";
|
||||
import { ViewFiltersSelection } from "./filters/filter-selection";
|
||||
import { ViewOrderByDropdown } from "./filters/order-by";
|
||||
|
||||
export const ViewListHeader = observer(() => {
|
||||
// states
|
||||
|
|
@ -14,13 +17,17 @@ export const ViewListHeader = observer(() => {
|
|||
// refs
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// store hooks
|
||||
const { searchQuery, updateSearchQuery } = useProjectView();
|
||||
const { filters, updateFilters } = useProjectView();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
// handlers
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
|
||||
else {
|
||||
if (filters?.searchQuery && filters?.searchQuery.trim() !== "") {
|
||||
updateFilters("searchQuery", "");
|
||||
} else {
|
||||
setIsSearchOpen(false);
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
|
|
@ -29,12 +36,12 @@ export const ViewListHeader = observer(() => {
|
|||
|
||||
// outside click detector hook
|
||||
useOutsideClickDetector(inputRef, () => {
|
||||
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
if (isSearchOpen && filters?.searchQuery.trim() === "") setIsSearchOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||
}, [searchQuery]);
|
||||
if (filters?.searchQuery.trim() !== "") setIsSearchOpen(true);
|
||||
}, [filters?.searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex items-center gap-2">
|
||||
|
|
@ -64,8 +71,8 @@ export const ViewListHeader = observer(() => {
|
|||
ref={inputRef}
|
||||
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
|
||||
placeholder="Search"
|
||||
value={searchQuery}
|
||||
onChange={(e) => updateSearchQuery(e.target.value)}
|
||||
value={filters?.searchQuery}
|
||||
onChange={(e) => updateFilters("searchQuery", e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
{isSearchOpen && (
|
||||
|
|
@ -73,7 +80,7 @@ export const ViewListHeader = observer(() => {
|
|||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
updateSearchQuery("");
|
||||
updateFilters("searchQuery", "");
|
||||
setIsSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
|
|
@ -82,6 +89,26 @@ export const ViewListHeader = observer(() => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ViewOrderByDropdown
|
||||
sortBy={filters.sortBy}
|
||||
sortKey={filters.sortKey}
|
||||
onChange={(val) => {
|
||||
if (val.key) updateFilters("sortKey", val.key);
|
||||
if (val.order) updateFilters("sortBy", val.order);
|
||||
}}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
icon={<ListFilter className="h-3 w-3" />}
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={false}
|
||||
>
|
||||
<ViewFiltersSelection
|
||||
filters={filters}
|
||||
handleFiltersUpdate={updateFilters}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
|
|
@ -8,26 +10,48 @@ import { ProjectViewListItem } from "@/components/views";
|
|||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// hooks
|
||||
import { useCommandPalette, useProjectView } from "@/hooks/store";
|
||||
// assets
|
||||
import AllFiltersImage from "@/public/empty-state/pages/all-filters.svg";
|
||||
import NameFilterImage from "@/public/empty-state/pages/name-filter.svg";
|
||||
|
||||
export const ProjectViewsList = observer(() => {
|
||||
const { projectId } = useParams();
|
||||
// store hooks
|
||||
const { toggleCreateViewModal } = useCommandPalette();
|
||||
const { projectViewIds, getViewById, loader, searchQuery } = useProjectView();
|
||||
const { getProjectViews, getFilteredProjectViews, filters, loader } = useProjectView();
|
||||
|
||||
if (loader || !projectViewIds) return <ViewListLoader />;
|
||||
const projectViews = getProjectViews(projectId?.toString());
|
||||
const filteredProjectViews = getFilteredProjectViews(projectId?.toString());
|
||||
|
||||
// derived values
|
||||
const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
|
||||
if (loader || !projectViews || !filteredProjectViews) return <ViewListLoader />;
|
||||
|
||||
const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
if (filteredProjectViews.length === 0 && projectViews) {
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={filters.searchQuery.length > 0 ? NameFilterImage : AllFiltersImage}
|
||||
className="h-36 sm:h-48 w-36 sm:w-48 mx-auto"
|
||||
alt="No matching modules"
|
||||
/>
|
||||
<h5 className="text-xl font-medium mt-7 mb-1">No matching views</h5>
|
||||
<p className="text-custom-text-400 text-base">
|
||||
{filters.searchQuery.length > 0
|
||||
? "Remove the search criteria to see all views"
|
||||
: "Remove the filters to see all views"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewsList.length > 0 ? (
|
||||
{filteredProjectViews.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<ListLayout>
|
||||
{filteredViewsList.length > 0 ? (
|
||||
filteredViewsList.map((view) => <ProjectViewListItem key={view.id} view={view} />)
|
||||
{filteredProjectViews.length > 0 ? (
|
||||
filteredProjectViews.map((view) => <ProjectViewListItem key={view.id} view={view} />)
|
||||
) : (
|
||||
<p className="mt-10 text-center text-sm text-custom-text-300">No results found</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@/constants/workspace";
|
|||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
type Props = {
|
||||
onSubmit?: (res: IWorkspace) => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { Button, Input, TextArea } from "@plane/ui";
|
|||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember } from "@/hooks/store";
|
||||
|
||||
|
|
@ -24,6 +26,8 @@ type Props = {
|
|||
const defaultValues: Partial<IWorkspaceView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
display_properties: getComputedDisplayProperties(),
|
||||
display_filters: getComputedDisplayFilters(),
|
||||
};
|
||||
|
||||
export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,33 @@
|
|||
import { Globe2, Lock, LucideIcon } from "lucide-react";
|
||||
import { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
|
||||
export enum EViewAccess {
|
||||
PRIVATE,
|
||||
PUBLIC,
|
||||
}
|
||||
|
||||
export const VIEW_ACCESS_SPECIFIERS: {
|
||||
key: EViewAccess;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}[] = [
|
||||
{ key: EViewAccess.PUBLIC, label: "Public", icon: Globe2 },
|
||||
{ key: EViewAccess.PRIVATE, label: "Private", icon: Lock },
|
||||
];
|
||||
|
||||
export const VIEW_SORTING_KEY_OPTIONS: {
|
||||
key: TViewFiltersSortKey;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "created_at", label: "Date created" },
|
||||
{ key: "updated_at", label: "Date modified" },
|
||||
];
|
||||
|
||||
export const VIEW_SORT_BY_OPTIONS: {
|
||||
key: TViewFiltersSortBy;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ key: "asc", label: "Ascending" },
|
||||
{ key: "desc", label: "Descending" },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { IProjectView } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// types
|
||||
// helpers
|
||||
|
||||
export class ViewService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
async createView(workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,13 @@ import {
|
|||
IWorkspaceView,
|
||||
TIssuesResponse,
|
||||
} from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
// types
|
||||
|
||||
export class WorkspaceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
constructor(baseUrl: string) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
async userWorkspaces(): Promise<IWorkspace[]> {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import { computedFn } from "mobx-utils";
|
|||
import { IIssueFilterOptions, IWorkspaceView } from "@plane/types";
|
||||
// constants
|
||||
import { EIssueFilterType } from "@/constants/issue";
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// store
|
||||
import { CoreRootStore } from "./root.store";
|
||||
|
||||
|
|
@ -130,14 +131,19 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
* @param workspaceSlug
|
||||
* @param data
|
||||
*/
|
||||
createGlobalView = async (workspaceSlug: string, data: Partial<IWorkspaceView>): Promise<IWorkspaceView> =>
|
||||
await this.workspaceService.createView(workspaceSlug, data).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, response.id, response);
|
||||
});
|
||||
return response;
|
||||
createGlobalView = async (workspaceSlug: string, data: Partial<IWorkspaceView>): Promise<IWorkspaceView> => {
|
||||
const response = await this.workspaceService.createView(workspaceSlug, data);
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, response.id, response);
|
||||
});
|
||||
|
||||
if (data.access === EViewAccess.PRIVATE) {
|
||||
await this.updateViewAccess(workspaceSlug, response.id, EViewAccess.PRIVATE);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update global view
|
||||
* @param workspaceSlug
|
||||
|
|
@ -155,7 +161,16 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
const currentKey = key as keyof IWorkspaceView;
|
||||
set(this.globalViewMap, [viewId, currentKey], data[currentKey]);
|
||||
});
|
||||
const currentView = await this.workspaceService.updateView(workspaceSlug, viewId, data);
|
||||
|
||||
const promiseRequests = [];
|
||||
promiseRequests.push(this.workspaceService.updateView(workspaceSlug, viewId, data));
|
||||
|
||||
if (data.access !== undefined && data.access !== currentViewData?.access) {
|
||||
promiseRequests.push(this.updateViewAccess(workspaceSlug, viewId, data.access));
|
||||
}
|
||||
|
||||
const [currentView] = await Promise.all(promiseRequests);
|
||||
|
||||
// applying the filters in the global view
|
||||
if (!isEqual(currentViewData?.filters || {}, currentView?.filters || {})) {
|
||||
if (isEmpty(currentView?.filters)) {
|
||||
|
|
@ -178,13 +193,13 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
workspaceSlug,
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
currentView.filters,
|
||||
currentView?.filters,
|
||||
viewId
|
||||
);
|
||||
}
|
||||
this.rootStore.issue.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
|
||||
}
|
||||
return currentView;
|
||||
return currentView as IWorkspaceView;
|
||||
} catch {
|
||||
Object.keys(data).forEach((key) => {
|
||||
const currentKey = key as keyof IWorkspaceView;
|
||||
|
|
@ -204,4 +219,74 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
delete this.globalViewMap[viewId];
|
||||
});
|
||||
});
|
||||
|
||||
/** Locks view
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @returns
|
||||
*/
|
||||
lockView = async (workspaceSlug: string, viewId: string) => {
|
||||
try {
|
||||
const currentView = this.getViewDetailsById(viewId);
|
||||
if (currentView?.is_locked) return;
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "is_locked"], true);
|
||||
});
|
||||
await this.workspaceService.lockView(workspaceSlug, viewId);
|
||||
} catch (error) {
|
||||
console.error("Failed to lock the view in view store", error);
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "is_locked"], false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* unlocks View
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @returns
|
||||
*/
|
||||
unLockView = async (workspaceSlug: string, viewId: string) => {
|
||||
try {
|
||||
const currentView = this.getViewDetailsById(viewId);
|
||||
if (!currentView?.is_locked) return;
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "is_locked"], false);
|
||||
});
|
||||
await this.workspaceService.unLockView(workspaceSlug, viewId);
|
||||
} catch (error) {
|
||||
console.error("Failed to unlock view in view store", error);
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "is_locked"], true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates View access
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @param access
|
||||
* @returns
|
||||
*/
|
||||
updateViewAccess = async (workspaceSlug: string, viewId: string, access: EViewAccess) => {
|
||||
const currentView = this.getViewDetailsById(viewId);
|
||||
const currentAccess = currentView?.access;
|
||||
try {
|
||||
if (currentAccess === access) return;
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "access"], access);
|
||||
});
|
||||
await this.workspaceService.updateViewAccess(workspaceSlug, viewId, access);
|
||||
} catch (error) {
|
||||
console.error("Failed to update Access for view", error);
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, [viewId, "access"], currentAccess);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ import {
|
|||
EIssuesStoreType,
|
||||
EIssueGroupByToServerOptions,
|
||||
EServerGroupByToFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@/constants/issue";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
|
||||
// lib
|
||||
import { storage } from "@/lib/local-storage";
|
||||
|
||||
|
|
@ -181,46 +182,15 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
computedDisplayFilters = (
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
): IIssueDisplayFilterOptions => {
|
||||
const filters = displayFilters || defaultValues;
|
||||
|
||||
return {
|
||||
calendar: {
|
||||
show_weekends: filters?.calendar?.show_weekends || false,
|
||||
layout: filters?.calendar?.layout || "month",
|
||||
},
|
||||
layout: filters?.layout || EIssueLayoutTypes.LIST,
|
||||
order_by: filters?.order_by || "sort_order",
|
||||
group_by: filters?.group_by || null,
|
||||
sub_group_by: filters?.sub_group_by || null,
|
||||
type: filters?.type || null,
|
||||
sub_issue: filters?.sub_issue || false,
|
||||
show_empty_groups: filters?.show_empty_groups || false,
|
||||
};
|
||||
};
|
||||
): IIssueDisplayFilterOptions => getComputedDisplayFilters(displayFilters, defaultValues);
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display properties on the issues
|
||||
* @param {IIssueDisplayProperties} displayProperties
|
||||
* @returns {IIssueDisplayProperties}
|
||||
*/
|
||||
computedDisplayProperties = (displayProperties: IIssueDisplayProperties): IIssueDisplayProperties => ({
|
||||
assignee: displayProperties?.assignee ?? true,
|
||||
start_date: displayProperties?.start_date ?? true,
|
||||
due_date: displayProperties?.due_date ?? true,
|
||||
labels: displayProperties?.labels ?? true,
|
||||
priority: displayProperties?.priority ?? true,
|
||||
state: displayProperties?.state ?? true,
|
||||
sub_issue_count: displayProperties?.sub_issue_count ?? true,
|
||||
attachment_count: displayProperties?.attachment_count ?? true,
|
||||
link: displayProperties?.link ?? true,
|
||||
estimate: displayProperties?.estimate ?? true,
|
||||
key: displayProperties?.key ?? true,
|
||||
created_on: displayProperties?.created_on ?? true,
|
||||
updated_on: displayProperties?.updated_on ?? true,
|
||||
modules: displayProperties?.modules ?? true,
|
||||
cycle: displayProperties?.cycle ?? true,
|
||||
});
|
||||
computedDisplayProperties = (displayProperties: IIssueDisplayProperties): IIssueDisplayProperties =>
|
||||
getComputedDisplayProperties(displayProperties);
|
||||
|
||||
handleIssuesLocalFilters = {
|
||||
fetchFiltersFromStorage: () => {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ import {
|
|||
} from "@plane/types";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper";
|
||||
import { ViewService } from "@/services/view.service";
|
||||
// services
|
||||
import { ViewService } from "@/plane-web/services";
|
||||
import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IProjectViewIssuesFilter extends IBaseIssueFilterStore {
|
||||
//helper actions
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import {
|
|||
IssuePaginationOptions,
|
||||
} from "@plane/types";
|
||||
import { EIssueFilterType, EIssueLayoutTypes, EIssuesStoreType } from "@/constants/issue";
|
||||
// services
|
||||
import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { action, makeObservable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { IssuePaginationOptions, TBulkOperationsPayload, TIssue, TIssuesResponse, TLoader, ViewFlags } from "@plane/types";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import {
|
||||
IssuePaginationOptions,
|
||||
TBulkOperationsPayload,
|
||||
TIssue,
|
||||
TIssuesResponse,
|
||||
TLoader,
|
||||
ViewFlags,
|
||||
} from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// types
|
||||
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitat
|
|||
// constants
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// types
|
||||
import { IRouterStore } from "@/store/router.store";
|
||||
import { IUserStore } from "@/store/user";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ import { set } from "lodash";
|
|||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import { IProjectView } from "@plane/types";
|
||||
import { IProjectView, TViewFilters } from "@plane/types";
|
||||
// constants
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { getViewName, orderViews, shouldFilterView } from "@/helpers/project-views.helpers";
|
||||
// services
|
||||
import { ViewService } from "@/services/view.service";
|
||||
import { ViewService } from "@/plane-web/services";
|
||||
// store
|
||||
import { CoreRootStore } from "./root.store";
|
||||
|
||||
|
|
@ -13,13 +17,14 @@ export interface IProjectViewStore {
|
|||
loader: boolean;
|
||||
fetchedMap: Record<string, boolean>;
|
||||
// observables
|
||||
searchQuery: string;
|
||||
viewMap: Record<string, IProjectView>;
|
||||
filters: TViewFilters;
|
||||
// computed
|
||||
projectViewIds: string[] | null;
|
||||
// computed actions
|
||||
getProjectViews: (projectId: string) => IProjectView[] | undefined;
|
||||
getFilteredProjectViews: (projectId: string) => IProjectView[] | undefined;
|
||||
getViewById: (viewId: string) => IProjectView;
|
||||
updateSearchQuery: (query: string) => void;
|
||||
// fetch actions
|
||||
fetchViews: (workspaceSlug: string, projectId: string) => Promise<undefined | IProjectView[]>;
|
||||
fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise<IProjectView>;
|
||||
|
|
@ -32,6 +37,8 @@ export interface IProjectViewStore {
|
|||
data: Partial<IProjectView>
|
||||
) => Promise<IProjectView>;
|
||||
deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||
updateFilters: <T extends keyof TViewFilters>(filterKey: T, filterValue: TViewFilters[T]) => void;
|
||||
clearAllFilters: () => void;
|
||||
// favorites actions
|
||||
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||
|
|
@ -41,9 +48,9 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
// observables
|
||||
loader: boolean = false;
|
||||
viewMap: Record<string, IProjectView> = {};
|
||||
searchQuery: string = "";
|
||||
//loaders
|
||||
fetchedMap: Record<string, boolean> = {};
|
||||
filters: TViewFilters = { searchQuery: "", sortBy: "asc", sortKey: "created_at" };
|
||||
// root store
|
||||
rootStore;
|
||||
// services
|
||||
|
|
@ -55,7 +62,7 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
loader: observable.ref,
|
||||
viewMap: observable,
|
||||
fetchedMap: observable,
|
||||
searchQuery: observable.ref,
|
||||
filters: observable,
|
||||
// computed
|
||||
projectViewIds: computed,
|
||||
// fetch actions
|
||||
|
|
@ -66,7 +73,8 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
updateView: action,
|
||||
deleteView: action,
|
||||
// actions
|
||||
updateSearchQuery: action,
|
||||
updateFilters: action,
|
||||
clearAllFilters: action,
|
||||
// favorites actions
|
||||
addViewToFavorites: action,
|
||||
removeViewFromFavorites: action,
|
||||
|
|
@ -87,16 +95,58 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
return viewIds;
|
||||
}
|
||||
|
||||
getProjectViews = computedFn((projectId: string) => {
|
||||
if (!this.fetchedMap[projectId]) return undefined;
|
||||
|
||||
const ViewsList = Object.values(this.viewMap ?? {});
|
||||
// helps to filter views based on the projectId
|
||||
let filteredViews = ViewsList.filter((view) => view?.project === projectId);
|
||||
filteredViews = orderViews(filteredViews, this.filters.sortKey, this.filters.sortBy);
|
||||
|
||||
return filteredViews ?? undefined;
|
||||
});
|
||||
/**
|
||||
* returns viewsIds of issues
|
||||
*/
|
||||
getFilteredProjectViews = computedFn((projectId: string) => {
|
||||
if (!this.fetchedMap[projectId]) return undefined;
|
||||
|
||||
const ViewsList = Object.values(this.viewMap ?? {});
|
||||
// helps to filter views based on the projectId, searchQuery and filters
|
||||
let filteredViews = ViewsList.filter(
|
||||
(view) =>
|
||||
view?.project === projectId &&
|
||||
getViewName(view.name).toLowerCase().includes(this.filters.searchQuery.toLowerCase()) &&
|
||||
shouldFilterView(view, this.filters.filters)
|
||||
);
|
||||
filteredViews = orderViews(filteredViews, this.filters.sortKey, this.filters.sortBy);
|
||||
|
||||
return filteredViews ?? undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns view details by id
|
||||
*/
|
||||
getViewById = computedFn((viewId: string) => this.viewMap?.[viewId] ?? null);
|
||||
|
||||
/**
|
||||
* @description update search query
|
||||
* @param {string} query
|
||||
* Updates the filter
|
||||
* @param filterKey
|
||||
* @param filterValue
|
||||
*/
|
||||
updateSearchQuery = (query: string) => (this.searchQuery = query);
|
||||
updateFilters = <T extends keyof TViewFilters>(filterKey: T, filterValue: TViewFilters[T]) => {
|
||||
runInAction(() => {
|
||||
set(this.filters, [filterKey], filterValue);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description clears all the filters
|
||||
*/
|
||||
clearAllFilters = () =>
|
||||
runInAction(() => {
|
||||
set(this.filters, ["filters"], {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetches views for current project
|
||||
|
|
@ -145,14 +195,20 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
* @param data
|
||||
* @returns Promise<IProjectView>
|
||||
*/
|
||||
createView = async (workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<IProjectView> =>
|
||||
await this.viewService.createView(workspaceSlug, projectId, data).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [response.id], response);
|
||||
});
|
||||
return response;
|
||||
createView = async (workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<IProjectView> => {
|
||||
const response = await this.viewService.createView(workspaceSlug, projectId, data);
|
||||
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [response.id], response);
|
||||
});
|
||||
|
||||
if (data.access === EViewAccess.PRIVATE) {
|
||||
await this.updateViewAccess(workspaceSlug, projectId, response.id, EViewAccess.PRIVATE);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a view details of specific view and updates it in the store
|
||||
* @param workspaceSlug
|
||||
|
|
@ -168,12 +224,21 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
data: Partial<IProjectView>
|
||||
): Promise<IProjectView> => {
|
||||
const currentView = this.getViewById(viewId);
|
||||
return await this.viewService.patchView(workspaceSlug, projectId, viewId, data).then((response) => {
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId], { ...currentView, ...data });
|
||||
});
|
||||
return response;
|
||||
|
||||
const promiseRequests = [];
|
||||
promiseRequests.push(this.viewService.patchView(workspaceSlug, projectId, viewId, data));
|
||||
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId], { ...currentView, ...data });
|
||||
});
|
||||
|
||||
if (data.access !== undefined && data.access !== currentView.access) {
|
||||
promiseRequests.push(this.updateViewAccess(workspaceSlug, projectId, viewId, data.access));
|
||||
}
|
||||
|
||||
const [response] = await Promise.all(promiseRequests);
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -191,6 +256,76 @@ export class ProjectViewStore implements IProjectViewStore {
|
|||
});
|
||||
};
|
||||
|
||||
/** Locks view
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @returns
|
||||
*/
|
||||
lockView = async (workspaceSlug: string, projectId: string, viewId: string) => {
|
||||
try {
|
||||
const currentView = this.getViewById(viewId);
|
||||
if (currentView?.is_locked) return;
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "is_locked"], true);
|
||||
});
|
||||
await this.viewService.lockView(workspaceSlug, projectId, viewId);
|
||||
} catch (error) {
|
||||
console.error("Failed to lock the view in view store", error);
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "is_locked"], false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* unlocks View
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @returns
|
||||
*/
|
||||
unLockView = async (workspaceSlug: string, projectId: string, viewId: string) => {
|
||||
try {
|
||||
const currentView = this.getViewById(viewId);
|
||||
if (!currentView?.is_locked) return;
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "is_locked"], false);
|
||||
});
|
||||
await this.viewService.unLockView(workspaceSlug, projectId, viewId);
|
||||
} catch (error) {
|
||||
console.error("Failed to unlock view in view store", error);
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "is_locked"], true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates View access
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param viewId
|
||||
* @param access
|
||||
* @returns
|
||||
*/
|
||||
updateViewAccess = async (workspaceSlug: string, projectId: string, viewId: string, access: EViewAccess) => {
|
||||
const currentView = this.getViewById(viewId);
|
||||
const currentAccess = currentView?.access;
|
||||
try {
|
||||
if (currentAccess === access) return;
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "access"], access);
|
||||
});
|
||||
await this.viewService.updateViewAccess(workspaceSlug, projectId, viewId, access);
|
||||
} catch (error) {
|
||||
console.error("Failed to update Access for view", error);
|
||||
runInAction(() => {
|
||||
set(this.viewMap, [viewId, "access"], currentAccess);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a view to favorites
|
||||
* @param workspaceSlug
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { IWorkspaceMemberMe, IProjectMember, IUserProjectsRole } from "@plane/ty
|
|||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { ProjectMemberService } from "@/services/project";
|
||||
import { UserService } from "@/services/user.service";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// plane web store
|
||||
import { CoreRootStore } from "../root.store";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"
|
|||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
// sub-stores
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
|
|||
const [value, operator, from] = filter.split(";");
|
||||
|
||||
const dateValue = getDate(value);
|
||||
const differenceInDays = differenceInCalendarDays(date, new Date());
|
||||
|
||||
if (operator === "custom" && from === "custom") {
|
||||
if (value === "today") return differenceInCalendarDays(date, new Date()) === 0;
|
||||
if (value === "yesterday") return differenceInCalendarDays(date, new Date()) === -1;
|
||||
if (value === "last_7_days") return differenceInCalendarDays(date, new Date()) >= -7;
|
||||
if (value === "last_30_days") return differenceInCalendarDays(date, new Date()) >= -30;
|
||||
if (value === "today") return differenceInDays === 0;
|
||||
if (value === "yesterday") return differenceInDays === -1;
|
||||
if (value === "last_7_days") return differenceInDays >= -7;
|
||||
if (value === "last_30_days") return differenceInDays >= -30;
|
||||
}
|
||||
|
||||
if (!from && dateValue) {
|
||||
|
|
@ -45,16 +46,16 @@ export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
|
|||
|
||||
if (from === "fromnow") {
|
||||
if (operator === "before") {
|
||||
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) <= -7;
|
||||
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) <= -14;
|
||||
if (value === "1_months") return differenceInCalendarDays(date, new Date()) <= -30;
|
||||
if (value === "1_weeks") return differenceInDays <= -7;
|
||||
if (value === "2_weeks") return differenceInDays <= -14;
|
||||
if (value === "1_months") return differenceInDays <= -30;
|
||||
}
|
||||
|
||||
if (operator === "after") {
|
||||
if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) >= 7;
|
||||
if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) >= 14;
|
||||
if (value === "1_months") return differenceInCalendarDays(date, new Date()) >= 30;
|
||||
if (value === "2_months") return differenceInCalendarDays(date, new Date()) >= 60;
|
||||
if (value === "1_weeks") return differenceInDays >= 7;
|
||||
if (value === "2_weeks") return differenceInDays >= 14;
|
||||
if (value === "1_months") return differenceInDays >= 30;
|
||||
if (value === "2_months") return differenceInDays >= 60;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
// types
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TGroupedIssues,
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
|
|
@ -169,12 +171,12 @@ export const shouldHighlightIssueDueDate = (
|
|||
return targetDateDistance <= 0;
|
||||
};
|
||||
export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => ({
|
||||
data: block,
|
||||
id: block?.id,
|
||||
sort_order: block?.sort_order,
|
||||
start_date: getDate(block?.start_date),
|
||||
target_date: getDate(block?.target_date),
|
||||
});
|
||||
data: block,
|
||||
id: block?.id,
|
||||
sort_order: block?.sort_order,
|
||||
start_date: getDate(block?.start_date),
|
||||
target_date: getDate(block?.target_date),
|
||||
});
|
||||
|
||||
export function getChangedIssuefields(formData: Partial<TIssue>, dirtyFields: { [key: string]: boolean | undefined }) {
|
||||
const changedFields: Partial<TIssue> = {};
|
||||
|
|
@ -252,3 +254,54 @@ export const issueCountBasedOnFilters = (
|
|||
|
||||
return issuesCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display filters on the issues
|
||||
* @param {IIssueDisplayFilterOptions} displayFilters
|
||||
* @returns {IIssueDisplayFilterOptions}
|
||||
*/
|
||||
export const getComputedDisplayFilters = (
|
||||
displayFilters: IIssueDisplayFilterOptions = {},
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
): IIssueDisplayFilterOptions => {
|
||||
const filters = displayFilters || defaultValues;
|
||||
|
||||
return {
|
||||
calendar: {
|
||||
show_weekends: filters?.calendar?.show_weekends || false,
|
||||
layout: filters?.calendar?.layout || "month",
|
||||
},
|
||||
layout: filters?.layout || EIssueLayoutTypes.LIST,
|
||||
order_by: filters?.order_by || "sort_order",
|
||||
group_by: filters?.group_by || null,
|
||||
sub_group_by: filters?.sub_group_by || null,
|
||||
type: filters?.type || null,
|
||||
sub_issue: filters?.sub_issue || false,
|
||||
show_empty_groups: filters?.show_empty_groups || false,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This method is used to apply the display properties on the issues
|
||||
* @param {IIssueDisplayProperties} displayProperties
|
||||
* @returns {IIssueDisplayProperties}
|
||||
*/
|
||||
export const getComputedDisplayProperties = (
|
||||
displayProperties: IIssueDisplayProperties = {}
|
||||
): IIssueDisplayProperties => ({
|
||||
assignee: displayProperties?.assignee ?? true,
|
||||
start_date: displayProperties?.start_date ?? true,
|
||||
due_date: displayProperties?.due_date ?? true,
|
||||
labels: displayProperties?.labels ?? true,
|
||||
priority: displayProperties?.priority ?? true,
|
||||
state: displayProperties?.state ?? true,
|
||||
sub_issue_count: displayProperties?.sub_issue_count ?? true,
|
||||
attachment_count: displayProperties?.attachment_count ?? true,
|
||||
link: displayProperties?.link ?? true,
|
||||
estimate: displayProperties?.estimate ?? true,
|
||||
key: displayProperties?.key ?? true,
|
||||
created_on: displayProperties?.created_on ?? true,
|
||||
updated_on: displayProperties?.updated_on ?? true,
|
||||
modules: displayProperties?.modules ?? true,
|
||||
cycle: displayProperties?.cycle ?? true,
|
||||
});
|
||||
76
web/helpers/project-views.helpers.ts
Normal file
76
web/helpers/project-views.helpers.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import orderBy from "lodash/orderBy";
|
||||
import { IProjectView, TViewFilterProps, TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { satisfiesDateFilter } from "./filter.helper";
|
||||
|
||||
/**
|
||||
* order views base on TViewFiltersSortKey
|
||||
* @param views
|
||||
* @param sortByKey
|
||||
* @param sortByOrder
|
||||
* @returns
|
||||
*/
|
||||
export const orderViews = (
|
||||
views: IProjectView[],
|
||||
sortByKey: TViewFiltersSortKey | undefined,
|
||||
sortByOrder: TViewFiltersSortBy
|
||||
): IProjectView[] => {
|
||||
if (views.length === 0 || !sortByKey) return [];
|
||||
|
||||
let iterableFunction;
|
||||
if (sortByKey === "name") {
|
||||
iterableFunction = (view: IProjectView) => view.name?.toLowerCase();
|
||||
}
|
||||
if (sortByKey === "created_at") {
|
||||
iterableFunction = (view: IProjectView) => view.created_at;
|
||||
}
|
||||
if (sortByKey === "updated_at") {
|
||||
iterableFunction = (view: IProjectView) => view.updated_at;
|
||||
}
|
||||
|
||||
if (!iterableFunction) return [];
|
||||
|
||||
return orderBy(views, [iterableFunction], [sortByOrder]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the passed down view should be filtered or not
|
||||
* @param view
|
||||
* @param filters
|
||||
* @returns
|
||||
*/
|
||||
export const shouldFilterView = (view: IProjectView, filters: TViewFilterProps | undefined): boolean => {
|
||||
let fallsInFilters = true;
|
||||
Object.keys(filters ?? {}).forEach((key) => {
|
||||
const filterKey = key as keyof TViewFilterProps;
|
||||
if (filterKey === "owned_by" && filters?.owned_by && filters.owned_by.length > 0) {
|
||||
fallsInFilters = fallsInFilters && filters.owned_by.includes(`${view.created_by}`);
|
||||
}
|
||||
|
||||
if (filterKey === "created_at" && filters?.created_at && filters.created_at.length > 0) {
|
||||
const createdDate = getDate(view.created_at);
|
||||
filters?.created_at.forEach((dateFilter) => {
|
||||
fallsInFilters = fallsInFilters && !!createdDate && satisfiesDateFilter(createdDate, dateFilter);
|
||||
});
|
||||
}
|
||||
|
||||
if (filterKey === "accessTypes" && filters?.accessTypes && filters?.accessTypes?.length > 0) {
|
||||
fallsInFilters = filters.accessTypes.includes(view.access);
|
||||
}
|
||||
});
|
||||
|
||||
if (filters?.favorites && !view.is_favorite) fallsInFilters = false;
|
||||
|
||||
return fallsInFilters;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns the name of the project after checking for untitled view
|
||||
* @param {string | undefined} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getViewName = (name: string | undefined) => {
|
||||
if (name === undefined) return "";
|
||||
if (!name || name.trim() === "") return "Untitled";
|
||||
return name;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue