mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
fix-sub-wo
...
add-layout
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b4019a69 |
3
packages/types/src/views.d.ts
vendored
3
packages/types/src/views.d.ts
vendored
@@ -13,6 +13,9 @@ export interface IProjectView {
|
|||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
updated_by: string;
|
updated_by: string;
|
||||||
|
isLocked: boolean | null;
|
||||||
|
isPrivate: boolean | null;
|
||||||
|
isPublished: boolean | null;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
|
|||||||
@@ -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={option.value}
|
||||||
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,6 +59,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
|||||||
option.className && option.className({ active, selected })
|
option.className && option.className({ active, selected })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
@@ -65,7 +67,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
|
|||||||
<>{renderItem({ value: option.data[option.value], selected })}</>
|
<>{renderItem({ value: option.data[option.value], 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" />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
8
packages/ui/src/dropdown/dropdown.d.ts
vendored
8
packages/ui/src/dropdown/dropdown.d.ts
vendored
@@ -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]),
|
||||||
@@ -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>
|
||||||
|
|||||||
26
web/core/components/dropdowns/layout.tsx
Normal file
26
web/core/components/dropdowns/layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Dropdown } from "@plane/ui";
|
||||||
|
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 = Object.values(ISSUE_LAYOUT_MAP).map((issueLayout) => ({
|
||||||
|
data: issueLayout.key,
|
||||||
|
value: issueLayout.key,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
onChange={onChange as (value: string) => void}
|
||||||
|
value={value?.toString()}
|
||||||
|
keyExtractor={(option) => option.value}
|
||||||
|
options={options}
|
||||||
|
disableSearch
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -48,7 +49,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
} = useForm<IProjectView>({
|
} = useForm<IProjectView & { layout: EIssueLayoutTypes }>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,13 +91,13 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateUpdateView = async (formData: IProjectView) => {
|
const handleCreateUpdateView = async (formData: IProjectView & { layout: EIssueLayoutTypes }) => {
|
||||||
await handleFormSubmit({
|
await handleFormSubmit({
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
logo_props: formData.logo_props,
|
logo_props: formData.logo_props,
|
||||||
filters: formData.filters,
|
filters: formData.filters,
|
||||||
display_filters: formData.display_filters,
|
display_filters: { ...formData.display_filters, layout: formData.layout },
|
||||||
display_properties: formData.display_properties,
|
display_properties: formData.display_properties,
|
||||||
} as IProjectView);
|
} as IProjectView);
|
||||||
|
|
||||||
@@ -211,6 +212,18 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="layout"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<LayoutDropDown
|
||||||
|
onChange={(selectedValue: EIssueLayoutTypes) => onChange(selectedValue)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -138,17 +138,19 @@ 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", icon: List },
|
||||||
|
[EIssueLayoutTypes.KANBAN]: { key: EIssueLayoutTypes.KANBAN, title: "Kanban Layout", icon: Kanban },
|
||||||
|
[EIssueLayoutTypes.CALENDAR]: { key: EIssueLayoutTypes.CALENDAR, title: "Calendar Layout", icon: Calendar },
|
||||||
|
[EIssueLayoutTypes.SPREADSHEET]: { key: EIssueLayoutTypes.SPREADSHEET, title: "Spreadsheet Layout", icon: Sheet },
|
||||||
|
[EIssueLayoutTypes.GANTT]: { key: EIssueLayoutTypes.GANTT, title: "Gantt Chart Layout", 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)[];
|
||||||
|
|||||||
Reference in New Issue
Block a user