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:
Aaryan Khandelwal 2023-03-23 02:13:52 +05:30 committed by GitHub
parent 79249c5c9b
commit d477c19ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 21 deletions

View File

@ -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}
</> </>

View File

@ -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

View File

@ -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}

View File

@ -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,

View File

@ -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,