mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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";
|
||||
// components
|
||||
import { SingleBoard } from "components/core/board-view/single-board";
|
||||
// helpers
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssue, IState, UserAuth } from "types";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
type: "issue" | "cycle" | "module";
|
||||
@ -30,7 +33,11 @@ export const AllBoards: React.FC<Props> = ({
|
||||
removeIssue,
|
||||
userAuth,
|
||||
}) => {
|
||||
const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useProjectIssuesView();
|
||||
const {
|
||||
groupedByIssues,
|
||||
groupByProperty: selectedGroup,
|
||||
showEmptyGroups,
|
||||
} = useProjectIssuesView();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -40,6 +47,8 @@ export const AllBoards: React.FC<Props> = ({
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
||||
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
|
||||
|
||||
return (
|
||||
<SingleBoard
|
||||
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>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -42,8 +42,10 @@ export const IssuesFilterView: React.FC = () => {
|
||||
setIssueViewToKanban,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
setOrderBy,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
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">
|
||||
<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">
|
||||
<h4 className="text-sm text-gray-600">Group by</h4>
|
||||
<h4 className="text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)?.name ??
|
||||
@ -226,7 +228,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<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
|
||||
label={
|
||||
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
|
||||
@ -249,7 +251,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<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
|
||||
label={
|
||||
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
|
||||
@ -271,17 +273,33 @@ export const IssuesFilterView: React.FC = () => {
|
||||
))}
|
||||
</CustomMenu>
|
||||
</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
|
||||
type="button"
|
||||
className="text-xs"
|
||||
onClick={() => resetFilterToDefault()}
|
||||
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 ${
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-theme"
|
||||
className="font-medium text-theme"
|
||||
onClick={() => setNewFilterDefaultView()}
|
||||
>
|
||||
Set as default
|
||||
|
@ -29,7 +29,7 @@ export const AllLists: React.FC<Props> = ({
|
||||
removeIssue,
|
||||
userAuth,
|
||||
}) => {
|
||||
const { groupedByIssues, groupByProperty: selectedGroup } = useIssuesView();
|
||||
const { groupedByIssues, groupByProperty: selectedGroup, showEmptyGroups } = useIssuesView();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -39,6 +39,8 @@ export const AllLists: React.FC<Props> = ({
|
||||
const currentState =
|
||||
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
|
||||
|
||||
if (!showEmptyGroups && groupedByIssues[singleGroup].length === 0) return null;
|
||||
|
||||
return (
|
||||
<SingleList
|
||||
key={singleGroup}
|
||||
|
@ -10,16 +10,9 @@ import ToastAlert from "components/toast-alert";
|
||||
import projectService from "services/project.service";
|
||||
import viewsService from "services/views.service";
|
||||
// types
|
||||
import { IIssueFilterOptions, IProjectMember, NestedKeyOf } from "types";
|
||||
import { IIssueFilterOptions, IProjectMember } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
USER_PROJECT_VIEW,
|
||||
VIEW_DETAILS,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
import { USER_PROJECT_VIEW, VIEW_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
export const issueViewContext = createContext<ContextType>({} as ContextType);
|
||||
|
||||
@ -27,6 +20,7 @@ type IssueViewProps = {
|
||||
issueView: "list" | "kanban";
|
||||
groupByProperty: "state" | "priority" | "labels" | null;
|
||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
showEmptyGroups: boolean;
|
||||
filters: IIssueFilterOptions;
|
||||
};
|
||||
|
||||
@ -35,6 +29,7 @@ type ReducerActionType = {
|
||||
| "REHYDRATE_THEME"
|
||||
| "SET_ISSUE_VIEW"
|
||||
| "SET_ORDER_BY_PROPERTY"
|
||||
| "SET_SHOW_EMPTY_STATES"
|
||||
| "SET_FILTERS"
|
||||
| "SET_GROUP_BY_PROPERTY"
|
||||
| "RESET_TO_DEFAULT";
|
||||
@ -44,6 +39,7 @@ type ReducerActionType = {
|
||||
type ContextType = IssueViewProps & {
|
||||
setGroupByProperty: (property: "state" | "priority" | "labels" | null) => void;
|
||||
setOrderBy: (property: "created_at" | "updated_at" | "priority" | "sort_order") => void;
|
||||
setShowEmptyGroups: (property: boolean) => void;
|
||||
setFilters: (filters: Partial<IIssueFilterOptions>, saveToServer?: boolean) => void;
|
||||
resetFilterToDefault: () => void;
|
||||
setNewFilterDefaultView: () => void;
|
||||
@ -55,6 +51,7 @@ type StateType = {
|
||||
issueView: "list" | "kanban";
|
||||
groupByProperty: "state" | "priority" | "labels" | null;
|
||||
orderBy: "created_at" | "updated_at" | "priority" | "sort_order";
|
||||
showEmptyGroups: boolean;
|
||||
filters: IIssueFilterOptions;
|
||||
};
|
||||
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
|
||||
@ -63,6 +60,7 @@ export const initialState: StateType = {
|
||||
issueView: "list",
|
||||
groupByProperty: null,
|
||||
orderBy: "created_at",
|
||||
showEmptyGroups: false,
|
||||
filters: {
|
||||
type: 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": {
|
||||
const newState = {
|
||||
...state,
|
||||
@ -190,7 +200,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
|
||||
const { data: myViewProps, mutate: mutateMyViewProps } = useSWR(
|
||||
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]
|
||||
);
|
||||
|
||||
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(
|
||||
(property: Partial<IIssueFilterOptions>, saveToServer = true) => {
|
||||
Object.keys(property).forEach((key) => {
|
||||
@ -438,7 +479,9 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
|
||||
groupByProperty: state.groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy: state.orderBy,
|
||||
showEmptyGroups: state.showEmptyGroups,
|
||||
setOrderBy,
|
||||
setShowEmptyGroups,
|
||||
filters: state.filters,
|
||||
setFilters,
|
||||
resetFilterToDefault: resetToDefault,
|
||||
|
@ -32,6 +32,8 @@ const useIssuesView = () => {
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilterToDefault,
|
||||
@ -107,6 +109,7 @@ const useIssuesView = () => {
|
||||
);
|
||||
const statesList = getStatesList(states ?? {});
|
||||
const stateIds = statesList.map((state) => state.id);
|
||||
|
||||
let emptyStatesObject: { [key: string]: [] } = {};
|
||||
for (let i = 0; i < stateIds.length; i++) {
|
||||
emptyStatesObject[stateIds[i]] = [];
|
||||
@ -132,6 +135,8 @@ const useIssuesView = () => {
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
filters,
|
||||
setFilters,
|
||||
params,
|
||||
|
Loading…
Reference in New Issue
Block a user