forked from github/plane
feat: show empty states toggle button in the views dropdown (#500)
* feat: show empty states toggle button in views dropdown * refactor: empty state toggle naming convention, feat: hidden groups in section in the kanban board
This commit is contained in:
parent
79249c5c9b
commit
d477c19ad9
@ -2,8 +2,11 @@
|
|||||||
import useProjectIssuesView from "hooks/use-issues-view";
|
import useProjectIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { SingleBoard } from "components/core/board-view/single-board";
|
import { SingleBoard } from "components/core/board-view/single-board";
|
||||||
|
// helpers
|
||||||
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IState, UserAuth } from "types";
|
import { IIssue, IState, UserAuth } from "types";
|
||||||
|
import { getStateGroupIcon } from "components/icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: "issue" | "cycle" | "module";
|
type: "issue" | "cycle" | "module";
|
||||||
@ -30,7 +33,11 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useProjectIssuesView();
|
const {
|
||||||
|
groupedByIssues,
|
||||||
|
groupByProperty: selectedGroup,
|
||||||
|
showEmptyGroups,
|
||||||
|
} = useProjectIssuesView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -40,6 +47,8 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
|
||||||
|
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleBoard
|
<SingleBoard
|
||||||
key={index}
|
key={index}
|
||||||
@ -57,6 +66,33 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{!showEmptyGroups && (
|
||||||
|
<div className="h-full w-96 flex-shrink-0 space-y-3 p-1">
|
||||||
|
<h2 className="text-lg font-semibold">Hidden groups</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.keys(groupedByIssues).map((singleGroup, index) => {
|
||||||
|
const currentState =
|
||||||
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
|
||||||
|
if (groupedByIssues[singleGroup].length === 0)
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded bg-white p-2 shadow">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentState &&
|
||||||
|
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
|
||||||
|
<h4 className="text-sm capitalize">
|
||||||
|
{selectedGroup === "state"
|
||||||
|
? addSpaceIfCamelCase(currentState?.name ?? "")
|
||||||
|
: addSpaceIfCamelCase(singleGroup)}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">0</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -42,8 +42,10 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
setIssueViewToKanban,
|
setIssueViewToKanban,
|
||||||
groupByProperty,
|
groupByProperty,
|
||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
setOrderBy,
|
|
||||||
orderBy,
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
showEmptyGroups,
|
||||||
|
setShowEmptyGroups,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
resetFilterToDefault,
|
resetFilterToDefault,
|
||||||
@ -203,9 +205,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
<Popover.Panel className="absolute right-0 z-20 mt-1 w-screen max-w-xs transform overflow-hidden rounded-lg bg-white p-3 shadow-lg">
|
||||||
<div className="relative divide-y-2">
|
<div className="relative divide-y-2">
|
||||||
<div className="space-y-4 pb-3">
|
<div className="space-y-4 pb-3 text-xs">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
<h4 className="text-gray-600">Group by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
||||||
@ -226,7 +228,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-sm text-gray-600">Order by</h4>
|
<h4 className="text-gray-600">Order by</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||||
@ -249,7 +251,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h4 className="text-sm text-gray-600">Issue type</h4>
|
<h4 className="text-gray-600">Issue type</h4>
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
label={
|
label={
|
||||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||||
@ -271,17 +273,33 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-end gap-x-3">
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-gray-600">Show empty states</h4>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs"
|
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
onClick={() => resetFilterToDefault()}
|
showEmptyGroups ? "bg-green-500" : "bg-gray-200"
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={showEmptyGroups}
|
||||||
|
onClick={() => setShowEmptyGroups(!showEmptyGroups)}
|
||||||
>
|
>
|
||||||
|
<span className="sr-only">Show empty groups</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
showEmptyGroups ? "translate-x-2.5" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-end gap-x-3">
|
||||||
|
<button type="button" onClick={() => resetFilterToDefault()}>
|
||||||
Reset to default
|
Reset to default
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs font-medium text-theme"
|
className="font-medium text-theme"
|
||||||
onClick={() => setNewFilterDefaultView()}
|
onClick={() => setNewFilterDefaultView()}
|
||||||
>
|
>
|
||||||
Set as default
|
Set as default
|
||||||
|
@ -29,7 +29,7 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
removeIssue,
|
removeIssue,
|
||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -39,6 +39,8 @@ export const AllLists: React.FC<Props> = ({
|
|||||||
const currentState =
|
const currentState =
|
||||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||||
|
|
||||||
|
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleList
|
<SingleList
|
||||||
key={singleGroup}
|
key={singleGroup}
|
||||||
|
@ -10,16 +10,9 @@ import ToastAlert from "components/toast-alert";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
|
import { IIssueFilterOptions, IProjectMember } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import {
|
import { USER_PROJECT_VIEW, VIEW_DETAILS } from "constants/fetch-keys";
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
|
||||||
USER_PROJECT_VIEW,
|
|
||||||
VIEW_DETAILS,
|
|
||||||
VIEW_ISSUES,
|
|
||||||
} from "constants/fetch-keys";
|
|
||||||
|
|
||||||
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||||
|
|
||||||
@ -27,6 +20,7 @@ type IssueViewProps = {
|
|||||||
issueView: "list" | "kanban";
|
issueView: "list" | "kanban";
|
||||||
groupByProperty: "state" | "priority" | "labels" | null;
|
groupByProperty: "state" | "priority" | "labels" | null;
|
||||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
|
showEmptyGroups: boolean;
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,6 +29,7 @@ type ReducerActionType = {
|
|||||||
| "REHYDRATE_THEME"
|
| "REHYDRATE_THEME"
|
||||||
| "SET_ISSUE_VIEW"
|
| "SET_ISSUE_VIEW"
|
||||||
| "SET_ORDER_BY_PROPERTY"
|
| "SET_ORDER_BY_PROPERTY"
|
||||||
|
| "SET_SHOW_EMPTY_STATES"
|
||||||
| "SET_FILTERS"
|
| "SET_FILTERS"
|
||||||
| "SET_GROUP_BY_PROPERTY"
|
| "SET_GROUP_BY_PROPERTY"
|
||||||
| "RESET_TO_DEFAULT";
|
| "RESET_TO_DEFAULT";
|
||||||
@ -44,6 +39,7 @@ type ReducerActionType = {
|
|||||||
type ContextType = IssueViewProps & {
|
type ContextType = IssueViewProps & {
|
||||||
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
|
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
|
||||||
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
|
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
|
||||||
|
setShowEmptyGroups: (property: boolean) => void;
|
||||||
setFilters: (filters: Partial<IIssueFilterOptions>, saveToServer?: boolean) => void;
|
setFilters: (filters: Partial<IIssueFilterOptions>, saveToServer?: boolean) => void;
|
||||||
resetFilterToDefault: () => void;
|
resetFilterToDefault: () => void;
|
||||||
setNewFilterDefaultView: () => void;
|
setNewFilterDefaultView: () => void;
|
||||||
@ -55,6 +51,7 @@ type StateType = {
|
|||||||
issueView: "list" | "kanban";
|
issueView: "list" | "kanban";
|
||||||
groupByProperty: "state" | "priority" | "labels" | null;
|
groupByProperty: "state" | "priority" | "labels" | null;
|
||||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||||
|
showEmptyGroups: boolean;
|
||||||
filters: IIssueFilterOptions;
|
filters: IIssueFilterOptions;
|
||||||
};
|
};
|
||||||
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
||||||
@ -63,6 +60,7 @@ export const initialState: StateType = {
|
|||||||
issueView: "list",
|
issueView: "list",
|
||||||
groupByProperty: null,
|
groupByProperty: null,
|
||||||
orderBy: "created_at",
|
orderBy: "created_at",
|
||||||
|
showEmptyGroups: false,
|
||||||
filters: {
|
filters: {
|
||||||
type: null,
|
type: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
@ -121,6 +119,18 @@ export const reducer: ReducerFunctionType = (state, action) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "SET_SHOW_EMPTY_STATES": {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
showEmptyGroups: payload?.showEmptyGroups || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "SET_FILTERS": {
|
case "SET_FILTERS": {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
@ -190,7 +200,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
||||||
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
|
||||||
@ -334,6 +344,37 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
[projectId, workspaceSlug, state, mutateMyViewProps]
|
[projectId, workspaceSlug, state, mutateMyViewProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setShowEmptyGroups = useCallback(
|
||||||
|
(property: boolean) => {
|
||||||
|
dispatch({
|
||||||
|
type: "SET_SHOW_EMPTY_STATES",
|
||||||
|
payload: {
|
||||||
|
showEmptyGroups: property,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
mutateMyViewProps((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
view_props: {
|
||||||
|
...state,
|
||||||
|
showEmptyGroups: property,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
saveDataToServer(workspaceSlug as string, projectId as string, {
|
||||||
|
...state,
|
||||||
|
showEmptyGroups: property,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[projectId, workspaceSlug, state, mutateMyViewProps]
|
||||||
|
);
|
||||||
|
|
||||||
const setFilters = useCallback(
|
const setFilters = useCallback(
|
||||||
(property: Partial<IIssueFilterOptions>, saveToServer = true) => {
|
(property: Partial<IIssueFilterOptions>, saveToServer = true) => {
|
||||||
Object.keys(property).forEach((key) => {
|
Object.keys(property).forEach((key) => {
|
||||||
@ -438,7 +479,9 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
|||||||
groupByProperty: state.groupByProperty,
|
groupByProperty: state.groupByProperty,
|
||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
orderBy: state.orderBy,
|
orderBy: state.orderBy,
|
||||||
|
showEmptyGroups: state.showEmptyGroups,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
|
setShowEmptyGroups,
|
||||||
filters: state.filters,
|
filters: state.filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
resetFilterToDefault: resetToDefault,
|
resetFilterToDefault: resetToDefault,
|
||||||
|
@ -32,6 +32,8 @@ const useIssuesView = () => {
|
|||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
orderBy,
|
orderBy,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
|
showEmptyGroups,
|
||||||
|
setShowEmptyGroups,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
resetFilterToDefault,
|
resetFilterToDefault,
|
||||||
@ -107,6 +109,7 @@ const useIssuesView = () => {
|
|||||||
);
|
);
|
||||||
const statesList = getStatesList(states ?? {});
|
const statesList = getStatesList(states ?? {});
|
||||||
const stateIds = statesList.map((state) => state.id);
|
const stateIds = statesList.map((state) => state.id);
|
||||||
|
|
||||||
let emptyStatesObject: { [key: string]: [] } = {};
|
let emptyStatesObject: { [key: string]: [] } = {};
|
||||||
for (let i = 0; i < stateIds.length; i++) {
|
for (let i = 0; i < stateIds.length; i++) {
|
||||||
emptyStatesObject[stateIds[i]] = [];
|
emptyStatesObject[stateIds[i]] = [];
|
||||||
@ -132,6 +135,8 @@ const useIssuesView = () => {
|
|||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
orderBy,
|
orderBy,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
|
showEmptyGroups,
|
||||||
|
setShowEmptyGroups,
|
||||||
filters,
|
filters,
|
||||||
setFilters,
|
setFilters,
|
||||||
params,
|
params,
|
||||||
|
Loading…
Reference in New Issue
Block a user