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

@ -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}