fix: enable global/ all issues (#3405)

* fix global issues and views

* remove separate layouts for specific views

* add permissions to views

* fix global issues filters

---------

Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
This commit is contained in:
rahulramesha 2024-01-18 15:51:17 +05:30 committed by sriram veeraghanta
parent c9337d4a41
commit ea3a0362b0
23 changed files with 214 additions and 252 deletions

View File

@ -12,7 +12,12 @@ import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plan
// icons
import { List, PlusIcon, Sheet } from "lucide-react";
// types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TStaticViewTypes,
} from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
import { EUserWorkspaceRoles } from "constants/workspace";
@ -35,7 +40,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const { workspaceSlug, globalViewId } = router.query;
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL);
const {
membership: { currentWorkspaceRole },
@ -47,6 +52,8 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
workspace: { workspaceMemberIds },
} = useMember();
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !globalViewId) return;

View File

@ -25,13 +25,14 @@ type Props = {
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
labels?: IIssueLabel[] | undefined;
states?: IState[] | undefined;
alwaysAllowEditing?: boolean;
};
const membersFilters = ["assignees", "mentions", "created_by", "subscriber"];
const dateFilters = ["start_date", "target_date"];
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states } = props;
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props;
// store hooks
const {
membership: { currentProjectRole },
@ -41,7 +42,7 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
if (Object.keys(appliedFilters).length === 0) return null;
const isEditingAllowed = currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER);
return (
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">

View File

@ -15,13 +15,13 @@ export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;
const {
project: { getProjectMemberDetails },
workspace: { getWorkspaceMemberDetails },
} = useMember();
return (
<>
{values.map((memberId) => {
const memberDetails = getProjectMemberDetails(memberId)?.member;
const memberDetails = getWorkspaceMemberDetails(memberId)?.member;
if (!memberDetails) return null;

View File

@ -1,32 +1,48 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import isEqual from "lodash/isEqual";
// hooks
import { useIssues, useLabel } from "hooks/store";
import { useGlobalView, useIssues, useLabel, useUser } from "hooks/store";
//ui
import { Button } from "@plane/ui";
// components
import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "@plane/types";
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
export const GlobalViewsAppliedFiltersRoot = observer(() => {
type Props = {
globalViewId: string;
};
export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const { globalViewId } = props;
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
const { workspaceSlug } = router.query;
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL);
const {
workspace: { workspaceLabels },
} = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
const {
membership: { currentWorkspaceRole },
} = useUser();
// derived values
const userFilters = issueFilters?.filters;
const userFilters = filters?.[globalViewId]?.filters;
const viewDetails = globalViewMap[globalViewId];
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
let appliedFilters: IIssueFilterOptions | undefined = undefined;
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
if (!appliedFilters) appliedFilters = {};
appliedFilters[key as keyof IIssueFilterOptions] = value;
});
@ -70,29 +86,24 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
);
};
// const handleUpdateView = () => {
// if (!workspaceSlug || !globalViewId || !viewDetails) return;
const handleUpdateView = () => {
if (!workspaceSlug || !globalViewId) return;
// globalViewsStore.updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), {
// query_data: {
// ...viewDetails.query_data,
// filters: {
// ...(storedFilters ?? {}),
// },
// },
// });
// };
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), {
filters: {
...(appliedFilters ?? {}),
},
});
};
// update stored filters when view details are fetched
// useEffect(() => {
// if (!globalViewId || !viewDetails) return;
const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters);
// if (!globalViewFiltersStore.storedFilters[globalViewId.toString()])
// globalViewFiltersStore.updateStoredFilters(globalViewId.toString(), viewDetails?.query_data?.filters ?? {});
// }, [globalViewId, globalViewFiltersStore, viewDetails]);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes);
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
if (!appliedFilters && areFiltersEqual) return null;
return (
<div className="flex items-start justify-between gap-4 p-4">
@ -101,13 +112,17 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
appliedFilters={appliedFilters ?? {}}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
alwaysAllowEditing
/>
{/* {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data.filters ?? {}) && (
<Button variant="primary" onClick={handleUpdateView}>
Update view
</Button>
)} */}
{!isDefaultView && !areFiltersEqual && isAuthorizedUser && (
<>
<div />
<Button variant="primary" onClick={handleUpdateView}>
Update view
</Button>
</>
)}
</div>
);
});

View File

@ -1,13 +1,12 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import isEqual from "lodash/isEqual";
// hooks
import { useIssues, useLabel, useProjectState, useProjectView } from "hooks/store";
// components
import { AppliedFiltersList } from "components/issues";
// ui
import { Button } from "@plane/ui";
// helpers
import { areFiltersDifferent } from "helpers/filter.helper";
// types
import { IIssueFilterOptions } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
@ -33,10 +32,11 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const viewDetails = viewId ? viewMap[viewId.toString()] : null;
const userFilters = issueFilters?.filters;
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
let appliedFilters: IIssueFilterOptions | undefined = undefined;
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
if (!appliedFilters) appliedFilters = {};
appliedFilters[key as keyof IIssueFilterOptions] = value;
});
@ -78,9 +78,9 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { ...newFilters }, viewId);
};
const areFiltersEqual = isEqual(appliedFilters, viewDetails?.filters);
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0 && !areFiltersDifferent(appliedFilters, viewDetails?.filters ?? {}))
return null;
if (!appliedFilters && areFiltersEqual) return null;
const handleUpdateView = () => {
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
@ -95,19 +95,23 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
return (
<div className="flex items-center justify-between gap-4 p-4">
<AppliedFiltersList
appliedFilters={appliedFilters}
appliedFilters={appliedFilters ?? {}}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []}
states={projectStates}
alwaysAllowEditing
/>
{viewDetails?.filters && areFiltersDifferent(appliedFilters, viewDetails?.filters ?? {}) && (
<div className="flex flex-shrink-0 items-center justify-center">
<Button variant="primary" size="sm" onClick={handleUpdateView}>
Update view
</Button>
</div>
{!areFiltersEqual && (
<>
<div />
<div className="flex flex-shrink-0 items-center justify-center">
<Button variant="primary" size="sm" onClick={handleUpdateView}>
Update view
</Button>
</div>
</>
)}
</div>
);

View File

@ -16,43 +16,43 @@ import { EIssueActions } from "../types";
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
type Props = {
type?: TStaticViewTypes | null;
};
export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
const { type = null } = props;
export const AllIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query as { workspaceSlug: string; globalViewId: string };
const { workspaceSlug, globalViewId } = router.query;
// store
const {
issuesFilter: { issueFilters, fetchFilters, updateFilters },
issuesFilter: { filters, fetchFilters, updateFilters },
issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue },
issueMap,
} = useIssues(EIssuesStoreType.GLOBAL);
const { dataViewId, issueIds } = groupedIssueIds;
const {
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const { fetchAllGlobalViews } = useGlobalView();
// derived values
const currentIssueView = type ?? globalViewId;
useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => {
if (workspaceSlug) {
await fetchAllGlobalViews(workspaceSlug);
await fetchAllGlobalViews(workspaceSlug.toString());
}
});
useSWR(
workspaceSlug && currentIssueView ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${currentIssueView}` : null,
workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null,
async () => {
if (workspaceSlug && currentIssueView) {
await fetchAllGlobalViews(workspaceSlug);
await fetchFilters(workspaceSlug, currentIssueView);
await fetchIssues(workspaceSlug, currentIssueView, groupedIssueIds ? "mutation" : "init-loader");
if (workspaceSlug && globalViewId) {
await fetchAllGlobalViews(workspaceSlug.toString());
await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
await fetchIssues(
workspaceSlug.toString(),
globalViewId.toString(),
groupedIssueIds ? "mutation" : "init-loader"
);
}
}
);
@ -65,22 +65,21 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
return !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
};
const issueIds = (groupedIssueIds ?? []) as TUnGroupedIssues;
const issuesArray = issueIds?.filter((id: string) => id && issueMap?.[id]).map((id: string) => issueMap?.[id]);
const issueFilters = globalViewId ? filters?.[globalViewId.toString()] : undefined;
const issueActions = useMemo(
() => ({
[EIssueActions.UPDATE]: async (issue: TIssue) => {
const projectId = issue.project_id;
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !globalViewId) return;
await updateIssue(workspaceSlug, projectId, issue.id, issue, currentIssueView);
await updateIssue(workspaceSlug.toString(), projectId, issue.id, issue, globalViewId.toString());
},
[EIssueActions.DELETE]: async (issue: TIssue) => {
const projectId = issue.project_id;
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !globalViewId) return;
await removeIssue(workspaceSlug, projectId, issue.id, currentIssueView);
await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString());
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -100,22 +99,22 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug) return;
updateFilters(workspaceSlug, undefined, EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter });
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.DISPLAY_FILTERS, { ...updatedDisplayFilter });
},
[updateFilters, workspaceSlug]
);
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
{globalViewId != currentIssueView && (loader === "init-loader" || !groupedIssueIds) ? (
{!globalViewId || globalViewId !== dataViewId || loader === "init-loader" || !issueIds ? (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
) : (
<>
<GlobalViewsAppliedFiltersRoot />
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId} />
{(groupedIssueIds ?? {}).length == 0 ? (
{(issueIds ?? {}).length == 0 ? (
<>{/* <GlobalViewEmptyState /> */}</>
) : (
<div className="relative h-full w-full overflow-auto">
@ -123,7 +122,7 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
displayProperties={issueFilters?.displayProperties ?? {}}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issuesArray}
issueIds={issueIds}
quickActions={(issue) => (
<AllIssueQuickActions
issue={issue}
@ -133,7 +132,7 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props) => {
)}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
viewId={currentIssueView}
viewId={globalViewId}
/>
</div>
)}

View File

@ -30,7 +30,7 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}` : null,
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
async () => {
if (workspaceSlug && projectId && viewId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());

View File

@ -58,8 +58,6 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
const issueIds = (issueStore.groupedIssueIds ?? []) as TUnGroupedIssues;
const issues = issueIds?.filter((id) => id && issueMap?.[id]).map((id) => issueMap?.[id]);
const handleIssues = useCallback(
async (issue: TIssue, action: EIssueActions) => {
if (issueActions[action]) {
@ -109,7 +107,7 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
displayProperties={issueFiltersStore.issueFilters?.displayProperties ?? {}}
displayFilters={issueFiltersStore.issueFilters?.displayFilters ?? {}}
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
issues={issues}
issueIds={issueIds}
quickActions={renderQuickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}

View File

@ -10,7 +10,7 @@ type Props = {
displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: TIssue[];
issueIds: string[];
isEstimateEnabled: boolean;
quickActions: (
issue: TIssue,
@ -27,7 +27,7 @@ export const SpreadsheetTable = observer((props: Props) => {
displayProperties,
displayFilters,
handleDisplayFilterUpdate,
issues,
issueIds,
isEstimateEnabled,
portalElement,
quickActions,
@ -44,7 +44,7 @@ export const SpreadsheetTable = observer((props: Props) => {
isEstimateEnabled={isEstimateEnabled}
/>
<tbody>
{issues.map(({ id }) => (
{issueIds.map((id) => (
<SpreadsheetIssueRow
key={id}
issueId={id}

View File

@ -14,7 +14,7 @@ type Props = {
displayProperties: IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
issues: TIssue[] | undefined;
issueIds: string[] | undefined;
quickActions: (
issue: TIssue,
customActionButton?: React.ReactElement,
@ -39,7 +39,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
displayProperties,
displayFilters,
handleDisplayFilterUpdate,
issues,
issueIds,
quickActions,
handleIssues,
quickAddCallback,
@ -91,7 +91,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
};
}, []);
if (!issues || issues.length === 0)
if (!issueIds || issueIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
<Spinner />
@ -106,7 +106,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
displayProperties={displayProperties}
displayFilters={displayFilters}
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
issues={issues}
issueIds={issueIds}
isEstimateEnabled={isEstimateEnabled}
portalElement={portalRef}
quickActions={quickActions}

View File

@ -61,12 +61,12 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
});
}, [data, preLoadedData, reset]);
const selectedFilters = watch("query_data")?.filters;
const selectedFilters = watch("filters");
const clearAllFilters = () => {
if (!selectedFilters) return;
setValue("query_data.filters", {});
setValue("filters", {});
};
return (
@ -120,7 +120,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
<div>
<Controller
control={control}
name="query_data.filters"
name="filters"
render={({ field: { onChange, value: filters } }) => (
<FiltersDropdown title="Filters">
<FilterSelection

View File

@ -4,11 +4,11 @@ import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// store hooks
import { useGlobalView } from "hooks/store";
import { useGlobalView, useUser } from "hooks/store";
// components
import { CreateUpdateWorkspaceViewModal } from "components/workspace";
// constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
const ViewTab = observer((props: { viewId: string }) => {
const { viewId } = props;
@ -46,6 +46,9 @@ export const GlobalViewsHeader: React.FC = observer(() => {
const { workspaceSlug, globalViewId } = router.query;
// store hooks
const { currentWorkspaceViews } = useGlobalView();
const {
membership: { currentWorkspaceRole },
} = useUser();
// bring the active view to the centre of the header
useEffect(() => {
@ -56,7 +59,8 @@ export const GlobalViewsHeader: React.FC = observer(() => {
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });
}, [globalViewId]);
const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
return (
<>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
@ -66,7 +70,7 @@ export const GlobalViewsHeader: React.FC = observer(() => {
<Link key={tab.key} href={`/${workspaceSlug}/workspace-views/${tab.key}`}>
<span
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
isTabSelected(tab.key)
tab.key === globalViewId
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
}`}
@ -81,13 +85,15 @@ export const GlobalViewsHeader: React.FC = observer(() => {
))}
</div>
<button
type="button"
className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
onClick={() => setCreateViewModal(true)}
>
<Plus className="h-4 w-4 text-custom-primary-200" />
</button>
{isAuthorizedUser && (
<button
type="button"
className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400"
onClick={() => setCreateViewModal(true)}
>
<Plus className="h-4 w-4 text-custom-primary-200" />
</button>
)}
</div>
</>
);

View File

@ -36,8 +36,8 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
const payloadData: Partial<IWorkspaceView> = {
...payload,
query: {
...payload.query_data?.filters,
filters: {
...payload?.filters,
},
};
@ -67,7 +67,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
const payloadData: Partial<IWorkspaceView> = {
...payload,
query: {
...payload.query_data?.filters,
...payload?.filters,
},
};

View File

@ -30,7 +30,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
if (!view) return null;
const totalFilters = calculateTotalFilters(view.query_data.filters ?? {});
const totalFilters = calculateTotalFilters(view.filters ?? {});
return (
<>

View File

@ -14,19 +14,3 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
.reduce((curr, prev) => curr + prev, 0)
: 0;
// check if there is any difference between the saved filters and the current filters
export const areFiltersDifferent = (filtersSet1: IIssueFilterOptions, filtersSet2: IIssueFilterOptions) => {
for (const [key, value] of Object.entries(filtersSet1) as [keyof IIssueFilterOptions, string[] | null][]) {
if (value) {
if (Array.isArray(value) && Array.isArray(filtersSet2[key])) {
if (value.length !== filtersSet2[key]?.length) return true;
for (let i = 0; i < value.length; i++) {
if (!filtersSet2[key]?.includes(value[i])) return true;
}
} else if (value !== filtersSet2[key]) return true;
}
}
return false;
};

View File

@ -1,24 +0,0 @@
import { ReactElement } from "react";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { AllIssueLayoutRoot } from "components/issues/issue-layouts";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPageWithLayout } from "lib/types";
const GlobalViewAllIssuesPage: NextPageWithLayout = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<AllIssueLayoutRoot type="all-issues" />
</div>
</div>
);
GlobalViewAllIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewAllIssuesPage;

View File

@ -1,24 +0,0 @@
import { ReactElement } from "react";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { AllIssueLayoutRoot } from "components/issues/issue-layouts";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPageWithLayout } from "lib/types";
const GlobalViewAssignedIssuesPage: NextPageWithLayout = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<AllIssueLayoutRoot type="assigned" />
</div>
</div>
);
GlobalViewAssignedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewAssignedIssuesPage;

View File

@ -1,24 +0,0 @@
import { ReactElement } from "react";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { AllIssueLayoutRoot } from "components/issues";
// layouts
import { AppLayout } from "layouts/app-layout";
// types
import { NextPageWithLayout } from "lib/types";
const GlobalViewCreatedIssuesPage: NextPageWithLayout = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<AllIssueLayoutRoot type="created" />
</div>
</div>
);
GlobalViewCreatedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewCreatedIssuesPage;

View File

@ -1,24 +0,0 @@
import { ReactElement } from "react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GlobalViewsHeader } from "components/workspace";
import { GlobalIssuesHeader } from "components/headers";
import { AllIssueLayoutRoot } from "components/issues";
// types
import { NextPageWithLayout } from "lib/types";
const GlobalViewSubscribedIssuesPage: NextPageWithLayout = () => (
<div className="h-full overflow-hidden bg-custom-background-100">
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
<GlobalViewsHeader />
<AllIssueLayoutRoot type="subscribed" />
</div>
</div>
);
GlobalViewSubscribedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return <AppLayout header={<GlobalIssuesHeader activeLayout="spreadsheet" />}>{page}</AppLayout>;
};
export default GlobalViewSubscribedIssuesPage;

View File

@ -8,6 +8,7 @@ import {
IIssueFiltersResponse,
TIssueKanbanFilters,
TIssueParams,
TStaticViewTypes,
} from "@plane/types";
// constants
import { isNil } from "constants/common";
@ -109,6 +110,39 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
subscriber: filters?.subscriber || null,
});
/**
* This PR is to get the filters of the fixed global views
* @param currentUserId current logged in user id
* @param type fixed view type
* @returns filterOptions based on views
*/
getComputedFiltersBasedOnViews = (currentUserId: string | undefined, type: TStaticViewTypes) => {
const noFilters = this.computedFilters({});
if (!currentUserId) return noFilters;
switch (type) {
case "assigned":
return {
...noFilters,
assignees: [currentUserId],
};
case "created":
return {
...noFilters,
created_by: [currentUserId],
};
case "subscribed":
return {
...noFilters,
subscriber: [currentUserId],
};
case "all-issues":
default:
return noFilters;
}
};
/**
* @description This method is used to apply the display filters on the issues
* @param {IIssueDisplayFilterOptions} displayFilters

View File

@ -160,9 +160,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
});
this.rootIssueStore.projectViewIssues.fetchIssues(workspaceSlug, projectId, "mutation", viewId);
await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
filters: _filters.filters,
});
break;
case EIssueFilterType.DISPLAY_FILTERS:
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;

View File

@ -14,6 +14,7 @@ import {
TIssueKanbanFilters,
IIssueFilters,
TIssueParams,
TStaticViewTypes,
} from "@plane/types";
// constants
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
@ -27,7 +28,7 @@ export interface IWorkspaceIssuesFilter {
// computed
issueFilters: IIssueFilters | undefined;
appliedFilters: Partial<Record<TIssueParams, string | boolean>> | undefined;
// action
// fetch action
fetchFilters: (workspaceSlug: string, viewId: string) => Promise<void>;
updateFilters: (
workspaceSlug: string,
@ -36,6 +37,9 @@ export interface IWorkspaceIssuesFilter {
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters,
viewId?: string | undefined
) => Promise<void>;
//helper action
getIssueFilters: (viewId: string | undefined) => IIssueFilters | undefined;
getAppliedFilters: (viewId: string) => Partial<Record<TIssueParams, string | boolean>> | undefined;
}
export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWorkspaceIssuesFilter {
@ -54,9 +58,12 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
// computed
issueFilters: computed,
appliedFilters: computed,
// actions
// fetch actions
fetchFilters: action,
updateFilters: action,
// helper actions
getIssueFilters: action,
getAppliedFilters: action,
});
// root store
this.rootIssueStore = _rootStore;
@ -64,8 +71,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
this.issueFilterService = new WorkspaceService();
}
get issueFilters() {
const viewId = this.rootIssueStore.globalViewId;
getIssueFilters = (viewId: string | undefined) => {
if (!viewId) return undefined;
const displayFilters = this.filters[viewId] || undefined;
@ -74,10 +80,12 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
return _filters;
}
};
get appliedFilters() {
const userFilters = this.issueFilters;
getAppliedFilters = (viewId: string | undefined) => {
if (!viewId) return undefined;
const userFilters = this.getIssueFilters(viewId);
if (!userFilters) return undefined;
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues");
@ -92,6 +100,16 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true;
return filteredRouteParams;
};
get issueFilters() {
const viewId = this.rootIssueStore.globalViewId;
return this.getIssueFilters(viewId);
}
get appliedFilters() {
const viewId = this.rootIssueStore.globalViewId;
return this.getAppliedFilters(viewId);
}
fetchFilters = async (workspaceSlug: string, viewId: TWorkspaceFilters) => {
@ -105,15 +123,17 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
};
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.GLOBAL, workspaceSlug, undefined, viewId);
filters = this.computedFilters(_filters?.filters);
displayFilters = this.computedDisplayFilters(_filters?.displayFilters);
displayProperties = this.computedDisplayProperties(_filters?.displayProperties);
displayFilters = this.computedDisplayFilters(_filters?.display_filters);
displayProperties = this.computedDisplayProperties(_filters?.display_properties);
kanbanFilters = {
group_by: _filters?.kanbanFilters?.group_by || [],
sub_group_by: _filters?.kanbanFilters?.sub_group_by || [],
group_by: _filters?.kanban_filters?.group_by || [],
sub_group_by: _filters?.kanban_filters?.sub_group_by || [],
};
if (!["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) {
const currentUserId = this.rootIssueStore.currentUserId;
filters = this.getComputedFiltersBasedOnViews(currentUserId, viewId as TStaticViewTypes);
} else {
const _filters = await this.issueFilterService.getViewDetails(workspaceSlug, viewId);
filters = this.computedFilters(_filters?.filters);
displayFilters = this.computedDisplayFilters(_filters?.display_filters);
@ -160,16 +180,7 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
set(this.filters, [viewId, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
});
});
this.rootIssueStore.workspaceIssues.fetchIssues(workspaceSlug, viewId, "mutation");
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
filters: _filters.filters,
});
else
await this.issueFilterService.updateView(workspaceSlug, viewId, {
filters: _filters.filters,
});
break;
case EIssueFilterType.DISPLAY_FILTERS:
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;

View File

@ -15,7 +15,7 @@ export interface IWorkspaceIssues {
issues: { [viewId: string]: string[] };
viewFlags: ViewFlags;
// computed
groupedIssueIds: TUnGroupedIssues | undefined;
groupedIssueIds: { dataViewId: string; issueIds: TUnGroupedIssues | undefined };
// actions
fetchIssues: (workspaceSlug: string, viewId: string, loadType: TLoader) => Promise<TIssue[]>;
createIssue: (
@ -59,7 +59,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
makeObservable(this, {
// observable
loader: observable.ref,
issues: observable.ref,
issues: observable,
// computed
groupedIssueIds: computed,
// action
@ -77,30 +77,32 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
get groupedIssueIds() {
const viewId = this.rootIssueStore.globalViewId;
if (!viewId) return undefined;
if (!viewId) return { dataViewId: "", issueIds: undefined };
const displayFilters = this.rootIssueStore?.workspaceIssuesFilter?.issueFilters?.displayFilters;
if (!displayFilters) return undefined;
const displayFilters = this.rootIssueStore?.workspaceIssuesFilter?.filters?.[viewId]?.displayFilters;
if (!displayFilters) return { dataViewId: viewId, issueIds: undefined };
const orderBy = displayFilters?.order_by;
const viewIssueIds = this.issues[viewId] ?? [];
const viewIssueIds = this.issues[viewId];
if (!viewIssueIds) return { dataViewId: viewId, issueIds: undefined };
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
if (!_issues) return undefined;
if (!_issues) return { dataViewId: viewId, issueIds: [] };
let issues: TIssue | TUnGroupedIssues | undefined = undefined;
let issueIds: TIssue | TUnGroupedIssues | undefined = undefined;
issues = this.unGroupedIssues(orderBy ?? "-created_at", _issues);
issueIds = this.unGroupedIssues(orderBy ?? "-created_at", _issues);
return issues;
return { dataViewId: viewId, issueIds };
}
fetchIssues = async (workspaceSlug: string, viewId: string, loadType: TLoader = "init-loader") => {
try {
this.loader = loadType;
const params = this.rootIssueStore?.workspaceIssuesFilter?.appliedFilters;
const params = this.rootIssueStore?.workspaceIssuesFilter?.getAppliedFilters(viewId);
const response = await this.workspaceService.getViewIssues(workspaceSlug, params);
runInAction(() => {