diff --git a/packages/types/src/views.d.ts b/packages/types/src/views.d.ts
index 6ce598168..29db479ed 100644
--- a/packages/types/src/views.d.ts
+++ b/packages/types/src/views.d.ts
@@ -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;
+};
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx
index a7e271425..fd192cc60 100644
--- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx
+++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/header.tsx
@@ -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(() => {
- {canUserCreateIssue && (
-
-
+
+
+ {canUserCreateView && (
-
- )}
+ )}
+
+ {isFiltersApplied && (
+
+
+
+ )}
>
);
});
diff --git a/web/app/invitations/page.tsx b/web/app/invitations/page.tsx
index 89ff66ab8..a56ff6ad4 100644
--- a/web/app/invitations/page.tsx
+++ b/web/app/invitations/page.tsx
@@ -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();
diff --git a/web/app/onboarding/page.tsx b/web/app/onboarding/page.tsx
index 9e318e521..ffc198698 100644
--- a/web/app/onboarding/page.tsx
+++ b/web/app/onboarding/page.tsx
@@ -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",
diff --git a/web/app/workspace-invitations/page.tsx b/web/app/workspace-invitations/page.tsx
index 2844713db..a68290198 100644
--- a/web/app/workspace-invitations/page.tsx
+++ b/web/app/workspace-invitations/page.tsx
@@ -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();
diff --git a/web/ce/services/index.ts b/web/ce/services/index.ts
index 9ff8c7dff..3a7bd7005 100644
--- a/web/ce/services/index.ts
+++ b/web/ce/services/index.ts
@@ -1 +1,2 @@
export * from "./project";
+export * from "./workspace.service";
\ No newline at end of file
diff --git a/web/ce/services/project/index.ts b/web/ce/services/project/index.ts
index 29b17e55d..6c0fc3df4 100644
--- a/web/ce/services/project/index.ts
+++ b/web/ce/services/project/index.ts
@@ -1 +1,2 @@
export * from "./estimate.service";
+export * from "./view.service";
\ No newline at end of file
diff --git a/web/ce/services/project/view.service.ts b/web/ce/services/project/view.service.ts
new file mode 100644
index 000000000..07872394a
--- /dev/null
+++ b/web/ce/services/project/view.service.ts
@@ -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();
+ }
+}
diff --git a/web/ce/services/workspace.service.ts b/web/ce/services/workspace.service.ts
new file mode 100644
index 000000000..59fe39c69
--- /dev/null
+++ b/web/ce/services/workspace.service.ts
@@ -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();
+ }
+}
diff --git a/web/core/components/account/auth-forms/auth-header.tsx b/web/core/components/account/auth-forms/auth-header.tsx
index c9b01d5e9..424fcbfe6 100644
--- a/web/core/components/account/auth-forms/auth-header.tsx
+++ b/web/core/components/account/auth-forms/auth-header.tsx
@@ -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;
diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx
index 8c64240fd..246485262 100644
--- a/web/core/components/command-palette/command-modal.tsx
+++ b/web/core/components/command-palette/command-modal.tsx
@@ -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
diff --git a/web/core/components/pages/list/applied-filters/date.tsx b/web/core/components/common/applied-filters/date.tsx
similarity index 100%
rename from web/core/components/pages/list/applied-filters/date.tsx
rename to web/core/components/common/applied-filters/date.tsx
index 67ad973b3..06d182647 100644
--- a/web/core/components/pages/list/applied-filters/date.tsx
+++ b/web/core/components/common/applied-filters/date.tsx
@@ -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;
diff --git a/web/core/components/common/applied-filters/index.ts b/web/core/components/common/applied-filters/index.ts
new file mode 100644
index 000000000..77f9f6304
--- /dev/null
+++ b/web/core/components/common/applied-filters/index.ts
@@ -0,0 +1,2 @@
+export * from "./date";
+export * from "./members";
diff --git a/web/core/components/pages/list/applied-filters/members.tsx b/web/core/components/common/applied-filters/members.tsx
similarity index 100%
rename from web/core/components/pages/list/applied-filters/members.tsx
rename to web/core/components/common/applied-filters/members.tsx
diff --git a/web/core/components/pages/list/filters/created-at.tsx b/web/core/components/common/filters/created-at.tsx
similarity index 100%
rename from web/core/components/pages/list/filters/created-at.tsx
rename to web/core/components/common/filters/created-at.tsx
diff --git a/web/core/components/pages/list/filters/created-by.tsx b/web/core/components/common/filters/created-by.tsx
similarity index 100%
rename from web/core/components/pages/list/filters/created-by.tsx
rename to web/core/components/common/filters/created-by.tsx
diff --git a/web/core/components/common/filters/index.ts b/web/core/components/common/filters/index.ts
new file mode 100644
index 000000000..6f0b5dbde
--- /dev/null
+++ b/web/core/components/common/filters/index.ts
@@ -0,0 +1,2 @@
+export * from "./created-at";
+export * from "./created-by";
diff --git a/web/core/components/common/product-updates-modal.tsx b/web/core/components/common/product-updates-modal.tsx
index dc10e6abd..9e1112db1 100644
--- a/web/core/components/common/product-updates-modal.tsx
+++ b/web/core/components/common/product-updates-modal.tsx
@@ -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;
diff --git a/web/core/components/integration/github/single-user-select.tsx b/web/core/components/integration/github/single-user-select.tsx
index 55c1e7118..0f50a5a12 100644
--- a/web/core/components/integration/github/single-user-select.tsx
+++ b/web/core/components/integration/github/single-user-select.tsx
@@ -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";
diff --git a/web/core/components/integration/jira/import-users.tsx b/web/core/components/integration/jira/import-users.tsx
index eb63e6bc1..0f8f82165 100644
--- a/web/core/components/integration/jira/import-users.tsx
+++ b/web/core/components/integration/jira/import-users.tsx
@@ -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
diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx
index c99343bb7..71928239a 100644
--- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx
+++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx
@@ -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}
/>
diff --git a/web/core/components/onboarding/create-workspace.tsx b/web/core/components/onboarding/create-workspace.tsx
index b8c708abc..d9d789ede 100644
--- a/web/core/components/onboarding/create-workspace.tsx
+++ b/web/core/components/onboarding/create-workspace.tsx
@@ -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) => Promise;
diff --git a/web/core/components/onboarding/invitations.tsx b/web/core/components/onboarding/invitations.tsx
index 4afd7953a..3c9d57e20 100644
--- a/web/core/components/onboarding/invitations.tsx
+++ b/web/core/components/onboarding/invitations.tsx
@@ -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;
diff --git a/web/core/components/onboarding/invite-members.tsx b/web/core/components/onboarding/invite-members.tsx
index db4042caf..5b1c24f01 100644
--- a/web/core/components/onboarding/invite-members.tsx
+++ b/web/core/components/onboarding/invite-members.tsx
@@ -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";
diff --git a/web/core/components/pages/list/applied-filters/index.ts b/web/core/components/pages/list/applied-filters/index.ts
index 687cffb99..1efe34c51 100644
--- a/web/core/components/pages/list/applied-filters/index.ts
+++ b/web/core/components/pages/list/applied-filters/index.ts
@@ -1,3 +1 @@
-export * from "./date";
-export * from "./members";
export * from "./root";
diff --git a/web/core/components/pages/list/applied-filters/root.tsx b/web/core/components/pages/list/applied-filters/root.tsx
index f889739cc..5ae9d7bb1 100644
--- a/web/core/components/pages/list/applied-filters/root.tsx
+++ b/web/core/components/pages/list/applied-filters/root.tsx
@@ -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
diff --git a/web/core/components/pages/list/filters/index.ts b/web/core/components/pages/list/filters/index.ts
index e95258f0e..1efe34c51 100644
--- a/web/core/components/pages/list/filters/index.ts
+++ b/web/core/components/pages/list/filters/index.ts
@@ -1,3 +1 @@
-export * from "./created-at";
-export * from "./created-by";
export * from "./root";
diff --git a/web/core/components/pages/list/filters/root.tsx b/web/core/components/pages/list/filters/root.tsx
index e228bd235..9b2aaeb4b 100644
--- a/web/core/components/pages/list/filters/root.tsx
+++ b/web/core/components/pages/list/filters/root.tsx
@@ -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;
diff --git a/web/core/components/views/filters/applied-filters.tsx b/web/core/components/views/filters/applied-filters.tsx
new file mode 100644
index 000000000..d532ff61d
--- /dev/null
+++ b/web/core/components/views/filters/applied-filters.tsx
@@ -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) => {
+ const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props;
+
+ if (!appliedFilters) return null;
+ if (Object.keys(appliedFilters).length === 0) return null;
+
+ const isEditingAllowed = alwaysAllowEditing;
+
+ return (
+
+ {Object.entries(appliedFilters).map(([key, value]) => {
+ const filterKey = key as keyof TViewFilterProps;
+
+ if (!value) return;
+ if (Array.isArray(value) && value.length === 0) return;
+
+ return (
+
+
+
{replaceUnderscoreIfSnakeCase(filterKey)}
+ {DATE_FILTERS.includes(filterKey) && (
+
handleRemoveFilter(filterKey, val)}
+ values={Array.isArray(value) ? (value as string[]) : []}
+ />
+ )}
+ {MEMBERS_FILTERS.includes(filterKey) && (
+ handleRemoveFilter(filterKey, val)}
+ values={Array.isArray(value) ? (value as string[]) : []}
+ />
+ )}
+ {isEditingAllowed && (
+
+ )}
+
+
+ );
+ })}
+ {isEditingAllowed && (
+
+ )}
+
+ );
+};
diff --git a/web/core/components/views/filters/filter-selection.tsx b/web/core/components/views/filters/filter-selection.tsx
new file mode 100644
index 000000000..9278ec410
--- /dev/null
+++ b/web/core/components/views/filters/filter-selection.tsx
@@ -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: (filterKey: T, filterValue: TViewFilters[T]) => void;
+ memberIds?: string[] | undefined;
+};
+
+export const ViewFiltersSelection: React.FC = 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 (
+
+
+
+
+ setFiltersSearchQuery(e.target.value)}
+ autoFocus
+ />
+ {filtersSearchQuery !== "" && (
+
+ )}
+
+
+
+
+
+ handleFiltersUpdate("filters", {
+ ...filters.filters,
+ favorites: !filters.filters?.favorites,
+ })
+ }
+ title="Favorites"
+ />
+
+
+ {/* created date */}
+
+ handleFilters("created_at", val)}
+ searchQuery={filtersSearchQuery}
+ />
+
+
+ {/* created by */}
+
+ handleFilters("owned_by", val)}
+ searchQuery={filtersSearchQuery}
+ memberIds={memberIds}
+ />
+
+
+
+ );
+});
diff --git a/web/core/components/views/filters/order-by.tsx b/web/core/components/views/filters/order-by.tsx
new file mode 100644
index 000000000..3f607e53d
--- /dev/null
+++ b/web/core/components/views/filters/order-by.tsx
@@ -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) => {
+ 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 (
+
+
+ {orderByDetails?.label}
+
+
+ }
+ placement="bottom-end"
+ maxHeight="lg"
+ closeOnSelect
+ >
+ {VIEW_SORTING_KEY_OPTIONS.map((option) => (
+
+ onChange({
+ key: option.key,
+ })
+ }
+ >
+ {option.label}
+ {sortKey === option.key && }
+
+ ))}
+
+ {sortByOptions.map((option) => (
+ {
+ if (!option.isSelected)
+ onChange({
+ order: option.key as TViewFiltersSortBy,
+ });
+ }}
+ >
+ {option.label}
+ {option.isSelected && }
+
+ ))}
+
+ );
+};
diff --git a/web/core/components/views/form.tsx b/web/core/components/views/form.tsx
index 4371481df..0f9020724 100644
--- a/web/core/components/views/form.tsx
+++ b/web/core/components/views/form.tsx
@@ -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 = {
name: "",
description: "",
+ display_properties: getComputedDisplayProperties(),
+ display_filters: getComputedDisplayFilters(),
};
export const ProjectViewForm: React.FC = observer((props) => {
diff --git a/web/core/components/views/update-view-component.tsx b/web/core/components/views/update-view-component.tsx
index 9e598d528..5b3f8c79b 100644
--- a/web/core/components/views/update-view-component.tsx
+++ b/web/core/components/views/update-view-component.tsx
@@ -51,14 +51,14 @@ export const UpdateViewComponent = (props: Props) => {
);
return (
-
+
{isLocked ? (
) : (
!areFiltersEqual &&
isAuthorizedUser && (
<>
-
+
{
+ if (val.key) updateFilters("sortKey", val.key);
+ if (val.order) updateFilters("sortBy", val.order);
+ }}
+ />
+ }
+ title="Filters"
+ placement="bottom-end"
+ isFiltersApplied={false}
+ >
+
+
);
});
diff --git a/web/core/components/views/views-list.tsx b/web/core/components/views/views-list.tsx
index cc962bd15..6001b668e 100644
--- a/web/core/components/views/views-list.tsx
+++ b/web/core/components/views/views-list.tsx
@@ -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 ;
+ const projectViews = getProjectViews(projectId?.toString());
+ const filteredProjectViews = getFilteredProjectViews(projectId?.toString());
- // derived values
- const viewsList = projectViewIds.map((viewId) => getViewById(viewId));
+ if (loader || !projectViews || !filteredProjectViews) return ;
- const filteredViewsList = viewsList.filter((v) => v?.name.toLowerCase().includes(searchQuery.toLowerCase()));
+ if (filteredProjectViews.length === 0 && projectViews) {
+ return (
+
+
+
0 ? NameFilterImage : AllFiltersImage}
+ className="h-36 sm:h-48 w-36 sm:w-48 mx-auto"
+ alt="No matching modules"
+ />
+ No matching views
+
+ {filters.searchQuery.length > 0
+ ? "Remove the search criteria to see all views"
+ : "Remove the filters to see all views"}
+
+
+
+ );
+ }
return (
<>
- {viewsList.length > 0 ? (
+ {filteredProjectViews.length > 0 ? (
- {filteredViewsList.length > 0 ? (
- filteredViewsList.map((view) => )
+ {filteredProjectViews.length > 0 ? (
+ filteredProjectViews.map((view) => )
) : (
No results found
)}
diff --git a/web/core/components/workspace/create-workspace-form.tsx b/web/core/components/workspace/create-workspace-form.tsx
index c293c43b0..f9234290d 100644
--- a/web/core/components/workspace/create-workspace-form.tsx
+++ b/web/core/components/workspace/create-workspace-form.tsx
@@ -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;
diff --git a/web/core/components/workspace/views/form.tsx b/web/core/components/workspace/views/form.tsx
index 9901c5462..61ef96602 100644
--- a/web/core/components/workspace/views/form.tsx
+++ b/web/core/components/workspace/views/form.tsx
@@ -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 = {
name: "",
description: "",
+ display_properties: getComputedDisplayProperties(),
+ display_filters: getComputedDisplayFilters(),
};
export const WorkspaceViewForm: React.FC = observer((props) => {
diff --git a/web/core/constants/views.ts b/web/core/constants/views.ts
index aab38da3f..a8e501db4 100644
--- a/web/core/constants/views.ts
+++ b/web/core/constants/views.ts
@@ -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" },
+];
diff --git a/web/core/services/view.service.ts b/web/core/services/view.service.ts
index f4fccbae0..b05331775 100644
--- a/web/core/services/view.service.ts
+++ b/web/core/services/view.service.ts
@@ -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): Promise {
diff --git a/web/core/services/workspace.service.ts b/web/core/services/workspace.service.ts
index 79bde3b8c..e1aa9b7cd 100644
--- a/web/core/services/workspace.service.ts
+++ b/web/core/services/workspace.service.ts
@@ -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 {
diff --git a/web/core/store/global-view.store.ts b/web/core/store/global-view.store.ts
index 52d826bb6..8fd883e1c 100644
--- a/web/core/store/global-view.store.ts
+++ b/web/core/store/global-view.store.ts
@@ -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): Promise =>
- await this.workspaceService.createView(workspaceSlug, data).then((response) => {
- runInAction(() => {
- set(this.globalViewMap, response.id, response);
- });
- return response;
+ createGlobalView = async (workspaceSlug: string, data: Partial): Promise => {
+ 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);
+ });
+ }
+ };
}
diff --git a/web/core/store/issue/helpers/issue-filter-helper.store.ts b/web/core/store/issue/helpers/issue-filter-helper.store.ts
index 42eccc6a6..380e1268f 100644
--- a/web/core/store/issue/helpers/issue-filter-helper.store.ts
+++ b/web/core/store/issue/helpers/issue-filter-helper.store.ts
@@ -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: () => {
diff --git a/web/core/store/issue/project-views/filter.store.ts b/web/core/store/issue/project-views/filter.store.ts
index a82bfd8f8..511ce8508 100644
--- a/web/core/store/issue/project-views/filter.store.ts
+++ b/web/core/store/issue/project-views/filter.store.ts
@@ -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
diff --git a/web/core/store/issue/workspace/filter.store.ts b/web/core/store/issue/workspace/filter.store.ts
index 8ac69e7fd..e9b001d35 100644
--- a/web/core/store/issue/workspace/filter.store.ts
+++ b/web/core/store/issue/workspace/filter.store.ts
@@ -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
diff --git a/web/core/store/issue/workspace/issue.store.ts b/web/core/store/issue/workspace/issue.store.ts
index a3bf7a6cc..6960135f1 100644
--- a/web/core/store/issue/workspace/issue.store.ts
+++ b/web/core/store/issue/workspace/issue.store.ts
@@ -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";
diff --git a/web/core/store/member/workspace-member.store.ts b/web/core/store/member/workspace-member.store.ts
index 444c1d1a3..8c7e68aa2 100644
--- a/web/core/store/member/workspace-member.store.ts
+++ b/web/core/store/member/workspace-member.store.ts
@@ -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";
diff --git a/web/core/store/project-view.store.ts b/web/core/store/project-view.store.ts
index ba7d738b7..13c366a14 100644
--- a/web/core/store/project-view.store.ts
+++ b/web/core/store/project-view.store.ts
@@ -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;
// observables
- searchQuery: string;
viewMap: Record;
+ 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;
fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
@@ -32,6 +37,8 @@ export interface IProjectViewStore {
data: Partial
) => Promise;
deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
+ updateFilters: (filterKey: T, filterValue: TViewFilters[T]) => void;
+ clearAllFilters: () => void;
// favorites actions
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise;
@@ -41,9 +48,9 @@ export class ProjectViewStore implements IProjectViewStore {
// observables
loader: boolean = false;
viewMap: Record = {};
- searchQuery: string = "";
//loaders
fetchedMap: Record = {};
+ 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 = (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
*/
- createView = async (workspaceSlug: string, projectId: string, data: Partial): Promise =>
- 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): Promise => {
+ 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
): Promise => {
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
diff --git a/web/core/store/user/user-membership.store.ts b/web/core/store/user/user-membership.store.ts
index e4675b758..f9e9e6468 100644
--- a/web/core/store/user/user-membership.store.ts
+++ b/web/core/store/user/user-membership.store.ts
@@ -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";
diff --git a/web/core/store/workspace/index.ts b/web/core/store/workspace/index.ts
index eba260eaa..4efba1e13 100644
--- a/web/core/store/workspace/index.ts
+++ b/web/core/store/workspace/index.ts
@@ -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
diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts
index 670d6d0e0..42c140ce3 100644
--- a/web/helpers/filter.helper.ts
+++ b/web/helpers/filter.helper.ts
@@ -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;
}
}
diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts
index e8ec791c4..6cf6588e4 100644
--- a/web/helpers/issue.helper.ts
+++ b/web/helpers/issue.helper.ts
@@ -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, dirtyFields: { [key: string]: boolean | undefined }) {
const changedFields: Partial = {};
@@ -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,
+});
\ No newline at end of file
diff --git a/web/helpers/project-views.helpers.ts b/web/helpers/project-views.helpers.ts
new file mode 100644
index 000000000..a8fe269be
--- /dev/null
+++ b/web/helpers/project-views.helpers.ts
@@ -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;
+};