diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
index 509b7561af..71beecdc54 100644
--- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
+++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
@@ -1,21 +1,25 @@
"use client";
+import { useState } from "react";
+import cloneDeep from "lodash/cloneDeep";
import isEmpty from "lodash/isEmpty";
-import isEqual from "lodash/isEqual";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
+// types
+import { cn } from "@plane/editor";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
-// hooks
//ui
-import { Button } from "@plane/ui";
// components
import { AppliedFiltersList } from "@/components/issues";
-// types
+import { UpdateViewComponent } from "@/components/views/update-view-component";
+import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
+// constants
import { GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace";
-// constants
+// hooks
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "@/hooks/store";
+import { getAreFiltersEqual } from "../../../utils";
type Props = {
globalViewId: string;
@@ -33,11 +37,15 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const { globalViewMap, updateGlobalView } = useGlobalView();
const { captureEvent } = useEventTracker();
const {
+ data,
membership: { currentWorkspaceRole },
} = useUser();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
// derived values
- const userFilters = filters?.[globalViewId]?.filters;
+ const issueFilters = filters?.[globalViewId];
+ const userFilters = issueFilters?.filters;
const viewDetails = globalViewMap[globalViewId];
// filters whose value not null or empty array
@@ -89,14 +97,15 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
);
};
+ const viewFilters = {
+ filters: cloneDeep(appliedFilters ?? {}),
+ display_filters: cloneDeep(issueFilters?.displayFilters),
+ display_properties: cloneDeep(issueFilters?.displayProperties),
+ };
const handleUpdateView = () => {
if (!workspaceSlug || !globalViewId) return;
- updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), {
- filters: {
- ...(appliedFilters ?? {}),
- },
- }).then((res) => {
+ updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => {
if (res)
captureEvent(GLOBAL_VIEW_UPDATED, {
view_id: res.id,
@@ -107,34 +116,56 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
});
};
- const areFiltersEqual = isEqual(appliedFilters ?? {}, viewDetails?.filters ?? {});
+ const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes);
+ const isLocked = viewDetails?.is_locked;
+ const isOwner = viewDetails?.owned_by === data?.id;
+ const areAppliedFiltersEmpty = isEmpty(appliedFilters);
+
// return if no filters are applied
- if (isEmpty(appliedFilters) && areFiltersEqual) return null;
+ if (areAppliedFiltersEmpty && areFiltersEqual) return null;
return (
-
-
+ setIsModalOpen(false)}
+ preLoadedData={{
+ name: `${viewDetails?.name} 2`,
+ description: viewDetails?.description,
+ ...viewFilters,
+ }}
/>
+
+
- {!isDefaultView && !areFiltersEqual && isAuthorizedUser && (
- <>
-
-
- >
- )}
-
+ {!isDefaultView && (
+
+ )}
+
+ >
);
});
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 291c031b36..c99343bb71 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
@@ -1,18 +1,22 @@
"use client";
+import { useState } from "react";
+import cloneDeep from "lodash/cloneDeep";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
-import { IIssueFilterOptions } from "@plane/types";
-// hooks
-import { Button } from "@plane/ui";
-import { AppliedFiltersList } from "@/components/issues";
-import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
-import { useIssues, useLabel, useProjectState, useProjectView } from "@/hooks/store";
-import { getAreFiltersEqual } from "../../../utils";
-// components
-// ui
// types
+import { IIssueFilterOptions } from "@plane/types";
+// components
+import { AppliedFiltersList } from "@/components/issues";
+import { CreateUpdateProjectViewModal } from "@/components/views";
+import { UpdateViewComponent } from "@/components/views/update-view-component";
+// constants
+import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
+import { EUserWorkspaceRoles } from "@/constants/workspace";
+// hooks
+import { useIssues, useLabel, useProjectView, useUser } from "@/hooks/store";
+import { getAreFiltersEqual } from "../../../utils";
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
// router
@@ -22,8 +26,13 @@ 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,
+ membership: { currentWorkspaceRole },
+ } = useUser();
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
// derived values
const viewDetails = viewId ? viewMap[viewId.toString()] : null;
const userFilters = issueFilters?.filters;
@@ -81,46 +90,56 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
};
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
-
+ const viewFilters = {
+ filters: cloneDeep(appliedFilters ?? {}),
+ display_filters: cloneDeep(issueFilters?.displayFilters),
+ display_properties: cloneDeep(issueFilters?.displayProperties),
+ };
// return if no filters are applied
if (isEmpty(appliedFilters) && areFiltersEqual) return null;
const handleUpdateView = () => {
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
- updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), {
- filters: {
- ...(appliedFilters ?? {}),
- },
- display_filters: {
- ...issueFilters?.displayFilters,
- },
- display_properties: {
- ...issueFilters?.displayProperties,
- },
- });
+ updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), viewFilters);
};
+ const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
+
+ const isLocked = !!viewDetails?.is_locked;
+ const isOwner = viewDetails?.owned_by === data?.id;
+
return (
+
setIsModalOpen(false)}
+ workspaceSlug={workspaceSlug.toString()}
+ projectId={projectId.toString()}
+ preLoadedData={{
+ name: `${viewDetails?.name} 2`,
+ description: viewDetails?.description,
+ logo_props: viewDetails?.logo_props,
+ ...viewFilters,
+ }}
+ />
-
- {!areFiltersEqual && (
-
-
-
- )}
+
);
});
diff --git a/web/core/components/issues/issue-layouts/utils.tsx b/web/core/components/issues/issue-layouts/utils.tsx
index 6314d5beb6..7840bb4621 100644
--- a/web/core/components/issues/issue-layouts/utils.tsx
+++ b/web/core/components/issues/issue-layouts/utils.tsx
@@ -4,6 +4,7 @@ import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tre
import clone from "lodash/clone";
import concat from "lodash/concat";
import isEqual from "lodash/isEqual";
+import isNil from "lodash/isNil";
import pull from "lodash/pull";
import uniq from "lodash/uniq";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
@@ -21,6 +22,7 @@ import {
IIssueFilters,
IProjectView,
TGroupedIssues,
+ IWorkspaceView,
} from "@plane/types";
// ui
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
@@ -570,11 +572,24 @@ export const handleGroupDragDrop = async (
export const getAreFiltersEqual = (
appliedFilters: IIssueFilterOptions | undefined,
issueFilters: IIssueFilters | undefined,
- viewDetails: IProjectView | null
-) =>
- isEqual(appliedFilters ?? {}, viewDetails?.filters ?? {}) &&
- isEqual(issueFilters?.displayFilters ?? {}, viewDetails?.display_filters ?? {}) &&
- isEqual(issueFilters?.displayProperties ?? {}, viewDetails?.display_properties ?? {});
+ viewDetails: IProjectView | IWorkspaceView | null
+) => {
+ if (isNil(appliedFilters) || isNil(issueFilters) || isNil(viewDetails)) return true;
+
+ return (
+ isEqual(appliedFilters, viewDetails.filters) &&
+ isEqual(issueFilters.displayFilters, viewDetails.display_filters) &&
+ isEqual(removeNillKeys(issueFilters.displayProperties), removeNillKeys(viewDetails.display_properties))
+ );
+};
+
+/**
+ * method that removes Null or undefined Keys from object
+ * @param obj
+ * @returns
+ */
+export const removeNillKeys =
(obj: T) =>
+ Object.fromEntries(Object.entries(obj ?? {}).filter(([key, value]) => key && !isNil(value)));
/**
* This Method returns if the the grouped values are subGrouped
diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx
index bd022eed26..06c0579e40 100644
--- a/web/core/components/pages/editor/header/extra-options.tsx
+++ b/web/core/components/pages/editor/header/extra-options.tsx
@@ -2,13 +2,14 @@
import { useState } from "react";
import { observer } from "mobx-react";
-import { Lock, Sparkle } from "lucide-react";
+import { Sparkle } from "lucide-react";
// editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
// ui
import { ArchiveIcon } from "@plane/ui";
// components
import { GptAssistantPopover } from "@/components/core";
+import { LockedComponent } from "@/components/icons/locked-component";
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
@@ -40,12 +41,7 @@ export const PageExtraOptions: React.FC = observer((props) => {
return (
- {is_locked && (
-
-
- Locked
-
- )}
+ {is_locked &&
}
{archived_at && (
diff --git a/web/core/components/views/form.tsx b/web/core/components/views/form.tsx
index 9a43cd2a9c..4371481dfc 100644
--- a/web/core/components/views/form.tsx
+++ b/web/core/components/views/form.tsx
@@ -11,11 +11,12 @@ import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, PhotoFilterIcon,
import { Logo } from "@/components/common";
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
-import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
+import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// helpers
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
// hooks
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
+import { LayoutDropDown } from "../dropdowns/layout";
type Props = {
data?: IProjectView | null;
@@ -190,7 +191,7 @@ export const ProjectViewForm: React.FC
= observer((props) => {
/>
)}
/>
- {errors?.name?.message}
+ {errors?.name?.message?.toString()}
@@ -211,7 +212,17 @@ export const ProjectViewForm: React.FC
= observer((props) => {
)}
/>
-
+
+
(
+ onChange(selectedValue)}
+ value={value}
+ />
+ )}
+ />
) => void;
+ handleUpdateView: () => void;
+ lockedTooltipContent?: string;
+};
+
+export const UpdateViewComponent = (props: Props) => {
+ const {
+ isLocked,
+ areFiltersEqual,
+ isOwner,
+ isAuthorizedUser,
+ setIsModalOpen,
+ handleUpdateView,
+ lockedTooltipContent,
+ } = props;
+
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ useEffect(() => {
+ if (areFiltersEqual) {
+ setIsUpdating(false);
+ }
+ }, [areFiltersEqual]);
+
+ // Change state while updating view to have a feedback
+ const updateButton = isUpdating ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {isLocked ? (
+
+ ) : (
+ !areFiltersEqual &&
+ isAuthorizedUser && (
+ <>
+
+ {isOwner && <>{updateButton}>}
+ >
+ )
+ )}
+
+ );
+};
diff --git a/web/core/components/views/view-list-item-action.tsx b/web/core/components/views/view-list-item-action.tsx
index 66f9a0622c..b7d37e3d72 100644
--- a/web/core/components/views/view-list-item-action.tsx
+++ b/web/core/components/views/view-list-item-action.tsx
@@ -1,14 +1,16 @@
import React, { FC, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
+import { Earth, Lock } from "lucide-react";
// types
import { IProjectView } from "@plane/types";
// ui
-import { FavoriteStar } from "@plane/ui";
+import { Tooltip, FavoriteStar } from "@plane/ui";
// components
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
// constants
import { EUserProjectRoles } from "@/constants/project";
+import { EViewAccess } from "@/constants/views";
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks
@@ -39,6 +41,8 @@ export const ViewListItemAction: FC = observer((props) => {
const totalFilters = calculateTotalFilters(view.filters ?? {});
+ const access = view.access;
+
// handlers
const handleAddToFavorites = () => {
if (!workspaceSlug || !projectId) return;
@@ -70,8 +74,14 @@ export const ViewListItemAction: FC = observer((props) => {
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
+
+
+ {access === EViewAccess.PUBLIC ? : }
+
+
+
{/* created by */}
- {createdByDetails && }
+ {}
{isEditingAllowed && (
= observer((props) => {
},
];
+ const isSelected = viewId === globalViewId;
+ const isPrivateView = view.access === EViewAccess.PRIVATE;
+
+ let customButton = (
+
+
+ {view.name}
+
+ {isPrivateView && (
+
+ )}
+
+ );
+
+ if (!isSelected) {
+ customButton = (
+
+ {customButton}
+
+ );
+ }
+
return (
<>
setUpdateViewModal(false)} />
@@ -85,38 +114,7 @@ export const WorkspaceViewQuickActions: React.FC = observer((props) => {
-
- {viewId === globalViewId ? (
-
- {view.name}
-
- ) : (
-
-
- {view.name}
-
-
- )}
- >
- }
- placement="bottom-end"
- menuItemsClassName="z-20"
- closeOnSelect
- >
+
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts
index 95a761e544..0e2a04b784 100644
--- a/web/core/constants/issue.ts
+++ b/web/core/constants/issue.ts
@@ -138,17 +138,34 @@ export const ISSUE_EXTRA_OPTIONS: {
{ key: "show_empty_groups", title: "Show empty groups" }, // filter on front-end
];
+export const ISSUE_LAYOUT_MAP = {
+ [EIssueLayoutTypes.LIST]: { key: EIssueLayoutTypes.LIST, title: "List Layout", label: "List", icon: List },
+ [EIssueLayoutTypes.KANBAN]: { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", label: "Kanban", icon: Kanban },
+ [EIssueLayoutTypes.CALENDAR]: {
+ key: EIssueLayoutTypes.CALENDAR,
+ title: "Calendar Layout",
+ label: "Calendar",
+ icon: Calendar,
+ },
+ [EIssueLayoutTypes.SPREADSHEET]: {
+ key: EIssueLayoutTypes.SPREADSHEET,
+ title: "Spreadsheet Layout",
+ label: "Spreadsheet",
+ icon: Sheet,
+ },
+ [EIssueLayoutTypes.GANTT]: {
+ key: EIssueLayoutTypes.GANTT,
+ title: "Gantt Chart Layout",
+ label: "Gantt",
+ icon: GanttChartSquare,
+ },
+};
+
export const ISSUE_LAYOUTS: {
key: EIssueLayoutTypes;
title: string;
icon: any;
-}[] = [
- { key: EIssueLayoutTypes.LIST, title: "List Layout", icon: List },
- { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", icon: Kanban },
- { key: EIssueLayoutTypes.CALENDAR, title: "Calendar Layout", icon: Calendar },
- { key: EIssueLayoutTypes.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
- { key: EIssueLayoutTypes.GANTT, title: "Gantt Chart Layout", icon: GanttChartSquare },
-];
+}[] = Object.values(ISSUE_LAYOUT_MAP);
export interface ILayoutDisplayFiltersOptions {
filters: (keyof IIssueFilterOptions)[];
diff --git a/web/core/constants/views.ts b/web/core/constants/views.ts
new file mode 100644
index 0000000000..aab38da3f0
--- /dev/null
+++ b/web/core/constants/views.ts
@@ -0,0 +1,4 @@
+export enum EViewAccess {
+ PRIVATE,
+ PUBLIC,
+}
diff --git a/web/core/store/issue/project-views/filter.store.ts b/web/core/store/issue/project-views/filter.store.ts
index 40169e89cf..a82bfd8f87 100644
--- a/web/core/store/issue/project-views/filter.store.ts
+++ b/web/core/store/issue/project-views/filter.store.ts
@@ -240,10 +240,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
);
}
- await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
- display_filters: _filters.displayFilters,
- });
-
break;
}
case EIssueFilterType.DISPLAY_PROPERTIES: {
@@ -260,9 +256,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
});
});
- await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
- display_properties: _filters.displayProperties,
- });
break;
}
case EIssueFilterType.KANBAN_FILTERS: {
diff --git a/web/core/store/issue/workspace/filter.store.ts b/web/core/store/issue/workspace/filter.store.ts
index 7bf2d9cd94..8ac69e7fd5 100644
--- a/web/core/store/issue/workspace/filter.store.ts
+++ b/web/core/store/issue/workspace/filter.store.ts
@@ -249,11 +249,6 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
display_filters: _filters.displayFilters,
});
- else
- await this.issueFilterService.updateView(workspaceSlug, viewId, {
- display_filters: _filters.displayFilters,
- });
-
break;
}
case EIssueFilterType.DISPLAY_PROPERTIES: {
@@ -268,15 +263,11 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
);
});
+ if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
+ this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
+ display_properties: _filters.displayProperties,
+ });
});
- if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
- this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
- display_properties: _filters.displayProperties,
- });
- else
- await this.issueFilterService.updateView(workspaceSlug, viewId, {
- display_properties: _filters.displayProperties,
- });
break;
}