diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index f4d3dbbb5..c77b58892 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1516,11 +1516,9 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView): return Response(serializer.data, status=status.HTTP_201_CREATED) def get(self, request, slug): - ( - workspace_properties, - _, - ) = WorkspaceUserProperties.objects.get_or_create( - user=request.user, workspace__slug=slug + workspace = Workspace.objects.get(slug=slug) + workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create( + user=request.user, workspace=workspace ) serializer = WorkspaceUserPropertiesSerializer(workspace_properties) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/packages/types/src/view/base.d.ts b/packages/types/src/view/base.d.ts index d41cba270..35363714c 100644 --- a/packages/types/src/view/base.d.ts +++ b/packages/types/src/view/base.d.ts @@ -5,10 +5,10 @@ import { } from "./filter"; export type TViewTypes = - | "WORKSPACE_YOUR_VIEWS" - | "WORKSPACE_VIEWS" - | "PROJECT_VIEWS" - | "PROJECT_YOUR_VIEWS"; + | "WORKSPACE_PRIVATE_VIEWS" + | "WORKSPACE_PUBLIC_VIEWS" + | "PROJECT_PRIVATE_VIEWS" + | "PROJECT_PUBLIC_VIEWS"; declare enum EViewAccess { "public" = 0, diff --git a/packages/types/src/view/user-base.d.ts b/packages/types/src/view/user-base.d.ts index b076b4e6b..4ed191468 100644 --- a/packages/types/src/view/user-base.d.ts +++ b/packages/types/src/view/user-base.d.ts @@ -7,13 +7,10 @@ import { export type TUserView = { id: string | undefined; workspace: string | undefined; - project: string | undefined; - module: string | undefined; - cycle: string | undefined; - filters: TViewFilters | undefined; - display_filters: TViewDisplayFilters | undefined; - display_properties: TViewDisplayProperties | undefined; user: string | undefined; + filters: TViewFilters; + display_filters: TViewDisplayFilters; + display_properties: TViewDisplayProperties; created_by: string | undefined; updated_by: string | undefined; created_at: Date | undefined; diff --git a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx index 59013a3e3..481f2882c 100644 --- a/web/components/analytics/custom-analytics/sidebar/sidebar.tsx +++ b/web/components/analytics/custom-analytics/sidebar/sidebar.tsx @@ -23,14 +23,13 @@ import { ANALYTICS } from "constants/fetch-keys"; type Props = { analytics: IAnalyticsResponse | undefined; params: IAnalyticsParams; - fullScreen: boolean; isProjectLevel: boolean; }; const analyticsService = new AnalyticsService(); export const CustomAnalyticsSidebar: React.FC = observer((props) => { - const { analytics, params, fullScreen, isProjectLevel = false } = props; + const { analytics, params, isProjectLevel = false } = props; // router const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -139,13 +138,7 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => { const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds; return ( -
+
@@ -164,16 +157,16 @@ export const CustomAnalyticsSidebar: React.FC = observer((props) => {
)}
+
- {fullScreen ? ( - <> - {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( - - )} - - - ) : null} + <> + {!isProjectLevel && selectedProjects && selectedProjects.length > 0 && ( + + )} + +
+
{workspaceViewTabOptions.map((tab) => ( -
{tab.title} -
+ ))}
@@ -136,9 +167,31 @@ export const AllIssuesViewRoot: FC = observer((props) => { viewId={viewId} viewType={viewType} viewOperations={viewOperations} + baseRoute={baseRoute} />
+
+ +
+ + {/*
+ +
*/} +
{/*
= observer((props) => { />
*/} - {/*
+
= observer((props) => { viewType={viewType} viewOperations={viewOperations} /> -
*/} +
- {/*
- Filters -
*/} +
+ +
- {/*
- Display Filters -
*/} +
+ +
- {/* {!viewDetailStore?.is_local_view && ( -
-
Edit
+
+
+
- )} */} +
)} - {currentCreateEditViewId != undefined && ( - + {/* create edit modal */} + {viewOperationsToggle.type && viewOperationsToggle.viewId && ( + + {["CREATE", "EDIT"].includes(viewOperationsToggle.type) && ( + + )} + + {["DUPLICATE"].includes(viewOperationsToggle.type) && ( + + )} + + {["DELETE"].includes(viewOperationsToggle.type) && ( + + )} + )}
); diff --git a/web/components/view/confirmation-modals/delete.tsx b/web/components/view/confirmation-modals/delete.tsx new file mode 100644 index 000000000..970a567a6 --- /dev/null +++ b/web/components/view/confirmation-modals/delete.tsx @@ -0,0 +1,77 @@ +import { FC, Fragment, useCallback, useEffect, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "@plane/ui"; +// types +import { TViewOperations } from "../types"; + +type TViewDeleteConfirmationModal = { + viewId: string; + viewOperations: TViewOperations; +}; + +export const ViewDeleteConfirmationModal: FC = (props) => { + const { viewId, viewOperations } = props; + // state + const [modalToggle, setModalToggle] = useState(false); + const [loader, setLoader] = useState(false); + + const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]); + const modalClose = useCallback(() => { + setModalToggle(false); + }, [setModalToggle]); + + useEffect(() => { + if (viewId) modalOpen(); + }, [viewId, modalOpen, modalClose]); + + const onContinue = async () => { + setLoader(true); + setLoader(false); + }; + + return ( + + + +
+ + +
+
+ + +
Content
+ +
+ + +
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/view/confirmation-modals/duplicate.tsx b/web/components/view/confirmation-modals/duplicate.tsx new file mode 100644 index 000000000..6253151d6 --- /dev/null +++ b/web/components/view/confirmation-modals/duplicate.tsx @@ -0,0 +1,77 @@ +import { FC, Fragment, useCallback, useEffect, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "@plane/ui"; +// types +import { TViewOperations } from "../types"; + +type TViewDuplicateConfirmationModal = { + viewId: string; + viewOperations: TViewOperations; +}; + +export const ViewDuplicateConfirmationModal: FC = (props) => { + const { viewId, viewOperations } = props; + // state + const [modalToggle, setModalToggle] = useState(false); + const [loader, setLoader] = useState(false); + + const modalOpen = useCallback(() => setModalToggle(true), [setModalToggle]); + const modalClose = useCallback(() => { + setModalToggle(false); + }, [setModalToggle]); + + useEffect(() => { + if (viewId) modalOpen(); + }, [viewId, modalOpen, modalClose]); + + const onContinue = async () => { + setLoader(true); + setLoader(false); + }; + + return ( + + + +
+ + +
+
+ + +
Content
+ +
+ + +
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/view/display-filters/dropdown.tsx b/web/components/view/display-filters/dropdown.tsx new file mode 100644 index 000000000..d6a1bf1e1 --- /dev/null +++ b/web/components/view/display-filters/dropdown.tsx @@ -0,0 +1,124 @@ +import { FC, Fragment, ReactNode, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { MonitorDot } from "lucide-react"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { ViewDisplayPropertiesRoot } from "../"; +// types +import { TViewOperations } from "../types"; +import { TViewTypes } from "@plane/types"; + +type TViewDisplayFiltersDropdown = { + workspaceSlug: string; + projectId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; + baseRoute: string; + children?: ReactNode; + displayDropdownText?: boolean; +}; + +export const ViewDisplayFiltersDropdown: FC = observer((props) => { + const { + workspaceSlug, + projectId, + viewId, + viewType, + viewOperations, + baseRoute, + children, + displayDropdownText = true, + } = props; + // state + const [dropdownToggle, setDropdownToggle] = useState(false); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const handleDropdownOpen = () => setDropdownToggle(true); + const handleDropdownClose = () => setDropdownToggle(false); + const handleDropdownToggle = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!dropdownToggle) handleDropdownOpen(); + else handleDropdownClose(); + }; + + useOutsideClickDetector(dropdownRef, handleDropdownClose); + + return ( + + + + + + {dropdownToggle && ( + +
+
+
+
Properties
+ +
+ +
Content
+
+
+
+ )} +
+ ); +}); diff --git a/web/components/view/display-filters/root.tsx b/web/components/view/display-filters/root.tsx index 53991e3a1..54e250273 100644 --- a/web/components/view/display-filters/root.tsx +++ b/web/components/view/display-filters/root.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; type TViewDisplayFiltersRoot = { workspaceSlug: string; projectId: string | undefined; - viewId: string | undefined; + viewId: string; }; export const ViewDisplayFiltersRoot: FC = (props) => { diff --git a/web/components/view/display-properties/root.tsx b/web/components/view/display-properties/root.tsx index 4b35bb62e..5c2601dca 100644 --- a/web/components/view/display-properties/root.tsx +++ b/web/components/view/display-properties/root.tsx @@ -1,17 +1,50 @@ import { FC } from "react"; +// types +import { TViewDisplayProperties, TViewTypes } from "@plane/types"; +import { TViewOperations } from "../types"; type TViewDisplayPropertiesRoot = { workspaceSlug: string; projectId: string | undefined; - viewId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; }; export const ViewDisplayPropertiesRoot: FC = (props) => { - const { workspaceSlug, projectId, viewId } = props; + const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; + + const displayProperties: Partial[] = [ + "key", + "state", + "labels", + "priority", + "assignee", + "start_date", + "due_date", + "sub_issue_count", + "attachment_count", + "estimate", + "link", + ]; return ( -
-
ViewDisplayPropertiesRoot
+
+ {displayProperties.map((property) => ( +
{}} + > + {["key"].includes(property) ? "ID" : property.replaceAll("_", " ")} +
+ ))}
); }; diff --git a/web/components/view/filters/dropdown.tsx b/web/components/view/filters/dropdown.tsx new file mode 100644 index 000000000..0a22ccaf6 --- /dev/null +++ b/web/components/view/filters/dropdown.tsx @@ -0,0 +1,133 @@ +import { FC, Fragment, ReactNode, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { usePopper } from "react-popper"; +import { ListFilter, Search } from "lucide-react"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { ViewFiltersRoot } from "../"; +// types +import { TViewOperations } from "../types"; +import { TViewTypes } from "@plane/types"; + +type TViewFiltersDropdown = { + workspaceSlug: string; + projectId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; + baseRoute: string; + children?: ReactNode; + displayDropdownText?: boolean; +}; + +export const ViewFiltersDropdown: FC = observer((props) => { + const { + workspaceSlug, + projectId, + viewId, + viewType, + viewOperations, + baseRoute, + children, + displayDropdownText = true, + } = props; + // state + const [dropdownToggle, setDropdownToggle] = useState(false); + const [query, setQuery] = useState(""); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const handleDropdownOpen = () => setDropdownToggle(true); + const handleDropdownClose = () => setDropdownToggle(false); + const handleDropdownToggle = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!dropdownToggle) handleDropdownOpen(); + else handleDropdownClose(); + }; + + useOutsideClickDetector(dropdownRef, handleDropdownClose); + + return ( + + + + + + {dropdownToggle && ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search for a view..." + displayValue={(assigned: any) => assigned?.name} + autoFocus + /> +
+ +
+ +
+
+
+ )} +
+ ); +}); diff --git a/web/components/view/filters/filter-item-root.tsx b/web/components/view/filters/filter-item-root.tsx new file mode 100644 index 000000000..e6da91ed9 --- /dev/null +++ b/web/components/view/filters/filter-item-root.tsx @@ -0,0 +1,87 @@ +import { FC, useState } from "react"; +import { observer } from "mobx-react-lite"; +import concat from "lodash/concat"; +import pull from "lodash/pull"; +import uniq from "lodash/uniq"; +// hooks +import { useViewFilter, useViewDetail } from "hooks/store"; +// components +import { ViewFiltersItem, ViewFilterSelection } from "../"; +// types +import { TViewOperations } from "../types"; +import { TViewFilters, TViewTypes } from "@plane/types"; + +type TViewFiltersItemRoot = { + workspaceSlug: string; + projectId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; + baseRoute: string; + filterKey: keyof TViewFilters; +}; + +export const ViewFiltersItemRoot: FC = observer((props) => { + const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey } = props; + // hooks + const viewFilterHelper = useViewFilter(workspaceSlug, projectId); + const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + // state + const [viewAll, setViewAll] = useState(false); + + const propertyIds = viewFilterHelper?.filterIdsWithKey(filterKey) || []; + + const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds; + + const handlePropertySelection = (_propertyId: string) => { + const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || []; + const isSelected = _propertyIds?.includes(_propertyId) || false; + viewOperations?.setFilters({ + [filterKey]: isSelected ? pull(_propertyIds, _propertyId) : uniq(concat(_propertyIds, [_propertyId])), + }); + }; + + if (propertyIds.length <= 0) + return
No items are available.
; + return ( +
+ {filterPropertyIds.map((propertyId) => ( + + ))} + + {propertyIds.length > 5 && ( +
setViewAll((prevData) => !prevData)} + > + {viewAll ? "View less" : "View all"} +
+ )} +
+ ); +}); diff --git a/web/components/view/filters/filter-item.tsx b/web/components/view/filters/filter-item.tsx new file mode 100644 index 000000000..dae19f5b9 --- /dev/null +++ b/web/components/view/filters/filter-item.tsx @@ -0,0 +1,40 @@ +import { FC, Fragment } from "react"; +import { CheckSquare } from "lucide-react"; +// hooks +import { useViewFilter } from "hooks/store"; +// types +import { TViewFilters, TViewTypes } from "@plane/types"; +import { TViewOperations } from "../types"; +// helpers +// import { filterPropertyItemByFilterKeyAndId } from "../helpers/filters"; + +type TViewFiltersItem = { + workspaceSlug: string; + projectId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; + baseRoute: string; + filterKey: keyof TViewFilters; + propertyId: string; +}; + +export const ViewFiltersItem: FC = (props) => { + const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey, propertyId } = props; + // hooks + const viewFilterHelper = useViewFilter(workspaceSlug, projectId); + + const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined; + + if (!propertyDetail) return <>; + return ( + +
+ {propertyDetail?.icon || } +
+
+ {propertyDetail?.label || propertyId} +
+
+ ); +}; diff --git a/web/components/view/filters/filter-selection.tsx b/web/components/view/filters/filter-selection.tsx new file mode 100644 index 000000000..2aa0c54c2 --- /dev/null +++ b/web/components/view/filters/filter-selection.tsx @@ -0,0 +1,40 @@ +import { FC } from "react"; +import { Check } from "lucide-react"; +import { observer } from "mobx-react-lite"; +// hooks +import { useViewDetail } from "hooks/store"; +// types +import { TViewFilters, TViewTypes } from "@plane/types"; +import { TViewOperations } from "../types"; + +type TViewFilterSelection = { + workspaceSlug: string; + projectId: string | undefined; + viewId: string; + viewType: TViewTypes; + viewOperations: TViewOperations; + baseRoute: string; + filterKey: keyof TViewFilters; + propertyId: string; +}; + +export const ViewFilterSelection: FC = observer((props) => { + const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute, filterKey, propertyId } = props; + + const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + + const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || []; + const isSelected = propertyIds?.includes(propertyId) || false; + + return ( +
+ {isSelected && } +
+ ); +}); diff --git a/web/components/view/filters/root.tsx b/web/components/view/filters/root.tsx index d8bee27ab..c2ecaf8f5 100644 --- a/web/components/view/filters/root.tsx +++ b/web/components/view/filters/root.tsx @@ -1,34 +1,73 @@ -import { FC } from "react"; +import { FC, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { ChevronDown, ChevronUp, Search } from "lucide-react"; +import concat from "lodash/concat"; +import uniq from "lodash/uniq"; +import filter from "lodash/filter"; +// hooks +import { useViewDetail } from "hooks/store"; +// components +import { ViewFiltersItemRoot } from "../"; // types import { TViewOperations } from "../types"; +import { TViewFilters, TViewTypes } from "@plane/types"; +import { VIEW_DEFAULT_FILTER_PARAMETERS } from "constants/view"; type TViewFiltersRoot = { workspaceSlug: string; projectId: string | undefined; - viewId: string | undefined; + viewId: string; + viewType: TViewTypes; viewOperations: TViewOperations; + baseRoute: string; }; -export const ViewFiltersRoot: FC = (props) => { - const { workspaceSlug, projectId, viewId, viewOperations } = props; - - const filters = { - project: ["1", "2", "3", "4", "5", "6"], - priority: ["1", "2", "3", "4", "5", "6"], - state: ["1", "2", "3", "4", "5", "6"], - state_group: ["1", "2", "3", "4", "5", "6"], - assignees: ["1", "2", "3", "4", "5", "6"], - mentions: ["1", "2", "3", "4", "5", "6"], - subscriber: ["1", "2", "3", "4", "5", "6"], - created_by: ["1", "2", "3", "4", "5", "6"], - labels: ["1", "2", "3", "4", "5", "6"], - start_date: ["1", "2", "3", "4", "5", "6"], - target_date: ["1", "2", "3", "4", "5", "6"], +export const ViewFiltersRoot: FC = observer((props) => { + const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute } = props; + // hooks + const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); + // state + const [filterVisibility, setFilterVisibility] = useState[]>([]); + const handleFilterVisibility = (key: keyof TViewFilters) => { + setFilterVisibility((prevData = []) => { + if (prevData.includes(key)) return filter(prevData, (item) => item !== key); + return uniq(concat(prevData, [key])); + }); }; + const layout = viewDetailStore?.appliedFilters?.display_filters?.layout || "spreadsheet"; + + const filtersProperties = VIEW_DEFAULT_FILTER_PARAMETERS?.["all"]?.["spreadsheet"]?.filters || []; + + if (!layout || filtersProperties.length <= 0) return <>; return ( -
-
ViewFiltersRoot
+
+ {filtersProperties.map((filterKey) => ( +
+
+
+ {filterKey.replace("_", " ")} +
+
handleFilterVisibility(filterKey)} + > + {!filterVisibility.includes(filterKey) ? : } +
+
+ {!filterVisibility.includes(filterKey) && ( + + )} +
+ ))}
); -}; +}); diff --git a/web/components/view/helpers/filters.tsx b/web/components/view/helpers/filters.tsx new file mode 100644 index 000000000..190a4828b --- /dev/null +++ b/web/components/view/helpers/filters.tsx @@ -0,0 +1,49 @@ +// hooks +import { useProject, useProjectState, useMember } from "hooks/store"; +// types +import { TViewFilters } from "@plane/types"; + +type TFilterPropertyItemByFilterKeyAndId = { + key: keyof TViewFilters; + id: string; + icon: string; + title: string; +}; + +export const filterPropertyItemByFilterKeyAndId = ( + key: keyof TViewFilters, + id: string +): TFilterPropertyItemByFilterKeyAndId | undefined => { + if (!key || id) return undefined; + + switch (key) { + case "project": + return undefined; // store + case "module": + return undefined; // store + case "cycle": + return undefined; // store + case "priority": + return undefined; // constant + case "state": + return undefined; // store + case "state_group": + return undefined; // constant + case "assignees": + return undefined; // store -> workspace and project level + case "mentions": + return undefined; // store -> workspace and project level + case "subscriber": + return undefined; // store -> workspace and project level + case "created_by": + return undefined; // store -> workspace and project level + case "labels": + return undefined; // store -> workspace and project level + case "start_date": + return undefined; // constants + case "target_date": + return undefined; // constants + default: + return undefined; + } +}; diff --git a/web/components/view/index.ts b/web/components/view/index.ts index 2cc7351a2..73d2622ef 100644 --- a/web/components/view/index.ts +++ b/web/components/view/index.ts @@ -13,9 +13,14 @@ export * from "./views/create-edit-form"; export * from "./layout"; // view filters +export * from "./filters/dropdown"; export * from "./filters/root"; +export * from "./filters/filter-item-root"; +export * from "./filters/filter-item"; +export * from "./filters/filter-selection"; // view display filters +export * from "./display-filters/dropdown"; export * from "./display-filters/root"; // view display properties @@ -23,3 +28,7 @@ export * from "./display-properties/root"; // view applied filters export * from "./applied-filters/root"; + +// confirmation modals +export * from "./confirmation-modals/duplicate"; +export * from "./confirmation-modals/delete"; diff --git a/web/components/view/layout.tsx b/web/components/view/layout.tsx index e3f9c5f4d..84323b899 100644 --- a/web/components/view/layout.tsx +++ b/web/components/view/layout.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, Fragment } from "react"; import { observer } from "mobx-react-lite"; import { LucideIcon, List, Kanban, Calendar, Sheet, GanttChartSquare } from "lucide-react"; // hooks @@ -31,23 +31,24 @@ export const ViewLayoutRoot: FC = observer((props) => { const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); return ( -
+
{LAYOUTS_DATA.map((layout) => ( - -
+ +
viewOperations.setDisplayFilters({ layout: layout.key })} - > - -
-
+ onClick={() => viewOperations.setDisplayFilters({ layout: layout.key })} + > + +
+
+ ))}
); diff --git a/web/components/view/views/dropdown/dropdown-item.tsx b/web/components/view/views/dropdown/dropdown-item.tsx index 8dab34bf2..07b5c0733 100644 --- a/web/components/view/views/dropdown/dropdown-item.tsx +++ b/web/components/view/views/dropdown/dropdown-item.tsx @@ -14,12 +14,13 @@ type TViewDropdownItem = { projectId: string | undefined; viewId: string; viewType: TViewTypes; - currentViewId: string | undefined; + currentViewId: string; searchQuery: string; + baseRoute: string; }; export const ViewDropdownItem: FC = (props) => { - const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery } = props; + const { workspaceSlug, projectId, viewId, viewType, currentViewId, searchQuery, baseRoute } = props; // hooks const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType); @@ -43,7 +44,7 @@ export const ViewDropdownItem: FC = (props) => {
)} = (props) => { - const { workspaceSlug, projectId, viewId: currentViewId, viewType, viewOperations, children } = props; + const { workspaceSlug, projectId, viewId: currentViewId, viewType, viewOperations, children, baseRoute } = props; // hooks const viewStore = useView(workspaceSlug, projectId, viewType); // states @@ -105,6 +106,7 @@ export const ViewDropdown: FC = (props) => { viewType={viewType} currentViewId={currentViewId} searchQuery={query} + baseRoute={baseRoute} /> ))} diff --git a/web/components/view/views/root.tsx b/web/components/view/views/root.tsx index 40edcca58..abdc417e0 100644 --- a/web/components/view/views/root.tsx +++ b/web/components/view/views/root.tsx @@ -14,13 +14,14 @@ import { TViewTypes } from "@plane/types"; type TViewRoot = { workspaceSlug: string; projectId: string | undefined; - viewId: string | undefined; + viewId: string; viewType: TViewTypes; viewOperations: TViewOperations; + baseRoute: string; }; export const ViewRoot: FC = observer((props) => { - const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props; + const { workspaceSlug, projectId, viewId, viewType, viewOperations, baseRoute } = props; // hooks const viewStore = useView(workspaceSlug, projectId, viewType); // state @@ -30,14 +31,14 @@ export const ViewRoot: FC = observer((props) => { const handleViewTabsVisibility = () => { const tabContainer = document.getElementById("tab-container"); const tabItemViewMore = document.getElementById("tab-item-view-more"); - const itemWidth = 124; + const itemWidth = 128; if (!tabContainer || !tabItemViewMore) return; const containerWidth = tabContainer.clientWidth; - const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft; + const itemViewMoreLeftOffset = tabItemViewMore.offsetLeft + (tabItemViewMore.clientWidth + 10); const itemViewMoreRightOffset = containerWidth - itemViewMoreLeftOffset; - if (itemViewMoreLeftOffset + (tabItemViewMore.clientWidth + 10) > containerWidth) { + if (itemViewMoreLeftOffset > containerWidth) { const itemsToRender = Math.floor(containerWidth / itemWidth); setItemsToRenderViewCount(itemsToRender); } @@ -54,6 +55,13 @@ export const ViewRoot: FC = observer((props) => { return () => window.removeEventListener("resize", () => handleViewTabsVisibility()); }, [viewStore?.viewIds]); + const viewIds = viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length) || []; + + if (!viewIds.includes(viewId)) { + viewIds.pop(); + viewIds.push(viewId); + } + return (
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && ( @@ -62,7 +70,7 @@ export const ViewRoot: FC = observer((props) => { id="tab-container" className="relative flex items-center w-full overflow-hidden" > - {viewStore?.viewIds?.slice(0, itemsToRenderViewsCount || viewStore?.viewIds.length).map((_viewId) => ( + {viewIds.map((_viewId) => ( = observer((props) => { viewId={viewId} viewType={viewType} viewItemId={_viewId} + baseRoute={baseRoute} /> ))} @@ -82,6 +91,7 @@ export const ViewRoot: FC = observer((props) => { viewId={viewId} viewType={viewType} viewOperations={viewOperations} + baseRoute={baseRoute} >
diff --git a/web/components/view/views/view-item.tsx b/web/components/view/views/view-item.tsx index b26054336..dffc31063 100644 --- a/web/components/view/views/view-item.tsx +++ b/web/components/view/views/view-item.tsx @@ -2,7 +2,7 @@ import { FC, Fragment } from "react"; import Link from "next/link"; import { observer } from "mobx-react-lite"; // hooks -import { useView, useViewDetail } from "hooks/store"; +import { useViewDetail } from "hooks/store"; // ui import { PhotoFilterIcon, Tooltip } from "@plane/ui"; // types @@ -11,13 +11,14 @@ import { TViewTypes } from "@plane/types"; type TViewItem = { workspaceSlug: string; projectId: string | undefined; - viewId: string | undefined; + viewId: string; viewType: TViewTypes; viewItemId: string; + baseRoute: string; }; export const ViewItem: FC = observer((props) => { - const { workspaceSlug, projectId, viewId, viewType, viewItemId } = props; + const { workspaceSlug, projectId, viewId, viewType, viewItemId, baseRoute } = props; // hooks const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewItemId, viewType); @@ -26,7 +27,7 @@ export const ViewItem: FC = observer((props) => {
= observer((props) => {
-
+
); }); diff --git a/web/constants/dashboard.ts b/web/constants/dashboard.ts index 3f251ca78..327b371e2 100644 --- a/web/constants/dashboard.ts +++ b/web/constants/dashboard.ts @@ -270,7 +270,7 @@ export const SIDEBAR_MENU_ITEMS: { { key: "all-issues", label: "All Issues", - href: `/workspace-views/all-issues`, + href: `/views/public/all-issues`, access: EUserWorkspaceRoles.GUEST, highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views`), Icon: CheckCircle, diff --git a/web/constants/view/filters.ts b/web/constants/view/filters.ts index a11f2a514..78d34eb5f 100644 --- a/web/constants/view/filters.ts +++ b/web/constants/view/filters.ts @@ -1,8 +1,65 @@ // types -import { TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types"; +import { TStateGroups, TIssuePriorities, TViewFilters, TViewDisplayFilters, TViewLayouts } from "@plane/types"; +// filters constants +export const STATE_GROUP_PROPERTY: { + [key in TStateGroups]: { + label: string; + color: string; + }; +} = { + backlog: { + label: "Backlog", + color: "#d9d9d9", + }, + unstarted: { + label: "Unstarted", + color: "#3f76ff", + }, + started: { + label: "Started", + color: "#f59e0b", + }, + completed: { + label: "Completed", + color: "#16a34a", + }, + cancelled: { + label: "Canceled", + color: "#dc2626", + }, +}; + +export const PRIORITIES_PROPERTY: { + [key in TIssuePriorities]: { + label: string; + }; +} = { + urgent: { label: "Urgent" }, + high: { label: "High" }, + medium: { label: "Medium" }, + low: { label: "Low" }, + none: { label: "None" }, +}; + +export const DATE_PROPERTY: { + [key in string]: { + label: string; + }; +} = { + last_week: { label: "Last Week" }, + "2_weeks_from_now": { label: "2 weeks from now" }, + "1_month_from_now": { label: "1 month from now" }, + "2_months_from_now": { label: "2 months from now" }, + custom: { label: "Custom" }, +}; + +// display filter constants + +// layout, filter, display filter and display properties permissions for views type TViewLayoutFilterProperties = { filters: Partial[]; + readonlyFilters?: Partial[]; display_filters: Partial[]; extra_options: ("sub_issue" | "show_empty_groups")[]; display_properties: boolean; @@ -34,17 +91,32 @@ type TFilterPermissions = { }; }; -export const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = { +const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = { layouts: ["spreadsheet"], spreadsheet: { - filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"], + // filters: ["project", "priority", "state_group", "assignees", "created_by", "labels", "start_date", "target_date"], + filters: [ + "project", + "module", + "cycle", + "priority", + "state", + "state_group", + "assignees", + "mentions", + "subscriber", + "created_by", + "labels", + "start_date", + "target_date", + ], display_filters: ["type"], extra_options: [], display_properties: true, }, }; -export const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = { +const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = { layouts: ["list", "kanban"], list: { filters: ["priority", "state_group", "labels", "start_date", "target_date"], @@ -60,7 +132,7 @@ export const PROFILE_FILTER_PERMISSIONS: TFilterPermissions["profile"] = { }, }; -export const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = { +const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = { layouts: ["list", "kanban", "spreadsheet", "calendar", "gantt"], list: { filters: [ @@ -150,7 +222,7 @@ export const PROJECT_FILTER_PERMISSIONS: TFilterPermissions["project"] = { }, }; -export const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = { +const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = { layouts: ["list"], list: { filters: [ @@ -171,7 +243,7 @@ export const ARCHIVED_FILTER_PERMISSIONS: TFilterPermissions["archived"] = { }, }; -export const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = { +const DRAFT_FILTER_PERMISSIONS: TFilterPermissions["draft"] = { layouts: ["list", "kanban"], list: { filters: [ diff --git a/web/constants/view/root.ts b/web/constants/view/root.ts index 1024ca928..e6ca70b72 100644 --- a/web/constants/view/root.ts +++ b/web/constants/view/root.ts @@ -3,10 +3,10 @@ import { v4 as uuidV4 } from "uuid"; import { TViewTypes, TView } from "@plane/types"; export const VIEW_TYPES: Record = { - WORKSPACE_YOUR_VIEWS: "WORKSPACE_YOUR_VIEWS", - WORKSPACE_VIEWS: "WORKSPACE_VIEWS", - PROJECT_VIEWS: "PROJECT_VIEWS", - PROJECT_YOUR_VIEWS: "PROJECT_YOUR_VIEWS", + WORKSPACE_PRIVATE_VIEWS: "WORKSPACE_PRIVATE_VIEWS", + WORKSPACE_PUBLIC_VIEWS: "WORKSPACE_PUBLIC_VIEWS", + PROJECT_PRIVATE_VIEWS: "PROJECT_PRIVATE_VIEWS", + PROJECT_PUBLIC_VIEWS: "PROJECT_PUBLIC_VIEWS", }; export const viewLocalPayload: Partial = { diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index 5627a5aed..77024ba99 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,5 +1,5 @@ export * from "./use-application"; -export * from "./use-event-tracker" +export * from "./use-event-tracker"; export * from "./use-calendar-view"; export * from "./use-cycle"; export * from "./use-dashboard"; @@ -24,5 +24,6 @@ export * from "./use-inbox"; export * from "./use-inbox-issues"; // new store -export * from "./use-view"; -export * from "./use-view-detail"; +export * from "./views/use-view"; +export * from "./views/use-view-detail"; +export * from "./views/use-view-filters"; diff --git a/web/hooks/store/use-view-detail.tsx b/web/hooks/store/views/use-view-detail.tsx similarity index 52% rename from web/hooks/store/use-view-detail.tsx rename to web/hooks/store/views/use-view-detail.tsx index da4bc8b9d..c46f5a947 100644 --- a/web/hooks/store/use-view-detail.tsx +++ b/web/hooks/store/views/use-view-detail.tsx @@ -5,6 +5,8 @@ import { StoreContext } from "contexts/store-context"; import { TViewStore } from "store/view/view.store"; // types import { TViewTypes } from "@plane/types"; +// constants +import { VIEW_TYPES } from "constants/view"; export const useViewDetail = ( workspaceSlug: string, @@ -18,16 +20,16 @@ export const useViewDetail = ( if (!workspaceSlug || !viewId) return undefined; switch (viewType) { - case "WORKSPACE_YOUR_VIEWS": - return context.view.workspaceViewMeStore.viewById(viewId); - case "WORKSPACE_VIEWS": - return context.view.workspaceViewStore.viewById(viewId); - case "PROJECT_YOUR_VIEWS": - if (!projectId) throw new Error("useView hook must require projectId"); - return context.view.projectViewMeStore.viewById(viewId); - case "PROJECT_VIEWS": - if (!projectId) throw new Error("useView hook must require projectId"); - return context.view.projectViewStore.viewById(viewId); + case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS: + return context.view.workspacePrivateViewStore.viewById(viewId); + case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS: + return context.view.workspacePublicViewStore.viewById(viewId); + case VIEW_TYPES.PROJECT_PRIVATE_VIEWS: + if (!projectId) return undefined; + return context.view.projectPrivateViewStore.viewById(viewId); + case VIEW_TYPES.PROJECT_PUBLIC_VIEWS: + if (!projectId) return undefined; + return context.view.projectPublicViewStore.viewById(viewId); default: return undefined; } diff --git a/web/hooks/store/views/use-view-filters.tsx b/web/hooks/store/views/use-view-filters.tsx new file mode 100644 index 000000000..49b5642f6 --- /dev/null +++ b/web/hooks/store/views/use-view-filters.tsx @@ -0,0 +1,225 @@ +import { ReactNode } from "react"; +// hooks +import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store"; +// ui +import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; +// types +import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types"; +// constants +import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters"; +import { Briefcase, CalendarDays } from "lucide-react"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; + +type TFilterPropertyDetails = { + icon: ReactNode; + label: string; +}; + +export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => { + const { projectMap, getProjectById } = useProject(); + const { getProjectModuleIds, getModuleById } = useModule(); + const { getProjectCycleIds, getCycleById } = useCycle(); + const { getProjectStates, getStateById } = useProjectState(); + const { + getUserDetails, + workspace: { workspaceMemberIds }, + project: { getProjectMemberIds }, + } = useMember(); + const { workspaceLabels, getProjectLabels, getLabelById } = useLabel(); + + if (!workspaceSlug) return undefined; + + const filterIdsWithKey = (filterKey: keyof TViewFilters): string[] | undefined => { + if (!filterKey) return undefined; + + switch (filterKey) { + case "project": + return Object.keys(projectMap) || undefined; + case "module": + if (!projectId) return undefined; + return getProjectModuleIds(projectId) || undefined; + case "cycle": + if (!projectId) return undefined; + return getProjectCycleIds(projectId) || undefined; + case "priority": + return Object.keys(PRIORITIES_PROPERTY) || undefined; + case "state": + if (!projectId) return undefined; + return getProjectStates(projectId)?.map((state) => state.id) || undefined; + case "state_group": + return Object.keys(STATE_GROUP_PROPERTY) || undefined; + case "assignees": + if (projectId) return getProjectMemberIds(projectId) || undefined; + return workspaceMemberIds || undefined; + case "mentions": + if (projectId) return getProjectMemberIds(projectId) || undefined; + return workspaceMemberIds || undefined; + case "subscriber": + if (projectId) return getProjectMemberIds(projectId) || undefined; + return workspaceMemberIds || undefined; + case "created_by": + if (projectId) return getProjectMemberIds(projectId) || undefined; + return workspaceMemberIds || undefined; + case "labels": + if (projectId) return getProjectLabels(projectId)?.map((label) => label.id) || undefined; + return workspaceLabels?.map((label) => label.id) || undefined; + case "start_date": + return Object.keys(DATE_PROPERTY) || undefined; + case "target_date": + return Object.keys(DATE_PROPERTY) || undefined; + default: + return undefined; + } + }; + + const propertyDetails = (filterKey: keyof TViewFilters, propertyId: string): TFilterPropertyDetails | undefined => { + if (!filterKey || !propertyId) return undefined; + + switch (filterKey) { + case "project": + const projectPropertyDetail = getProjectById(propertyId); + if (!projectPropertyDetail) return undefined; + return { + icon: ( + <> + {projectPropertyDetail.emoji ? ( +
{renderEmoji(projectPropertyDetail.emoji)}
+ ) : projectPropertyDetail.icon_prop ? ( +
{renderEmoji(projectPropertyDetail.icon_prop)}
+ ) : ( + + )} + + ), + label: projectPropertyDetail.name, + }; + case "module": + const modulePropertyDetail = getModuleById(propertyId); + if (!modulePropertyDetail) return undefined; + return { + icon: , + label: modulePropertyDetail.name, + }; + case "cycle": + const cyclePropertyDetail = getCycleById(propertyId); + if (!cyclePropertyDetail) return undefined; + return { + icon: , + label: cyclePropertyDetail.name, + }; + case "priority": + const priorityPropertyDetail = PRIORITIES_PROPERTY?.[propertyId as TIssuePriorities]; + if (!priorityPropertyDetail) return undefined; + return { + icon: , + label: priorityPropertyDetail.label, + }; + case "state": + const statePropertyDetail = getStateById(propertyId); + if (!statePropertyDetail) return undefined; + return { + icon: , + label: statePropertyDetail.name, + }; + case "state_group": + const stateGroupPropertyDetail = STATE_GROUP_PROPERTY?.[propertyId as TStateGroups]; + if (!stateGroupPropertyDetail) return undefined; + return { + icon: , + label: stateGroupPropertyDetail.label, + }; + case "assignees": + const assigneePropertyDetail = getUserDetails(propertyId); + if (!assigneePropertyDetail) return undefined; + return { + icon: ( + + ), + label: assigneePropertyDetail.display_name, + }; + case "mentions": + const mentionPropertyDetail = getUserDetails(propertyId); + if (!mentionPropertyDetail) return undefined; + return { + icon: ( + + ), + label: mentionPropertyDetail.display_name, + }; + case "subscriber": + const subscribedPropertyDetail = getUserDetails(propertyId); + if (!subscribedPropertyDetail) return undefined; + return { + icon: ( + + ), + label: subscribedPropertyDetail.display_name, + }; + case "created_by": + const createdByPropertyDetail = getUserDetails(propertyId); + if (!createdByPropertyDetail) return undefined; + return { + icon: ( + + ), + label: createdByPropertyDetail.display_name, + }; + case "labels": + const labelPropertyDetail = getLabelById(propertyId); + if (!labelPropertyDetail) return undefined; + return { + icon: ( +
+ ), + label: labelPropertyDetail.name, + }; + case "start_date": + const startDatePropertyDetail = DATE_PROPERTY?.[propertyId]; + if (!startDatePropertyDetail) return undefined; + return { + icon: , + label: startDatePropertyDetail.label, + }; + case "target_date": + const targetDatePropertyDetail = DATE_PROPERTY?.[propertyId]; + if (!targetDatePropertyDetail) return undefined; + return { + icon: , + label: targetDatePropertyDetail.label, + }; + default: + return undefined; + } + }; + + return { + filterIdsWithKey, + propertyDetails, + }; +}; diff --git a/web/hooks/store/use-view.tsx b/web/hooks/store/views/use-view.tsx similarity index 54% rename from web/hooks/store/use-view.tsx rename to web/hooks/store/views/use-view.tsx index c6701a839..70339cdbc 100644 --- a/web/hooks/store/use-view.tsx +++ b/web/hooks/store/views/use-view.tsx @@ -3,8 +3,9 @@ import { useContext } from "react"; import { StoreContext } from "contexts/store-context"; // types import { ViewRootStore } from "store/view/view-root.store"; -// types import { TViewTypes } from "@plane/types"; +// constants +import { VIEW_TYPES } from "constants/view"; export const useView = ( workspaceSlug: string, @@ -17,16 +18,16 @@ export const useView = ( if (!workspaceSlug || !viewType) return undefined; switch (viewType) { - case "WORKSPACE_YOUR_VIEWS": - return context.view.workspaceViewMeStore; - case "WORKSPACE_VIEWS": - return context.view.workspaceViewStore; - case "PROJECT_YOUR_VIEWS": - if (!projectId) throw new Error("useView hook must require projectId"); - return context.view.projectViewMeStore; - case "PROJECT_VIEWS": - if (!projectId) throw new Error("useView hook must require projectId"); - return context.view.projectViewStore; + case VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS: + return context.view.workspacePrivateViewStore; + case VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS: + return context.view.workspacePublicViewStore; + case VIEW_TYPES.PROJECT_PRIVATE_VIEWS: + if (!projectId) return undefined; + return context.view.projectPrivateViewStore; + case VIEW_TYPES.PROJECT_PUBLIC_VIEWS: + if (!projectId) return undefined; + return context.view.projectPublicViewStore; default: return undefined; } diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 9f402cb57..32929d1ec 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -20,6 +20,7 @@ export const WorkspaceAuthWrapper: FC = observer((props) const { workspace: { fetchWorkspaceMembers }, } = useMember(); + const { fetchWorkspaceLabels } = useLabel(); // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -38,6 +39,11 @@ export const WorkspaceAuthWrapper: FC = observer((props) workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null ); + // fetch workspace labels + useSWR( + workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null, + workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null + ); // fetch workspace user projects role useSWR( workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, diff --git a/web/pages/[workspaceSlug]/views/private/[viewId].tsx b/web/pages/[workspaceSlug]/views/private/[viewId].tsx new file mode 100644 index 000000000..c9583bfbc --- /dev/null +++ b/web/pages/[workspaceSlug]/views/private/[viewId].tsx @@ -0,0 +1,36 @@ +import { ReactElement } from "react"; +import { useRouter } from "next/router"; +// layouts +import { AppLayout } from "layouts/app-layout"; +// components +import { AllIssuesViewRoot } from "components/view"; +// types +import { NextPageWithLayout } from "lib/types"; +// constants +import { VIEW_TYPES } from "constants/view"; + +const GlobalViewIssuesPage: NextPageWithLayout = () => { + const router = useRouter(); + const { workspaceSlug, viewId } = router.query; + + if (!workspaceSlug || !viewId) return <>; + return ( +
+
+ +
+
+ ); +}; + +GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; +}; + +export default GlobalViewIssuesPage; diff --git a/web/pages/[workspaceSlug]/views/public/[viewId].tsx b/web/pages/[workspaceSlug]/views/public/[viewId].tsx new file mode 100644 index 000000000..10f8600c9 --- /dev/null +++ b/web/pages/[workspaceSlug]/views/public/[viewId].tsx @@ -0,0 +1,36 @@ +import { ReactElement } from "react"; +import { useRouter } from "next/router"; +// layouts +import { AppLayout } from "layouts/app-layout"; +// components +import { AllIssuesViewRoot } from "components/view"; +// types +import { NextPageWithLayout } from "lib/types"; +// constants +import { VIEW_TYPES } from "constants/view"; + +const GlobalViewIssuesPage: NextPageWithLayout = () => { + const router = useRouter(); + const { workspaceSlug, viewId } = router.query; + + if (!workspaceSlug || !viewId) return <>; + return ( +
+
+ +
+
+ ); +}; + +GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) { + return }>{page}; +}; + +export default GlobalViewIssuesPage; diff --git a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx index cb05f77c7..542fe68fb 100644 --- a/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx +++ b/web/pages/[workspaceSlug]/workspace-views/[globalViewId].tsx @@ -3,10 +3,9 @@ import { useRouter } from "next/router"; // layouts import { AppLayout } from "layouts/app-layout"; // components -// import { GlobalViewsHeader } from "components/workspace"; -// import { AllIssueLayoutRoot } from "components/issues"; -// import { GlobalIssuesHeader } from "components/headers"; -import { AllIssuesViewRoot } from "components/view"; +import { GlobalViewsHeader } from "components/workspace"; +import { AllIssueLayoutRoot } from "components/issues"; +import { GlobalIssuesHeader } from "components/headers"; // types import { NextPageWithLayout } from "lib/types"; @@ -18,17 +17,15 @@ const GlobalViewIssuesPage: NextPageWithLayout = () => { return (
- - {/* - */} + +
); }; GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) { - // return }>{page}; - return }>{page}; + return }>{page}; }; export default GlobalViewIssuesPage; diff --git a/web/services/view/index.ts b/web/services/view/index.ts index 17dc9c210..0a7942e4f 100644 --- a/web/services/view/index.ts +++ b/web/services/view/index.ts @@ -1,14 +1,11 @@ // view services -export * from "./workspace_me.service"; -export * from "./workspace.service"; -export * from "./project_me.service"; -export * from "./project.service"; +export * from "./workspace_private.service"; +export * from "./workspace_public.service"; +export * from "./project_private.service"; +export * from "./project_public.service"; // user view services export * from "./user/workspace.service"; export * from "./user/project.service"; export * from "./user/module.service"; export * from "./user/cycle.service"; - -// views that are being stored in the local-store -// export * from "./user/local_storage.service"; diff --git a/web/services/view/project_me.service.ts b/web/services/view/project_private.service.ts similarity index 98% rename from web/services/view/project_me.service.ts rename to web/services/view/project_private.service.ts index d647a08d0..650640e05 100644 --- a/web/services/view/project_me.service.ts +++ b/web/services/view/project_private.service.ts @@ -5,7 +5,7 @@ import { TViewService } from "./types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; -export class ProjectViewMeService extends APIService implements TViewService { +export class ProjectPrivateViewService extends APIService implements TViewService { constructor() { super(API_BASE_URL); } diff --git a/web/services/view/project.service.ts b/web/services/view/project_public.service.ts similarity index 98% rename from web/services/view/project.service.ts rename to web/services/view/project_public.service.ts index ffa9d9688..0a0d2e3fa 100644 --- a/web/services/view/project.service.ts +++ b/web/services/view/project_public.service.ts @@ -5,7 +5,7 @@ import { TViewService } from "./types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; -export class ProjectViewService extends APIService implements TViewService { +export class ProjectPublicViewService extends APIService implements TViewService { constructor() { super(API_BASE_URL); } diff --git a/web/services/view/types.d.ts b/web/services/view/types.d.ts index e1842622b..78a5f9bf4 100644 --- a/web/services/view/types.d.ts +++ b/web/services/view/types.d.ts @@ -21,10 +21,10 @@ export type TViewService = { data: Partial, projectId?: string ) => Promise; - remove?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise | undefined; - lock?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; - unlock?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; - duplicate?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; - makeFavorite?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; - removeFavorite?: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + remove: (workspaceSlug: string, viewId: string, projectId?: string) => Promise | undefined; + lock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + unlock: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + duplicate: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + makeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; + removeFavorite: (workspaceSlug: string, viewId: string, projectId?: string) => Promise; }; diff --git a/web/services/view/workspace_me.service.ts b/web/services/view/workspace_private.service.ts similarity index 97% rename from web/services/view/workspace_me.service.ts rename to web/services/view/workspace_private.service.ts index bffcf821f..5b36e62b6 100644 --- a/web/services/view/workspace_me.service.ts +++ b/web/services/view/workspace_private.service.ts @@ -5,7 +5,7 @@ import { TViewService } from "./types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; -export class WorkspaceMeViewService extends APIService implements TViewService { +export class WorkspacePrivateViewService extends APIService implements TViewService { constructor() { super(API_BASE_URL); } diff --git a/web/services/view/workspace.service.ts b/web/services/view/workspace_public.service.ts similarity index 97% rename from web/services/view/workspace.service.ts rename to web/services/view/workspace_public.service.ts index 9802f4a01..0ca02e485 100644 --- a/web/services/view/workspace.service.ts +++ b/web/services/view/workspace_public.service.ts @@ -5,7 +5,7 @@ import { TViewService } from "./types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; -export class WorkspaceViewService extends APIService implements TViewService { +export class WorkspacePublicViewService extends APIService implements TViewService { constructor() { super(API_BASE_URL); } diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 422c6e582..7930a070d 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -24,8 +24,6 @@ import { GlobalViewRootStore } from "./view/root.store"; enableStaticRendering(typeof window === "undefined"); export class RootStore { - view: GlobalViewRootStore; - // old store structure app: IAppRootStore; eventTracker: IEventTrackerStore; user: IUserRootStore; @@ -44,9 +42,9 @@ export class RootStore { mention: IMentionStore; dashboard: IDashboardStore; projectPages: IProjectPageStore; + view: GlobalViewRootStore; constructor() { - this.view = new GlobalViewRootStore(this); // old store structure this.app = new AppRootStore(this); this.eventTracker = new EventTrackerStore(this); @@ -67,6 +65,7 @@ export class RootStore { this.mention = new MentionStore(this); this.projectPages = new ProjectPageStore(this); this.dashboard = new DashboardStore(this); + this.view = new GlobalViewRootStore(this); } resetOnSignout() { @@ -86,5 +85,6 @@ export class RootStore { this.mention = new MentionStore(this); this.projectPages = new ProjectPageStore(this); this.dashboard = new DashboardStore(this); + this.view = new GlobalViewRootStore(this); } } diff --git a/web/store/view/root.store.ts b/web/store/view/root.store.ts index 69a8f5dae..8dc3f81b7 100644 --- a/web/store/view/root.store.ts +++ b/web/store/view/root.store.ts @@ -1,41 +1,31 @@ +// stores +import { autorun, makeObservable, observable } from "mobx"; +import { ViewRootStore } from "./view-root.store"; // services import { - WorkspaceViewService, - WorkspaceMeViewService, - ProjectViewService, - ProjectViewMeService, + WorkspacePrivateViewService, + WorkspacePublicViewService, + ProjectPublicViewService, + ProjectPrivateViewService, WorkspaceFiltersService, ProjectFiltersService, - ModuleFiltersService, - CycleFiltersService, - // LocalStorageFiltersService, } from "services/view"; -// stores -import { ViewRootStore } from "./view-root.store"; -import { userViewRootStore } from "./user/view-root.store"; // types import { RootStore } from "store/root.store"; export class GlobalViewRootStore { - // views root - workspaceViewMeStore: ViewRootStore; - workspaceViewStore: ViewRootStore; - projectViewStore: ViewRootStore; - projectViewMeStore: ViewRootStore; - - // user views root - workspaceUserViewStore?: userViewRootStore; - projectUserViewStore?: userViewRootStore; - moduleUserViewStore?: userViewRootStore; - cycleUserViewStore?: userViewRootStore; + workspacePrivateViewStore: ViewRootStore; + workspacePublicViewStore: ViewRootStore; + projectPrivateViewStore: ViewRootStore; + projectPublicViewStore: ViewRootStore; constructor(private store: RootStore) { - const workspaceViewMeStoreDefaultViews: any[] = [ + const workspacePrivateDefaultViews: any[] = [ { id: "assigned", name: "Assigned", filters: { - assignees: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [], + assignees: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [], }, is_local_view: true, }, @@ -43,7 +33,7 @@ export class GlobalViewRootStore { id: "created", name: "Created", filters: { - created_by: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [], + created_by: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [], }, is_local_view: true, }, @@ -51,12 +41,13 @@ export class GlobalViewRootStore { id: "subscribed", name: "Subscribed", filters: { - subscriber: store.user?.currentUser?.id ? [store.user?.currentUser?.id] : [], + subscriber: store?.user?.currentUser?.id ? [store?.user?.currentUser?.id] : [], }, is_local_view: true, }, ]; - const workspaceViewStoreDefaultViews: any[] = [ + + const workspacePublicDefaultViews: any[] = [ { id: "all-issues", name: "All Issues", @@ -65,41 +56,29 @@ export class GlobalViewRootStore { }, ]; - this.workspaceViewMeStore = new ViewRootStore( + this.workspacePrivateViewStore = new ViewRootStore( this.store, - new WorkspaceMeViewService(), - workspaceViewMeStoreDefaultViews + workspacePrivateDefaultViews, + new WorkspacePrivateViewService(), + new WorkspaceFiltersService() ); - this.workspaceViewStore = new ViewRootStore(this.store, new WorkspaceViewService(), workspaceViewStoreDefaultViews); - this.projectViewStore = new ViewRootStore(this.store, new ProjectViewService()); - this.projectViewMeStore = new ViewRootStore(this.store, new ProjectViewMeService()); - - // user views root - this.workspaceUserViewStore = new userViewRootStore( - new WorkspaceFiltersService(), - store.app?.router?.workspaceSlug, + this.workspacePublicViewStore = new ViewRootStore( + this.store, + workspacePublicDefaultViews, + new WorkspacePublicViewService(), + new WorkspaceFiltersService() + ); + this.projectPrivateViewStore = new ViewRootStore( + this.store, undefined, - undefined + new ProjectPrivateViewService(), + new ProjectFiltersService() ); - this.projectUserViewStore = new userViewRootStore( - new ProjectFiltersService(), - store.app?.router?.workspaceSlug, - store.app?.router?.projectId, - undefined + this.projectPublicViewStore = new ViewRootStore( + this.store, + undefined, + new ProjectPublicViewService(), + new ProjectFiltersService() ); - this.moduleUserViewStore = new userViewRootStore( - new ModuleFiltersService(), - store.app?.router?.workspaceSlug, - store.app?.router?.projectId, - store.app?.router?.moduleId - ); - this.cycleUserViewStore = new userViewRootStore( - new CycleFiltersService(), - store.app?.router?.workspaceSlug, - store.app?.router?.projectId, - store.app?.router?.cycleId - ); - // this.archivedUserViewStore = new userViewRootStore( new LocalStorageFiltersService()); - // this.draftUserViewStore = new userViewRootStore( new LocalStorageFiltersService()); } } diff --git a/web/store/view/user/view-root.store.ts b/web/store/view/user/view-root.store.ts deleted file mode 100644 index d5322b016..000000000 --- a/web/store/view/user/view-root.store.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -// stores -import { UserViewStore } from "./view.store"; -// types -import { TUserViewService } from "services/view/types"; - -type TUserViewRootStore = { - // observables - viewMap: Record; - // computed - viewIds: string[]; - // helper actions - viewById: (viewId: string) => UserViewStore | undefined; - // actions - fetch: () => Promise; -}; - -export class userViewRootStore implements TUserViewRootStore { - // observables - viewMap: Record = {}; - - constructor( - private service: TUserViewService, - private workspaceSlug: string | undefined, - private projectId: string | undefined, - private featureId: string | undefined // moduleId/cycleId - ) { - makeObservable(this, { - // observables - viewMap: observable.ref, - // computed - viewIds: computed, - // actions - fetch: action, - }); - } - - // computed - get viewIds() { - return Object.keys(this.viewMap); - } - - // helper actions - viewById = (viewId: string) => this.viewMap?.[viewId] || undefined; - - // actions - fetch = async () => { - if (!this.workspaceSlug) return; - - const view = await this.service.fetch(this.workspaceSlug, this.projectId, this.featureId); - if (!view) return; - - // runInAction(() => { - // if (this.workspaceSlug && view.id) - // set( - // this.viewMap, - // [view.id], - // new UserViewStore(view, this.service, this.workspaceSlug, this.projectId, this.featureId) - // ); - // }); - }; -} diff --git a/web/store/view/user/view.store.ts b/web/store/view/user/view.store.ts deleted file mode 100644 index c3c6d2972..000000000 --- a/web/store/view/user/view.store.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { action, computed, makeObservable, observable, runInAction } from "mobx"; -import set from "lodash/set"; -// store -import { RootStore } from "store/root.store"; -// types -import { TViewService } from "services/view/types"; -import { - TView, - TViewFilters, - TViewDisplayFilters, - TViewDisplayProperties, - TViewFilterProps, - TViewAccess, -} from "@plane/types"; -// helpers -import { FiltersHelper } from "../helpers/filters_helpers"; - -type TLoader = "submitting" | "submit" | undefined; - -export type TUserViewStore = TView & { - // observables - loader: TLoader; - filtersToUpdate: Partial; - // computed - appliedFilters: TViewFilterProps | undefined; - appliedFiltersQueryParams: string | undefined; - // helper actions - setName: (name: string) => void; - setDescription: (description: string) => void; - setFilters: (filters: Partial) => void; - setDisplayFilters: (display_filters: Partial) => void; - setDisplayProperties: (display_properties: Partial) => void; - resetChanges: () => void; - saveChanges: () => Promise; - // actions - lockView: () => Promise; - unlockView: () => Promise; - makeFavorite: () => Promise; - removeFavorite: () => Promise; - update: (viewData: Partial) => Promise; -}; - -export class UserViewStore extends FiltersHelper implements TUserViewStore { - id: string | undefined; - workspace: string | undefined; - project: string | undefined; - name: string | undefined; - description: string | undefined; - query: string | undefined; - filters: TViewFilters; - display_filters: TViewDisplayFilters; - display_properties: TViewDisplayProperties; - access: TViewAccess | undefined; - owned_by: string | undefined; - sort_order: number | undefined; - is_locked: boolean = false; - is_pinned: boolean = false; - is_favorite: boolean = false; - created_by: string | undefined; - updated_by: string | undefined; - created_at: Date | undefined; - updated_at: Date | undefined; - is_local_view: boolean = false; - is_create: boolean = false; - is_editable: boolean = false; - loader: TLoader = undefined; - filtersToUpdate: Partial = { - name: "", - description: "", - filters: undefined, - display_filters: undefined, - display_properties: undefined, - }; - - constructor(private store: RootStore, _view: TView, private service: TViewService) { - super(); - this.id = _view.id; - this.workspace = _view.workspace; - this.project = _view.project; - this.name = _view.name; - this.description = _view.description; - this.query = _view.query; - this.filters = this.computedFilters(_view.filters); - this.display_filters = this.computedDisplayFilters(_view.display_filters); - this.display_properties = this.computedDisplayProperties(_view.display_properties); - this.access = _view.access; - this.owned_by = _view.owned_by; - this.sort_order = _view.sort_order; - this.is_locked = _view.is_locked; - this.is_pinned = _view.is_pinned; - this.is_favorite = _view.is_favorite; - this.created_by = _view.created_by; - this.updated_by = _view.updated_by; - this.created_at = _view.created_at; - this.updated_at = _view.updated_at; - - this.is_local_view = _view.is_local_view; - this.is_create = _view.is_create; - this.is_editable = _view.is_editable; - - makeObservable(this, { - // observables - id: observable.ref, - workspace: observable.ref, - project: observable.ref, - name: observable.ref, - description: observable.ref, - query: observable.ref, - filters: observable, - display_filters: observable, - display_properties: observable, - access: observable.ref, - owned_by: observable.ref, - sort_order: observable.ref, - is_locked: observable.ref, - is_pinned: observable.ref, - is_favorite: observable.ref, - created_by: observable.ref, - updated_by: observable.ref, - created_at: observable.ref, - updated_at: observable.ref, - is_local_view: observable.ref, - is_create: observable.ref, - is_editable: observable.ref, - loader: observable.ref, - filtersToUpdate: observable, - // computed - appliedFilters: computed, - appliedFiltersQueryParams: computed, - // helper actions - setName: action, - setFilters: action, - setDisplayFilters: action, - setDisplayProperties: action, - resetChanges: action, - saveChanges: action, - // actions - update: action, - lockView: action, - unlockView: action, - }); - } - - // computed - get appliedFilters() { - return { - filters: this.computedFilters(this.filters, this.filtersToUpdate.filters), - display_filters: this.computedDisplayFilters(this.display_filters, this.filtersToUpdate.display_filters), - display_properties: this.computedDisplayProperties( - this.display_properties, - this.filtersToUpdate.display_properties - ), - }; - } - - get appliedFiltersQueryParams() { - const filters = this.appliedFilters; - if (!filters) return undefined; - return this.computeAppliedFiltersQueryParameters(filters, [])?.query || undefined; - } - - // helper actions - setName = (name: string) => { - runInAction(() => { - this.filtersToUpdate.name = name; - }); - }; - - setDescription = (description: string) => { - runInAction(() => { - this.filtersToUpdate.description = description; - }); - }; - - setFilters = (filters: Partial) => { - runInAction(() => { - this.loader = "submit"; - Object.keys(filters).forEach((key) => { - const _key = key as keyof TViewFilters; - set(this.filtersToUpdate, ["filters", _key], filters[_key]); - }); - }); - }; - - setDisplayFilters = async (display_filters: Partial) => { - const appliedFilters = this.appliedFilters; - - const layout = appliedFilters?.display_filters?.layout; - const sub_group_by = appliedFilters?.display_filters?.sub_group_by; - const group_by = appliedFilters?.display_filters?.group_by; - const sub_issue = appliedFilters?.display_filters?.sub_issue; - - if (group_by === undefined && display_filters.sub_group_by) display_filters.sub_group_by = undefined; - if (layout === "kanban") { - if (sub_group_by === group_by) display_filters.group_by = undefined; - if (group_by === null) display_filters.group_by = "state"; - } - if (layout === "spreadsheet" && sub_issue === true) display_filters.sub_issue = false; - - runInAction(() => { - Object.keys(display_filters).forEach((key) => { - const _key = key as keyof TViewDisplayFilters; - set(this.filtersToUpdate, ["display_filters", _key], display_filters[_key]); - }); - }); - }; - - setDisplayProperties = async (display_properties: Partial) => { - runInAction(() => { - Object.keys(display_properties).forEach((key) => { - const _key = key as keyof TViewDisplayProperties; - set(this.filtersToUpdate, ["display_properties", _key], display_properties[_key]); - }); - }); - }; - - resetChanges = () => { - runInAction(() => { - this.loader = undefined; - this.filtersToUpdate = { - name: this.name, - description: this.description, - filters: this.filters, - display_filters: this.display_filters, - display_properties: this.display_properties, - }; - }); - }; - - saveChanges = async () => { - try { - this.loader = "submitting"; - if (this.filtersToUpdate) await this.update(this.filtersToUpdate); - this.loader = undefined; - } catch { - this.loader = undefined; - Object.keys(this.filtersToUpdate).forEach((key) => { - const _key = key as keyof TView; - set(this, _key, this.filtersToUpdate[_key]); - }); - } - }; - - // actions - lockView = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug || !this.id || !this.service.lock) return; - - const view = await this.service.lock(workspaceSlug, this.id, projectId); - if (!view) return; - - runInAction(() => { - this.is_locked = view.is_locked; - }); - } catch { - this.is_locked = this.is_locked; - } - }; - - unlockView = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug || !this.id || !this.service.unlock) return; - - const view = await this.service.unlock(workspaceSlug, this.id, projectId); - if (!view) return; - - runInAction(() => { - this.is_locked = view.is_locked; - }); - } catch { - this.is_locked = this.is_locked; - } - }; - - makeFavorite = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug || !this.id || !this.service.makeFavorite) return; - - const view = await this.service.makeFavorite(workspaceSlug, this.id, projectId); - if (!view) return; - - runInAction(() => { - this.is_favorite = view.is_locked; - }); - } catch { - this.is_favorite = this.is_favorite; - } - }; - - removeFavorite = async () => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug || !this.id || !this.service.removeFavorite) return; - - const view = await this.service.removeFavorite(workspaceSlug, this.id, projectId); - if (!view) return; - - runInAction(() => { - this.is_favorite = view.is_locked; - }); - } catch { - this.is_favorite = this.is_favorite; - } - }; - - update = async (viewData: Partial) => { - try { - const { workspaceSlug, projectId } = this.store.app.router; - if (!workspaceSlug || !this.id) return; - - const view = await this.service.update(workspaceSlug, this.id, viewData, projectId); - if (!view) return; - - runInAction(() => { - Object.keys(viewData).forEach((key) => { - const _key = key as keyof TView; - set(this, _key, viewData[_key]); - }); - }); - } catch { - this.resetChanges(); - } - }; -} diff --git a/web/store/view/view-root.store.ts b/web/store/view/view-root.store.ts index 4a607d61f..ba6d960fa 100644 --- a/web/store/view/view-root.store.ts +++ b/web/store/view/view-root.store.ts @@ -1,4 +1,5 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; import set from "lodash/set"; import sortBy from "lodash/sortBy"; import reverse from "lodash/reverse"; @@ -6,7 +7,7 @@ import reverse from "lodash/reverse"; import { RootStore } from "store/root.store"; import { ViewStore } from "./view.store"; // types -import { TViewService } from "services/view/types"; +import { TUserViewService, TViewService } from "services/view/types"; import { TView } from "@plane/types"; export type TLoader = "init-loader" | "mutation-loader" | "submitting" | undefined; @@ -17,11 +18,11 @@ type TViewRootStore = { viewMap: Record; // computed viewIds: string[]; - // helper actions viewById: (viewId: string) => ViewStore | undefined; // actions localViewCreate: (view: TView) => Promise; fetch: (_loader?: TLoader) => Promise; + fetchById: (viewId: string) => Promise; create: (view: Partial) => Promise; remove: (viewId: string) => Promise; duplicate: (viewId: string) => Promise; @@ -32,7 +33,12 @@ export class ViewRootStore implements TViewRootStore { loader: TLoader = "init-loader"; viewMap: Record = {}; - constructor(private store: RootStore, private service: TViewService, private defaultViews: TView[] = []) { + constructor( + private store: RootStore, + private defaultViews: TView[] = [], + private service: TViewService, + private userService: TUserViewService + ) { makeObservable(this, { // observables loader: observable.ref, @@ -42,6 +48,7 @@ export class ViewRootStore implements TViewRootStore { // actions localViewCreate: action, fetch: action, + fetchById: action, create: action, remove: action, duplicate: action, @@ -54,19 +61,16 @@ export class ViewRootStore implements TViewRootStore { const localViews = views.filter((view) => view.is_local_view); let apiViews = views.filter((view) => !view.is_local_view && !view.is_create); apiViews = reverse(sortBy(apiViews, "sort_order")); - const _viewIds = [...localViews.map((view) => view.id), ...apiViews.map((view) => view.id)]; - return _viewIds as string[]; } - // helper actions - viewById = (viewId: string) => this.viewMap?.[viewId] || undefined; + viewById = computedFn((viewId: string) => this.viewMap?.[viewId] || undefined); // actions localViewCreate = async (view: TView) => { runInAction(() => { - if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service)); + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); }); }; @@ -75,26 +79,57 @@ export class ViewRootStore implements TViewRootStore { const { workspaceSlug, projectId } = this.store.app.router; if (!workspaceSlug) return; + this.loader = _loader; + if (this.defaultViews && this.defaultViews.length > 0) runInAction(() => { this.defaultViews?.forEach((view) => { - if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service)); + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); }); }); - this.loader = _loader; const views = await this.service.fetch(workspaceSlug, projectId); if (!views) return; runInAction(() => { views.forEach((view) => { - if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service)); + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); }); this.loader = undefined; }); } catch {} }; + fetchById = async (viewId: string) => { + try { + const { workspaceSlug, projectId } = this.store.app.router; + if (!workspaceSlug || !viewId) return; + + const userView = await this.userService.fetch(workspaceSlug, projectId); + if (!userView) return; + + if (["all-issues", "assigned", "created", "subscribed"].includes(viewId)) { + const view = { ...this.viewById(viewId) }; + if (!view) return; + + runInAction(() => { + view.display_filters = userView.display_filters; + view.display_properties = userView.display_properties; + }); + } else { + const view = await this.service.fetchById(workspaceSlug, viewId, projectId); + if (!view) return; + + view?.display_filters && (view.display_filters = userView.display_filters); + view?.display_properties && (view.display_properties = userView.display_properties); + + runInAction(() => { + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); + }); + } + } catch {} + }; + create = async (data: Partial) => { try { const { workspaceSlug, projectId } = this.store.app.router; @@ -104,7 +139,7 @@ export class ViewRootStore implements TViewRootStore { if (!view) return; runInAction(() => { - if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service)); + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); }); if (data.id) this.remove(data.id); @@ -134,7 +169,7 @@ export class ViewRootStore implements TViewRootStore { if (!view) return; runInAction(() => { - if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service)); + if (view.id) set(this.viewMap, [view.id], new ViewStore(this.store, view, this.service, this.userService)); }); } catch {} }; diff --git a/web/store/view/view.store.ts b/web/store/view/view.store.ts index 7da271a53..6f44f2958 100644 --- a/web/store/view/view.store.ts +++ b/web/store/view/view.store.ts @@ -3,7 +3,7 @@ import set from "lodash/set"; // store import { RootStore } from "store/root.store"; // types -import { TViewService } from "services/view/types"; +import { TUserViewService, TViewService } from "services/view/types"; import { TView, TViewFilters, @@ -72,7 +72,12 @@ export class ViewStore extends FiltersHelper implements TViewStore { display_properties: undefined, }; - constructor(private store: RootStore, _view: TView, private service: TViewService) { + constructor( + private store: RootStore, + _view: TView, + private service: TViewService, + private userService: TUserViewService + ) { super(); this.id = _view.id; this.workspace = _view.workspace;