[WEB-1255] chore: public and unlocked views (#4932)
* chore private and public views required changes * fix slight alignment of view icons * add feedback for update View button * fix object reference sharing by using cloneDeep to replicate objects * addressing review comments
This commit is contained in:
parent
711494b72e
commit
635efeab7b
27 changed files with 524 additions and 240 deletions
5
packages/types/src/views.d.ts
vendored
5
packages/types/src/views.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
import { TLogoProps } from "./common";
|
import { TLogoProps } from "./common";
|
||||||
import {
|
import {
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
|
|
@ -7,7 +8,7 @@ import {
|
||||||
|
|
||||||
export interface IProjectView {
|
export interface IProjectView {
|
||||||
id: string;
|
id: string;
|
||||||
access: string;
|
access: EViewAccess;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
|
|
@ -23,4 +24,6 @@ export interface IProjectView {
|
||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
logo_props: TLogoProps | undefined;
|
logo_props: TLogoProps | undefined;
|
||||||
|
is_locked: boolean;
|
||||||
|
owned_by: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
packages/types/src/workspace-views.d.ts
vendored
5
packages/types/src/workspace-views.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
import {
|
import {
|
||||||
IWorkspaceViewProps,
|
IWorkspaceViewProps,
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
|
|
@ -7,7 +8,7 @@ import {
|
||||||
|
|
||||||
export interface IWorkspaceView {
|
export interface IWorkspaceView {
|
||||||
id: string;
|
id: string;
|
||||||
access: string;
|
access: EViewAccess;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
|
|
@ -22,6 +23,8 @@ export interface IWorkspaceView {
|
||||||
query_data: IWorkspaceViewProps;
|
query_data: IWorkspaceViewProps;
|
||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
|
is_locked: boolean;
|
||||||
|
owned_by: string;
|
||||||
workspace_detail?: {
|
workspace_detail?: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export const DropdownButton: React.FC<IMultiSelectDropdownButton | ISingleSelect
|
||||||
)}
|
)}
|
||||||
onClick={handleOnClick}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{buttonContent ? <>{buttonContent(isOpen)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
|
{buttonContent ? <>{buttonContent(isOpen, value)}</> : <span className={cn("", buttonClassName)}>{value}</span>}
|
||||||
</button>
|
</button>
|
||||||
</Combobox.Button>
|
</Combobox.Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||||
disableSearch,
|
disableSearch,
|
||||||
keyExtractor,
|
keyExtractor,
|
||||||
options,
|
options,
|
||||||
|
handleClose,
|
||||||
value,
|
value,
|
||||||
renderItem,
|
renderItem,
|
||||||
loader,
|
loader,
|
||||||
|
|
@ -46,7 +47,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||||
options?.map((option) => (
|
options?.map((option) => (
|
||||||
<Combobox.Option
|
<Combobox.Option
|
||||||
key={keyExtractor(option)}
|
key={keyExtractor(option)}
|
||||||
value={option.data[option.value]}
|
value={keyExtractor(option)}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
cn(
|
cn(
|
||||||
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
"flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5",
|
||||||
|
|
@ -58,14 +59,15 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
||||||
option.className && option.className({ active, selected })
|
option.className && option.className({ active, selected })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
{renderItem ? (
|
{renderItem ? (
|
||||||
<>{renderItem({ value: option.data[option.value], selected })}</>
|
<>{renderItem({ value: keyExtractor(option), selected })}</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="flex-grow truncate">{value}</span>
|
<span className="flex-grow truncate">{option.value}</span>
|
||||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
10
packages/ui/src/dropdown/dropdown.d.ts
vendored
10
packages/ui/src/dropdown/dropdown.d.ts
vendored
|
|
@ -10,7 +10,7 @@ export interface IDropdown {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
// button props
|
// button props
|
||||||
buttonContent?: (isOpen: boolean) => React.ReactNode;
|
buttonContent?: (isOpen: boolean, value: string | string[] | undefined) => React.ReactNode;
|
||||||
buttonContainerClassName?: string;
|
buttonContainerClassName?: string;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
|
|
||||||
|
|
@ -24,8 +24,8 @@ export interface IDropdown {
|
||||||
// options props
|
// options props
|
||||||
keyExtractor: (option: TDropdownOption) => string;
|
keyExtractor: (option: TDropdownOption) => string;
|
||||||
optionsContainerClassName?: string;
|
optionsContainerClassName?: string;
|
||||||
queryArray: string[];
|
queryArray?: string[];
|
||||||
sortByKey: string;
|
sortByKey?: string;
|
||||||
firstItem?: (optionValue: string) => boolean;
|
firstItem?: (optionValue: string) => boolean;
|
||||||
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
|
renderItem?: ({ value, selected }: { value: string; selected: boolean }) => React.ReactNode;
|
||||||
loader?: React.ReactNode;
|
loader?: React.ReactNode;
|
||||||
|
|
@ -52,7 +52,7 @@ export interface ISingleSelectDropdown extends IDropdown {
|
||||||
|
|
||||||
export interface IDropdownButton {
|
export interface IDropdownButton {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
buttonContent?: (isOpen: boolean) => React.ReactNode;
|
buttonContent?: (isOpen: boolean, value: string | string[] | undefined) => React.ReactNode;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
buttonContainerClassName?: string;
|
buttonContainerClassName?: string;
|
||||||
handleOnClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
handleOnClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
|
|
@ -79,6 +79,8 @@ export interface IDropdownOptions {
|
||||||
inputContainerClassName?: string;
|
inputContainerClassName?: string;
|
||||||
disableSearch?: boolean;
|
disableSearch?: boolean;
|
||||||
|
|
||||||
|
handleClose?: () => void;
|
||||||
|
|
||||||
keyExtractor: (option: TDropdownOption) => string;
|
keyExtractor: (option: TDropdownOption) => string;
|
||||||
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
|
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
|
||||||
options: TDropdownOption[] | undefined;
|
options: TDropdownOption[] | undefined;
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,12 @@ export const MultiSelectDropdown: FC<IMultiSelectDropdown> = (props) => {
|
||||||
const sortedOptions = useMemo(() => {
|
const sortedOptions = useMemo(() => {
|
||||||
if (!options) return undefined;
|
if (!options) return undefined;
|
||||||
|
|
||||||
const filteredOptions = (options || []).filter((options) => {
|
const filteredOptions = queryArray
|
||||||
const queryString = queryArray.map((query) => options.data[query]).join(" ");
|
? (options || []).filter((options) => {
|
||||||
return queryString.toLowerCase().includes(query.toLowerCase());
|
const queryString = queryArray.map((query) => options.data[query]).join(" ");
|
||||||
});
|
return queryString.toLowerCase().includes(query.toLowerCase());
|
||||||
|
})
|
||||||
|
: options;
|
||||||
|
|
||||||
if (disableSorting) return filteredOptions;
|
if (disableSorting) return filteredOptions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,14 @@ export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
|
||||||
const sortedOptions = useMemo(() => {
|
const sortedOptions = useMemo(() => {
|
||||||
if (!options) return undefined;
|
if (!options) return undefined;
|
||||||
|
|
||||||
const filteredOptions = (options || []).filter((options) => {
|
const filteredOptions = queryArray
|
||||||
const queryString = queryArray.map((query) => options.data[query]).join(" ");
|
? (options || []).filter((options) => {
|
||||||
return queryString.toLowerCase().includes(query.toLowerCase());
|
const queryString = queryArray.map((query) => options.data[query]).join(" ");
|
||||||
});
|
return queryString.toLowerCase().includes(query.toLowerCase());
|
||||||
|
})
|
||||||
|
: options;
|
||||||
|
|
||||||
if (disableSorting) return filteredOptions;
|
if (disableSorting || !sortByKey) return filteredOptions;
|
||||||
|
|
||||||
return sortBy(filteredOptions, [
|
return sortBy(filteredOptions, [
|
||||||
(option) => firstItem && firstItem(option.data[option.value]),
|
(option) => firstItem && firstItem(option.data[option.value]),
|
||||||
|
|
@ -136,7 +138,7 @@ export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
|
||||||
<Combobox.Options className="fixed z-10" static>
|
<Combobox.Options className="fixed z-10" static>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none",
|
"my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2 text-xs shadow-custom-shadow-rg focus:outline-none",
|
||||||
optionsContainerClassName
|
optionsContainerClassName
|
||||||
)}
|
)}
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
|
|
@ -157,6 +159,7 @@ export const Dropdown: FC<ISingleSelectDropdown> = (props) => {
|
||||||
value={value}
|
value={value}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
loader={loader}
|
loader={loader}
|
||||||
|
handleClose={handleClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import { useCallback } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { Earth, Lock } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon } from "@plane/ui";
|
import { Breadcrumbs, Button, CustomMenu, PhotoFilterIcon, Tooltip } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, Logo } from "@/components/common";
|
import { BreadcrumbLink, Logo } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
|
|
@ -19,6 +20,7 @@ import {
|
||||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
ISSUE_DISPLAY_FILTERS_BY_LAYOUT,
|
||||||
} from "@/constants/issue";
|
} from "@/constants/issue";
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
// helpers
|
// helpers
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
|
|
@ -205,54 +207,64 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<div className="cursor-default text-custom-text-300">
|
||||||
|
<Tooltip tooltipContent={viewDetails?.access === EViewAccess.PUBLIC ? "Public" : "Private"}>
|
||||||
|
{viewDetails?.access === EViewAccess.PUBLIC ? <Earth className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutSelection
|
{!viewDetails?.is_locked && (
|
||||||
layouts={[
|
<>
|
||||||
EIssueLayoutTypes.LIST,
|
<LayoutSelection
|
||||||
EIssueLayoutTypes.KANBAN,
|
layouts={[
|
||||||
EIssueLayoutTypes.CALENDAR,
|
EIssueLayoutTypes.LIST,
|
||||||
EIssueLayoutTypes.SPREADSHEET,
|
EIssueLayoutTypes.KANBAN,
|
||||||
EIssueLayoutTypes.GANTT,
|
EIssueLayoutTypes.CALENDAR,
|
||||||
]}
|
EIssueLayoutTypes.SPREADSHEET,
|
||||||
onChange={(layout) => handleLayoutChange(layout)}
|
EIssueLayoutTypes.GANTT,
|
||||||
selectedLayout={activeLayout}
|
]}
|
||||||
/>
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
|
selectedLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
|
||||||
<FiltersDropdown
|
<FiltersDropdown
|
||||||
title="Filters"
|
title="Filters"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
disabled={!canUserCreateIssue}
|
disabled={!canUserCreateIssue}
|
||||||
isFiltersApplied={isFiltersApplied}
|
isFiltersApplied={isFiltersApplied}
|
||||||
>
|
>
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
filters={issueFilters?.filters ?? {}}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
labels={projectLabels}
|
labels={projectLabels}
|
||||||
memberIds={projectMemberIds ?? undefined}
|
memberIds={projectMemberIds ?? undefined}
|
||||||
states={projectStates}
|
states={projectStates}
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
<FiltersDropdown title="Display" placement="bottom-end">
|
<FiltersDropdown title="Display" placement="bottom-end">
|
||||||
<DisplayFiltersSelection
|
<DisplayFiltersSelection
|
||||||
layoutDisplayFiltersOptions={
|
layoutDisplayFiltersOptions={
|
||||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||||
}
|
}
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||||
/>
|
/>
|
||||||
</FiltersDropdown>
|
</FiltersDropdown>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{canUserCreateIssue && (
|
{canUserCreateIssue && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
||||||
|
|
@ -33,15 +33,17 @@ const ProjectViewIssuesPage = observer(() => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
<EmptyState
|
return (
|
||||||
image={emptyView}
|
<EmptyState
|
||||||
title="View does not exist"
|
image={emptyView}
|
||||||
description="The view you are looking for does not exist or has been deleted."
|
title="View does not exist"
|
||||||
primaryButton={{
|
description="The view you are looking for does not exist or you don't have permission to view it."
|
||||||
text: "View other views",
|
primaryButton={{
|
||||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
|
text: "View other views",
|
||||||
}}
|
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
|
||||||
/>;
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
// helpers
|
// helpers
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
|
import { useLabel, useMember, useUser, useIssues, useGlobalView } from "@/hooks/store";
|
||||||
|
|
||||||
export const GlobalIssuesHeader = observer(() => {
|
export const GlobalIssuesHeader = observer(() => {
|
||||||
// states
|
// states
|
||||||
|
|
@ -28,6 +28,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
const {
|
const {
|
||||||
issuesFilter: { filters, updateFilters },
|
issuesFilter: { filters, updateFilters },
|
||||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||||
|
const { getViewDetailsById } = useGlobalView();
|
||||||
const {
|
const {
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
@ -38,6 +39,8 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
|
|
||||||
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
||||||
|
|
||||||
|
const viewDetails = getViewDetailsById(globalViewId.toString());
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
const handleFiltersUpdate = useCallback(
|
||||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
if (!workspaceSlug || !globalViewId) return;
|
if (!workspaceSlug || !globalViewId) return;
|
||||||
|
|
@ -96,6 +99,7 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0;
|
||||||
|
const isLocked = viewDetails?.is_locked;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -112,28 +116,30 @@ export const GlobalIssuesHeader = observer(() => {
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<>
|
{!isLocked && (
|
||||||
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
<>
|
||||||
<FilterSelection
|
<FiltersDropdown title="Filters" placement="bottom-end" isFiltersApplied={isFiltersApplied}>
|
||||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
<FilterSelection
|
||||||
filters={issueFilters?.filters ?? {}}
|
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||||
handleFiltersUpdate={handleFiltersUpdate}
|
filters={issueFilters?.filters ?? {}}
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
labels={workspaceLabels ?? undefined}
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
memberIds={workspaceMemberIds ?? undefined}
|
labels={workspaceLabels ?? undefined}
|
||||||
/>
|
memberIds={workspaceMemberIds ?? undefined}
|
||||||
</FiltersDropdown>
|
/>
|
||||||
<FiltersDropdown title="Display" placement="bottom-end">
|
</FiltersDropdown>
|
||||||
<DisplayFiltersSelection
|
<FiltersDropdown title="Display" placement="bottom-end">
|
||||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
<DisplayFiltersSelection
|
||||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
|
||||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||||
/>
|
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||||
</FiltersDropdown>
|
/>
|
||||||
</>
|
</FiltersDropdown>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isAuthorizedUser && (
|
{isAuthorizedUser && (
|
||||||
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
<Button variant="primary" size="sm" onClick={() => setCreateViewModal(true)}>
|
||||||
Add View
|
Add View
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const ListItem: FC<IListItemProps> = (props) => {
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
{actionableItems && (
|
{actionableItems && (
|
||||||
<div className="absolute right-5 bottom-4 flex items-center gap-1.5">
|
<div className="absolute right-5 bottom-4 flex items-center gap-1.5">
|
||||||
<div className="relative flex items-center gap-4 sm:w-auto sm:flex-shrink-0 sm:justify-end">
|
<div className="relative flex items-center gap-4 sm:w-auto sm:flex-shrink-0 sm:justify-end items-center">
|
||||||
{actionableItems}
|
{actionableItems}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
66
web/core/components/dropdowns/layout.tsx
Normal file
66
web/core/components/dropdowns/layout.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
// plane packages
|
||||||
|
import { cn } from "@plane/editor";
|
||||||
|
import { Dropdown } from "@plane/ui";
|
||||||
|
// constants
|
||||||
|
import { EIssueLayoutTypes, ISSUE_LAYOUT_MAP } from "@/constants/issue";
|
||||||
|
|
||||||
|
type TLayoutDropDown = {
|
||||||
|
onChange: (value: EIssueLayoutTypes) => void;
|
||||||
|
value: EIssueLayoutTypes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LayoutDropDown = observer((props: TLayoutDropDown) => {
|
||||||
|
const { onChange, value = EIssueLayoutTypes.LIST } = props;
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(ISSUE_LAYOUT_MAP).map((issueLayout) => ({
|
||||||
|
data: issueLayout.key,
|
||||||
|
value: issueLayout.key,
|
||||||
|
})),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonContent = useCallback((isOpen: boolean, buttonValue: string | string[] | undefined) => {
|
||||||
|
const dropdownValue = ISSUE_LAYOUT_MAP[buttonValue as EIssueLayoutTypes];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center text-custom-text-200">
|
||||||
|
<dropdownValue.icon strokeWidth={2} className={`size-3.5 text-custom-text-200`} />
|
||||||
|
<span className="font-medium text-xs">{dropdownValue.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const itemContent = useCallback((props: { value: string; selected: boolean }) => {
|
||||||
|
const dropdownValue = ISSUE_LAYOUT_MAP[props.value as EIssueLayoutTypes];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex gap-2 items-center text-custom-text-200 w-full justify-between")}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<dropdownValue.icon strokeWidth={2} className={`size-3 text-custom-text-200`} />
|
||||||
|
<span className="font-medium text-xs">{dropdownValue.label}</span>
|
||||||
|
</div>
|
||||||
|
{props.selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const keyExtractor = useCallback((option: any) => option.value, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
onChange={onChange as (value: string) => void}
|
||||||
|
value={value?.toString()}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
options={options}
|
||||||
|
buttonContainerClassName="bg-custom-background-100 border border-custom-border-200 hover:bg-custom-background-90 focus:text-custom-text-300 focus:bg-custom-background-90 px-2 py-1.5 rounded flex items-center gap-1.5 whitespace-nowrap transition-all justify-center relative"
|
||||||
|
buttonContent={buttonContent}
|
||||||
|
renderItem={itemContent}
|
||||||
|
disableSearch
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -38,5 +38,5 @@ export const ButtonAvatars: React.FC<AvatarProps> = observer((props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Icon ? <Icon className="h-3 w-3 flex-shrink-0" /> : <Users className="h-3 w-3 flex-shrink-0" />;
|
return Icon ? <Icon className="h-3 w-3 flex-shrink-0" /> : <Users className="h-3 w-3 mx-[4px] flex-shrink-0" />;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
18
web/core/components/icons/locked-component.tsx
Normal file
18
web/core/components/icons/locked-component.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Lock } from "lucide-react";
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
|
|
||||||
|
export const LockedComponent = (props: { toolTipContent?: string }) => {
|
||||||
|
const { toolTipContent } = props;
|
||||||
|
const lockedComponent = (
|
||||||
|
<div className="flex h-7 items-center gap-2 rounded-full bg-custom-background-80 px-3 py-0.5 text-xs font-medium text-custom-text-300">
|
||||||
|
<Lock className="h-3 w-3" />
|
||||||
|
<span>Locked</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{toolTipContent ? <Tooltip tooltipContent={toolTipContent}>{lockedComponent}</Tooltip> : <>{lockedComponent}</>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -28,13 +28,22 @@ type Props = {
|
||||||
labels?: IIssueLabel[] | undefined;
|
labels?: IIssueLabel[] | undefined;
|
||||||
states?: IState[] | undefined;
|
states?: IState[] | undefined;
|
||||||
alwaysAllowEditing?: boolean;
|
alwaysAllowEditing?: boolean;
|
||||||
|
disableEditing?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const membersFilters = ["assignees", "mentions", "created_by", "subscriber"];
|
const membersFilters = ["assignees", "mentions", "created_by", "subscriber"];
|
||||||
const dateFilters = ["start_date", "target_date"];
|
const dateFilters = ["start_date", "target_date"];
|
||||||
|
|
||||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||||
const { appliedFilters, handleClearAllFilters, handleRemoveFilter, labels, states, alwaysAllowEditing } = props;
|
const {
|
||||||
|
appliedFilters,
|
||||||
|
handleClearAllFilters,
|
||||||
|
handleRemoveFilter,
|
||||||
|
labels,
|
||||||
|
states,
|
||||||
|
alwaysAllowEditing,
|
||||||
|
disableEditing = false,
|
||||||
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
|
|
@ -44,7 +53,8 @@ export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
if (Object.keys(appliedFilters).length === 0) return null;
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER);
|
const isEditingAllowed =
|
||||||
|
!disableEditing && (alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate">
|
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate">
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,25 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import isEqual from "lodash/isEqual";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
// types
|
||||||
|
import { cn } from "@plane/editor";
|
||||||
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
import { IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||||
// hooks
|
|
||||||
//ui
|
//ui
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList } from "@/components/issues";
|
import { AppliedFiltersList } from "@/components/issues";
|
||||||
// types
|
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||||
|
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||||
|
// constants
|
||||||
import { GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker";
|
import { GLOBAL_VIEW_UPDATED } from "@/constants/event-tracker";
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
// constants
|
// hooks
|
||||||
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "@/hooks/store";
|
import { useEventTracker, useGlobalView, useIssues, useLabel, useUser } from "@/hooks/store";
|
||||||
|
import { getAreFiltersEqual } from "../../../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
globalViewId: string;
|
globalViewId: string;
|
||||||
|
|
@ -33,11 +37,15 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||||
const { globalViewMap, updateGlobalView } = useGlobalView();
|
const { globalViewMap, updateGlobalView } = useGlobalView();
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const {
|
const {
|
||||||
|
data,
|
||||||
membership: { currentWorkspaceRole },
|
membership: { currentWorkspaceRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const userFilters = filters?.[globalViewId]?.filters;
|
const issueFilters = filters?.[globalViewId];
|
||||||
|
const userFilters = issueFilters?.filters;
|
||||||
const viewDetails = globalViewMap[globalViewId];
|
const viewDetails = globalViewMap[globalViewId];
|
||||||
|
|
||||||
// filters whose value not null or empty array
|
// filters whose value not null or empty array
|
||||||
|
|
@ -89,14 +97,15 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewFilters = {
|
||||||
|
filters: cloneDeep(appliedFilters ?? {}),
|
||||||
|
display_filters: cloneDeep(issueFilters?.displayFilters),
|
||||||
|
display_properties: cloneDeep(issueFilters?.displayProperties),
|
||||||
|
};
|
||||||
const handleUpdateView = () => {
|
const handleUpdateView = () => {
|
||||||
if (!workspaceSlug || !globalViewId) return;
|
if (!workspaceSlug || !globalViewId) return;
|
||||||
|
|
||||||
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), {
|
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters).then((res) => {
|
||||||
filters: {
|
|
||||||
...(appliedFilters ?? {}),
|
|
||||||
},
|
|
||||||
}).then((res) => {
|
|
||||||
if (res)
|
if (res)
|
||||||
captureEvent(GLOBAL_VIEW_UPDATED, {
|
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||||
view_id: res.id,
|
view_id: res.id,
|
||||||
|
|
@ -107,34 +116,56 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const areFiltersEqual = isEqual(appliedFilters ?? {}, viewDetails?.filters ?? {});
|
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
|
||||||
|
|
||||||
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes);
|
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes);
|
||||||
|
|
||||||
|
const isLocked = viewDetails?.is_locked;
|
||||||
|
const isOwner = viewDetails?.owned_by === data?.id;
|
||||||
|
const areAppliedFiltersEmpty = isEmpty(appliedFilters);
|
||||||
|
|
||||||
// return if no filters are applied
|
// return if no filters are applied
|
||||||
|
|
||||||
if (isEmpty(appliedFilters) && areFiltersEqual) return null;
|
if (areAppliedFiltersEmpty && areFiltersEqual) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start justify-between gap-4 p-4">
|
<>
|
||||||
<AppliedFiltersList
|
<CreateUpdateWorkspaceViewModal
|
||||||
labels={workspaceLabels ?? undefined}
|
isOpen={isModalOpen}
|
||||||
appliedFilters={appliedFilters ?? {}}
|
onClose={() => setIsModalOpen(false)}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
preLoadedData={{
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
name: `${viewDetails?.name} 2`,
|
||||||
alwaysAllowEditing
|
description: viewDetails?.description,
|
||||||
|
...viewFilters,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
className={cn("flex items-start justify-between gap-4 p-4", {
|
||||||
|
"justify-end": areAppliedFiltersEmpty,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AppliedFiltersList
|
||||||
|
labels={workspaceLabels ?? undefined}
|
||||||
|
appliedFilters={appliedFilters ?? {}}
|
||||||
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
disableEditing={isLocked}
|
||||||
|
alwaysAllowEditing
|
||||||
|
/>
|
||||||
|
|
||||||
{!isDefaultView && !areFiltersEqual && isAuthorizedUser && (
|
{!isDefaultView && (
|
||||||
<>
|
<UpdateViewComponent
|
||||||
<div />
|
isLocked={isLocked}
|
||||||
<Button variant="primary" onClick={handleUpdateView}>
|
areFiltersEqual={!!areFiltersEqual}
|
||||||
Update view
|
isOwner={isOwner}
|
||||||
</Button>
|
isAuthorizedUser={isAuthorizedUser}
|
||||||
</>
|
setIsModalOpen={setIsModalOpen}
|
||||||
)}
|
handleUpdateView={handleUpdateView}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { IIssueFilterOptions } from "@plane/types";
|
|
||||||
// hooks
|
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
import { AppliedFiltersList } from "@/components/issues";
|
|
||||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
|
||||||
import { useIssues, useLabel, useProjectState, useProjectView } from "@/hooks/store";
|
|
||||||
import { getAreFiltersEqual } from "../../../utils";
|
|
||||||
// components
|
|
||||||
// ui
|
|
||||||
// types
|
// types
|
||||||
|
import { IIssueFilterOptions } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { AppliedFiltersList } from "@/components/issues";
|
||||||
|
import { CreateUpdateProjectViewModal } from "@/components/views";
|
||||||
|
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||||
|
// constants
|
||||||
|
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||||
|
import { EUserWorkspaceRoles } from "@/constants/workspace";
|
||||||
|
// hooks
|
||||||
|
import { useIssues, useLabel, useProjectView, useUser } from "@/hooks/store";
|
||||||
|
import { getAreFiltersEqual } from "../../../utils";
|
||||||
|
|
||||||
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
// router
|
// router
|
||||||
|
|
@ -22,8 +26,13 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
issuesFilter: { issueFilters, updateFilters },
|
issuesFilter: { issueFilters, updateFilters },
|
||||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||||
const { projectLabels } = useLabel();
|
const { projectLabels } = useLabel();
|
||||||
const { projectStates } = useProjectState();
|
|
||||||
const { viewMap, updateView } = useProjectView();
|
const { viewMap, updateView } = useProjectView();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
membership: { currentWorkspaceRole },
|
||||||
|
} = useUser();
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
// derived values
|
// derived values
|
||||||
const viewDetails = viewId ? viewMap[viewId.toString()] : null;
|
const viewDetails = viewId ? viewMap[viewId.toString()] : null;
|
||||||
const userFilters = issueFilters?.filters;
|
const userFilters = issueFilters?.filters;
|
||||||
|
|
@ -81,46 +90,56 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
|
const areFiltersEqual = getAreFiltersEqual(appliedFilters, issueFilters, viewDetails);
|
||||||
|
const viewFilters = {
|
||||||
|
filters: cloneDeep(appliedFilters ?? {}),
|
||||||
|
display_filters: cloneDeep(issueFilters?.displayFilters),
|
||||||
|
display_properties: cloneDeep(issueFilters?.displayProperties),
|
||||||
|
};
|
||||||
// return if no filters are applied
|
// return if no filters are applied
|
||||||
if (isEmpty(appliedFilters) && areFiltersEqual) return null;
|
if (isEmpty(appliedFilters) && areFiltersEqual) return null;
|
||||||
|
|
||||||
const handleUpdateView = () => {
|
const handleUpdateView = () => {
|
||||||
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
|
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
|
||||||
|
|
||||||
updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), {
|
updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), viewFilters);
|
||||||
filters: {
|
|
||||||
...(appliedFilters ?? {}),
|
|
||||||
},
|
|
||||||
display_filters: {
|
|
||||||
...issueFilters?.displayFilters,
|
|
||||||
},
|
|
||||||
display_properties: {
|
|
||||||
...issueFilters?.displayProperties,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
|
const isLocked = !!viewDetails?.is_locked;
|
||||||
|
const isOwner = viewDetails?.owned_by === data?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-4 p-4">
|
<div className="flex justify-between gap-4 p-4">
|
||||||
|
<CreateUpdateProjectViewModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
preLoadedData={{
|
||||||
|
name: `${viewDetails?.name} 2`,
|
||||||
|
description: viewDetails?.description,
|
||||||
|
logo_props: viewDetails?.logo_props,
|
||||||
|
...viewFilters,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<AppliedFiltersList
|
<AppliedFiltersList
|
||||||
appliedFilters={appliedFilters ?? {}}
|
appliedFilters={appliedFilters ?? {}}
|
||||||
handleClearAllFilters={handleClearAllFilters}
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
handleRemoveFilter={handleRemoveFilter}
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
labels={projectLabels ?? []}
|
labels={projectLabels ?? []}
|
||||||
states={projectStates}
|
disableEditing={isLocked}
|
||||||
alwaysAllowEditing
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<UpdateViewComponent
|
||||||
{!areFiltersEqual && (
|
isLocked={isLocked}
|
||||||
<div>
|
areFiltersEqual={!!areFiltersEqual}
|
||||||
<Button variant="primary" size="sm" className="flex-shrink-0" onClick={handleUpdateView}>
|
isOwner={isOwner}
|
||||||
Update view
|
isAuthorizedUser={isAuthorizedUser}
|
||||||
</Button>
|
setIsModalOpen={setIsModalOpen}
|
||||||
</div>
|
handleUpdateView={handleUpdateView}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tre
|
||||||
import clone from "lodash/clone";
|
import clone from "lodash/clone";
|
||||||
import concat from "lodash/concat";
|
import concat from "lodash/concat";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
|
import isNil from "lodash/isNil";
|
||||||
import pull from "lodash/pull";
|
import pull from "lodash/pull";
|
||||||
import uniq from "lodash/uniq";
|
import uniq from "lodash/uniq";
|
||||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||||
|
|
@ -21,6 +22,7 @@ import {
|
||||||
IIssueFilters,
|
IIssueFilters,
|
||||||
IProjectView,
|
IProjectView,
|
||||||
TGroupedIssues,
|
TGroupedIssues,
|
||||||
|
IWorkspaceView,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
||||||
|
|
@ -570,11 +572,24 @@ export const handleGroupDragDrop = async (
|
||||||
export const getAreFiltersEqual = (
|
export const getAreFiltersEqual = (
|
||||||
appliedFilters: IIssueFilterOptions | undefined,
|
appliedFilters: IIssueFilterOptions | undefined,
|
||||||
issueFilters: IIssueFilters | undefined,
|
issueFilters: IIssueFilters | undefined,
|
||||||
viewDetails: IProjectView | null
|
viewDetails: IProjectView | IWorkspaceView | null
|
||||||
) =>
|
) => {
|
||||||
isEqual(appliedFilters ?? {}, viewDetails?.filters ?? {}) &&
|
if (isNil(appliedFilters) || isNil(issueFilters) || isNil(viewDetails)) return true;
|
||||||
isEqual(issueFilters?.displayFilters ?? {}, viewDetails?.display_filters ?? {}) &&
|
|
||||||
isEqual(issueFilters?.displayProperties ?? {}, viewDetails?.display_properties ?? {});
|
return (
|
||||||
|
isEqual(appliedFilters, viewDetails.filters) &&
|
||||||
|
isEqual(issueFilters.displayFilters, viewDetails.display_filters) &&
|
||||||
|
isEqual(removeNillKeys(issueFilters.displayProperties), removeNillKeys(viewDetails.display_properties))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* method that removes Null or undefined Keys from object
|
||||||
|
* @param obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const removeNillKeys = <T,>(obj: T) =>
|
||||||
|
Object.fromEntries(Object.entries(obj ?? {}).filter(([key, value]) => key && !isNil(value)));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Method returns if the the grouped values are subGrouped
|
* This Method returns if the the grouped values are subGrouped
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Lock, Sparkle } from "lucide-react";
|
import { Sparkle } from "lucide-react";
|
||||||
// editor
|
// editor
|
||||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon } from "@plane/ui";
|
import { ArchiveIcon } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { GptAssistantPopover } from "@/components/core";
|
import { GptAssistantPopover } from "@/components/core";
|
||||||
|
import { LockedComponent } from "@/components/icons/locked-component";
|
||||||
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
|
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||||
|
|
@ -40,12 +41,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-grow items-center justify-end gap-3">
|
<div className="flex flex-grow items-center justify-end gap-3">
|
||||||
{is_locked && (
|
{is_locked && <LockedComponent />}
|
||||||
<div className="flex h-7 items-center gap-2 rounded-full bg-custom-background-80 px-3 py-0.5 text-xs font-medium text-custom-text-300">
|
|
||||||
<Lock className="h-3 w-3" />
|
|
||||||
<span>Locked</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{archived_at && (
|
{archived_at && (
|
||||||
<div className="flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
<div className="flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
||||||
<ArchiveIcon className="h-3 w-3" />
|
<ArchiveIcon className="h-3 w-3" />
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ import { Button, EmojiIconPicker, EmojiIconPickerTypes, Input, PhotoFilterIcon,
|
||||||
import { Logo } from "@/components/common";
|
import { Logo } from "@/components/common";
|
||||||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||||
// constants
|
// constants
|
||||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||||
// helpers
|
// helpers
|
||||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||||
|
import { LayoutDropDown } from "../dropdowns/layout";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data?: IProjectView | null;
|
data?: IProjectView | null;
|
||||||
|
|
@ -190,7 +191,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
<span className="text-xs text-red-500">{errors?.name?.message?.toString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -211,7 +212,17 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="display_filters.layout"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<LayoutDropDown
|
||||||
|
onChange={(selectedValue: EIssueLayoutTypes) => onChange(selectedValue)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="filters"
|
name="filters"
|
||||||
|
|
|
||||||
70
web/core/components/views/update-view-component.tsx
Normal file
70
web/core/components/views/update-view-component.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
import { LockedComponent } from "../icons/locked-component";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLocked: boolean;
|
||||||
|
areFiltersEqual: boolean;
|
||||||
|
isOwner: boolean;
|
||||||
|
isAuthorizedUser: boolean;
|
||||||
|
setIsModalOpen: (value: SetStateAction<boolean>) => void;
|
||||||
|
handleUpdateView: () => void;
|
||||||
|
lockedTooltipContent?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdateViewComponent = (props: Props) => {
|
||||||
|
const {
|
||||||
|
isLocked,
|
||||||
|
areFiltersEqual,
|
||||||
|
isOwner,
|
||||||
|
isAuthorizedUser,
|
||||||
|
setIsModalOpen,
|
||||||
|
handleUpdateView,
|
||||||
|
lockedTooltipContent,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (areFiltersEqual) {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
}, [areFiltersEqual]);
|
||||||
|
|
||||||
|
// Change state while updating view to have a feedback
|
||||||
|
const updateButton = isUpdating ? (
|
||||||
|
<Button variant="primary" size="sm" className="flex-shrink-0">
|
||||||
|
Updating...
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
handleUpdateView();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update view
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isLocked ? (
|
||||||
|
<LockedComponent toolTipContent={lockedTooltipContent} />
|
||||||
|
) : (
|
||||||
|
!areFiltersEqual &&
|
||||||
|
isAuthorizedUser && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline-primary" size="sm" className="flex-shrink-0" onClick={() => setIsModalOpen(true)}>
|
||||||
|
Save as
|
||||||
|
</Button>
|
||||||
|
{isOwner && <>{updateButton}</>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { Earth, Lock } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IProjectView } from "@plane/types";
|
import { IProjectView } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { FavoriteStar } from "@plane/ui";
|
import { Tooltip, FavoriteStar } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
|
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
// helpers
|
// helpers
|
||||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -39,6 +41,8 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
||||||
|
|
||||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||||
|
|
||||||
|
const access = view.access;
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
const handleAddToFavorites = () => {
|
const handleAddToFavorites = () => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
@ -70,8 +74,14 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
||||||
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="cursor-default text-custom-text-300">
|
||||||
|
<Tooltip tooltipContent={access === EViewAccess.PUBLIC ? "Public" : "Private"}>
|
||||||
|
{access === EViewAccess.PUBLIC ? <Earth className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* created by */}
|
{/* created by */}
|
||||||
{createdByDetails && <ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />}
|
{<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id ?? []} />}
|
||||||
|
|
||||||
{isEditingAllowed && (
|
{isEditingAllowed && (
|
||||||
<FavoriteStar
|
<FavoriteStar
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
import { ExternalLink, LinkIcon, Pencil, Trash2, Lock } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceView } from "@plane/types";
|
import { IWorkspaceView } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -12,6 +12,7 @@ import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from
|
||||||
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
|
import { CreateUpdateWorkspaceViewModal, DeleteGlobalViewModal } from "@/components/workspace";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "@/constants/project";
|
import { EUserProjectRoles } from "@/constants/project";
|
||||||
|
import { EViewAccess } from "@/constants/views";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||||
|
|
@ -78,6 +79,34 @@ export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isSelected = viewId === globalViewId;
|
||||||
|
const isPrivateView = view.access === EViewAccess.PRIVATE;
|
||||||
|
|
||||||
|
let customButton = (
|
||||||
|
<div
|
||||||
|
className={`flex gap-1 items-center flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
||||||
|
isSelected
|
||||||
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
|
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
||||||
|
} ${isPrivateView ? "pr-2" : ""}`}
|
||||||
|
>
|
||||||
|
<span className={`flex min-w-min flex-shrink-0 whitespace-nowrap text-sm font-medium outline-none`}>
|
||||||
|
{view.name}
|
||||||
|
</span>
|
||||||
|
{isPrivateView && (
|
||||||
|
<Lock className={`${isSelected ? "text-custom-primary-100" : "text-custom-text-400"} h-4 w-4`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isSelected) {
|
||||||
|
customButton = (
|
||||||
|
<Link key={viewId} id={`global-view-${viewId}`} href={`/${workspaceSlug}/workspace-views/${viewId}`}>
|
||||||
|
{customButton}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
||||||
|
|
@ -85,38 +114,7 @@ export const WorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||||
|
|
||||||
<CustomMenu
|
<CustomMenu customButton={customButton} placement="bottom-end" menuItemsClassName="z-20" closeOnSelect>
|
||||||
customButton={
|
|
||||||
<>
|
|
||||||
{viewId === globalViewId ? (
|
|
||||||
<span
|
|
||||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
|
||||||
viewId === globalViewId
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
|
||||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{view.name}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<Link key={viewId} id={`global-view-${viewId}`} href={`/${workspaceSlug}/workspace-views/${viewId}`}>
|
|
||||||
<span
|
|
||||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
|
||||||
viewId === globalViewId
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
|
||||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{view.name}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
placement="bottom-end"
|
|
||||||
menuItemsClassName="z-20"
|
|
||||||
closeOnSelect
|
|
||||||
>
|
|
||||||
{MENU_ITEMS.map((item) => {
|
{MENU_ITEMS.map((item) => {
|
||||||
if (item.shouldRender === false) return null;
|
if (item.shouldRender === false) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -138,17 +138,34 @@ export const ISSUE_EXTRA_OPTIONS: {
|
||||||
{ key: "show_empty_groups", title: "Show empty groups" }, // filter on front-end
|
{ key: "show_empty_groups", title: "Show empty groups" }, // filter on front-end
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ISSUE_LAYOUT_MAP = {
|
||||||
|
[EIssueLayoutTypes.LIST]: { key: EIssueLayoutTypes.LIST, title: "List Layout", label: "List", icon: List },
|
||||||
|
[EIssueLayoutTypes.KANBAN]: { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", label: "Kanban", icon: Kanban },
|
||||||
|
[EIssueLayoutTypes.CALENDAR]: {
|
||||||
|
key: EIssueLayoutTypes.CALENDAR,
|
||||||
|
title: "Calendar Layout",
|
||||||
|
label: "Calendar",
|
||||||
|
icon: Calendar,
|
||||||
|
},
|
||||||
|
[EIssueLayoutTypes.SPREADSHEET]: {
|
||||||
|
key: EIssueLayoutTypes.SPREADSHEET,
|
||||||
|
title: "Spreadsheet Layout",
|
||||||
|
label: "Spreadsheet",
|
||||||
|
icon: Sheet,
|
||||||
|
},
|
||||||
|
[EIssueLayoutTypes.GANTT]: {
|
||||||
|
key: EIssueLayoutTypes.GANTT,
|
||||||
|
title: "Gantt Chart Layout",
|
||||||
|
label: "Gantt",
|
||||||
|
icon: GanttChartSquare,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const ISSUE_LAYOUTS: {
|
export const ISSUE_LAYOUTS: {
|
||||||
key: EIssueLayoutTypes;
|
key: EIssueLayoutTypes;
|
||||||
title: string;
|
title: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
}[] = [
|
}[] = Object.values(ISSUE_LAYOUT_MAP);
|
||||||
{ key: EIssueLayoutTypes.LIST, title: "List Layout", icon: List },
|
|
||||||
{ key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", icon: Kanban },
|
|
||||||
{ key: EIssueLayoutTypes.CALENDAR, title: "Calendar Layout", icon: Calendar },
|
|
||||||
{ key: EIssueLayoutTypes.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
|
|
||||||
{ key: EIssueLayoutTypes.GANTT, title: "Gantt Chart Layout", icon: GanttChartSquare },
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface ILayoutDisplayFiltersOptions {
|
export interface ILayoutDisplayFiltersOptions {
|
||||||
filters: (keyof IIssueFilterOptions)[];
|
filters: (keyof IIssueFilterOptions)[];
|
||||||
|
|
|
||||||
4
web/core/constants/views.ts
Normal file
4
web/core/constants/views.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum EViewAccess {
|
||||||
|
PRIVATE,
|
||||||
|
PUBLIC,
|
||||||
|
}
|
||||||
|
|
@ -240,10 +240,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
|
|
||||||
display_filters: _filters.displayFilters,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
||||||
|
|
@ -260,9 +256,6 @@ export class ProjectViewIssuesFilter extends IssueFilterHelperStore implements I
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.issueFilterService.patchView(workspaceSlug, projectId, viewId, {
|
|
||||||
display_properties: _filters.displayProperties,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EIssueFilterType.KANBAN_FILTERS: {
|
case EIssueFilterType.KANBAN_FILTERS: {
|
||||||
|
|
|
||||||
|
|
@ -249,11 +249,6 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
|
||||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
||||||
display_filters: _filters.displayFilters,
|
display_filters: _filters.displayFilters,
|
||||||
});
|
});
|
||||||
else
|
|
||||||
await this.issueFilterService.updateView(workspaceSlug, viewId, {
|
|
||||||
display_filters: _filters.displayFilters,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
||||||
|
|
@ -268,15 +263,11 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
|
||||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
|
||||||
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
||||||
|
display_properties: _filters.displayProperties,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (["all-issues", "assigned", "created", "subscribed"].includes(viewId))
|
|
||||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.GLOBAL, type, workspaceSlug, undefined, viewId, {
|
|
||||||
display_properties: _filters.displayProperties,
|
|
||||||
});
|
|
||||||
else
|
|
||||||
await this.issueFilterService.updateView(workspaceSlug, viewId, {
|
|
||||||
display_properties: _filters.displayProperties,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue