From 992fb7f20f2a3978d51c139ab164e2b965715f07 Mon Sep 17 00:00:00 2001 From: Horia Coman Date: Mon, 17 Jun 2024 00:02:16 +0300 Subject: [PATCH] Merged branch feature/is-done-toggle into develop --- .../app/components/infra/section-actions.tsx | 239 +++++++++++++++--- .../app/routes/workspace/time-plans/$id.tsx | 122 +++++++-- 2 files changed, 302 insertions(+), 59 deletions(-) diff --git a/src/webui/app/components/infra/section-actions.tsx b/src/webui/app/components/infra/section-actions.tsx index 5d5c7d8f..eab43ab3 100644 --- a/src/webui/app/components/infra/section-actions.tsx +++ b/src/webui/app/components/infra/section-actions.tsx @@ -1,9 +1,13 @@ import type { WorkspaceFeature } from "@jupiter/webapi-client"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import BoltIcon from "@mui/icons-material/Bolt"; +import CheckBoxIcon from "@mui/icons-material/CheckBox"; +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; import { + Autocomplete, Button, ButtonGroup, + Checkbox, ClickAwayListener, Dialog, DialogActions, @@ -15,6 +19,7 @@ import { Paper, Popper, Stack, + TextField, useTheme, } from "@mui/material"; import { Link } from "@remix-run/react"; @@ -52,10 +57,19 @@ interface FilterFewOptionsDesc { hideIfOneOption?: boolean; } +interface FilterManyOptionsDesc { + kind: "filter-many-options"; + title: string; + options: Array>; + onSelect: (selected: Array) => void; + hideIfOneOption?: boolean; +} + type ActionDesc = | NavSingleDesc // A single button, can be a navigation or a callback | NavMultipleDesc // A group of buttons, can be a navigation or a callback - | FilterFewOptionsDesc; // A group to filter on, can be a navigation or a callback + | FilterFewOptionsDesc // A group to filter on, can be a navigation or a callback + | FilterManyOptionsDesc; // A group to filter on, with many options export function NavSingle( text: string, @@ -105,56 +119,40 @@ export function FilterFewOptions( }; } +export function FilterManyOptions( + title: string, + options: Array>, + onSelect: (selected: Array) => void +): FilterManyOptionsDesc { + return { + kind: "filter-many-options", + title: title, + options: options, + onSelect: onSelect, + hideIfOneOption: true, + }; +} + interface SectionActionsProps { id: string; topLevelInfo: TopLevelInfo; inputsEnabled: boolean; actions: Array; + extraActions?: Array; } export function SectionActions(props: SectionActionsProps) { const isBigScreen = useBigScreen(); - const [showExtraActionsDialog, setShowExtraActionsDialog] = useState(false); - if (!isBigScreen) { + const allActions = props.actions.concat(props.extraActions ?? []); return ( - <> - - - setShowExtraActionsDialog(false)} - open={showExtraActionsDialog} - > - Actions - - - {props.actions.map((action, index) => ( - - ))} - - - - - - - + ); } @@ -169,10 +167,72 @@ export function SectionActions(props: SectionActionsProps) { action={action} /> ))} + + ); } +interface SectionActionsWithDialogProps { + id: string; + topLevelInfo: TopLevelInfo; + inputsEnabled: boolean; + actions: Array; +} + +function SectionActionsWithDialog(props: SectionActionsWithDialogProps) { + const [showExtraActionsDialog, setShowExtraActionsDialog] = useState(false); + + if (props.actions.length === 0) { + return <>; + } + + return ( + <> + + + setShowExtraActionsDialog(false)} + open={showExtraActionsDialog} + > + Actions + + + {props.actions.map((action, index) => ( + + ))} + + + + + + + + ); +} + interface ActionViewProps { topLevelInfo: TopLevelInfo; inputsEnabled: boolean; @@ -210,6 +270,16 @@ function ActionView(props: ActionViewProps) { action={props.action} /> ); + + case "filter-many-options": + return ( + + ); } } @@ -349,7 +419,7 @@ function NavMultipleCompactView(props: NavMultipleViewProps) { { function FilterFewOptionsView(props: FilterFewOptionsViewProps) { const [selected, setSelected] = useState(props.action.defaultOption); + const realOptions: FilterOption[] = []; + for (const option of props.action.options) { + if (option.gatedOn) { + const workspace = props.topLevelInfo.workspace; + if (!isWorkspaceFeatureAvailable(workspace, option.gatedOn)) { + continue; + } + } + realOptions.push(option); + } + + if (realOptions.length === 0) { + return <>; + } + + if (props.action.hideIfOneOption && realOptions.length === 1) { + return <>; + } + return ( {props.action.options.map((option, index) => { @@ -428,3 +517,73 @@ function FilterFewOptionsView(props: FilterFewOptionsViewProps) { ); } + +interface FilterManyOptionsViewProps { + topLevelInfo: TopLevelInfo; + inputsEnabled: boolean; + orientation: "horizontal" | "vertical"; + action: FilterManyOptionsDesc; +} + +function FilterManyOptionsView(props: FilterManyOptionsViewProps) { + const icon = ; + const checkedIcon = ; + + const [selected, setSelected] = useState[]>([]); + + const realOptions: FilterOption[] = []; + for (const option of props.action.options) { + if (option.gatedOn) { + const workspace = props.topLevelInfo.workspace; + if (!isWorkspaceFeatureAvailable(workspace, option.gatedOn)) { + continue; + } + } + realOptions.push(option); + } + + if (realOptions.length === 0) { + return <>; + } + + if (props.action.hideIfOneOption && realOptions.length === 1) { + return <>; + } + + return ( + option.text} + value={selected} + onChange={(_, selected) => { + setSelected(selected); + props.action.onSelect(selected.map((option) => option.value)); + }} + isOptionEqualToValue={(option, value) => option.value === value.value} + renderOption={(props, option, { selected }) => ( +
  • + + {option.text} +
  • + )} + style={{ minWidth: "180px" }} + renderInput={(params) => ( + + )} + /> + ); +} diff --git a/src/webui/app/routes/workspace/time-plans/$id.tsx b/src/webui/app/routes/workspace/time-plans/$id.tsx index a9bc567f..f9e969d6 100644 --- a/src/webui/app/routes/workspace/time-plans/$id.tsx +++ b/src/webui/app/routes/workspace/time-plans/$id.tsx @@ -8,6 +8,8 @@ import type { import { ApiError, RecurringTaskPeriod, + TimePlanActivityFeasability, + TimePlanActivityKind, TimePlanActivityTarget, WorkspaceFeature, } from "@jupiter/webapi-client"; @@ -52,6 +54,7 @@ import { BranchPanel } from "~/components/infra/layout/branch-panel"; import { NestingAwareBlock } from "~/components/infra/layout/nesting-aware-block"; import { FilterFewOptions, + FilterManyOptions, NavMultipleCompact, NavSingle, SectionActions, @@ -225,10 +228,21 @@ export default function TimePlanView() { const [selectedView, setSelectedView] = useState( inferDefaultSelectedView(topLevelInfo.workspace, loaderData.timePlan) ); + const [selectedKinds, setSelectedKinds] = useState( + [] + ); + const [selectedFeasabilities, setSelectedFeasabilities] = useState< + TimePlanActivityFeasability[] + >([]); + const [selectedDoneness, setSelectedDoneness] = useState([]); + useEffect(() => { setSelectedView( inferDefaultSelectedView(topLevelInfo.workspace, loaderData.timePlan) ); + setSelectedKinds([]); + setSelectedFeasabilities([]); + setSelectedDoneness([]); }, [topLevelInfo, loaderData]); const sortedProjects = sortProjectsByTreeOrder(loaderData.allProjects || []); @@ -243,7 +257,6 @@ export default function TimePlanView() { enableArchiveButton={inputsEnabled} returnLocation="/workspace/time-plans" > -
    @@ -355,6 +368,45 @@ export default function TimePlanView() { (selected) => setSelectedView(selected) ), ]} + extraActions={[ + FilterManyOptions( + "Kind", + [ + { value: TimePlanActivityKind.FINISH, text: "Finish" }, + { + value: TimePlanActivityKind.MAKE_PROGRESS, + text: "Make Progress", + }, + ], + setSelectedKinds + ), + FilterManyOptions( + "Feasability", + [ + { + value: TimePlanActivityFeasability.MUST_DO, + text: "Must Do", + }, + { + value: TimePlanActivityFeasability.NICE_TO_HAVE, + text: "Nice to Have", + }, + { + value: TimePlanActivityFeasability.STRETCH, + text: "Stretch", + }, + ], + setSelectedFeasabilities + ), + FilterManyOptions( + "Done", + [ + { value: true, text: "Done" }, + { value: false, text: "Not Done" }, + ], + setSelectedDoneness + ), + ]} /> } > @@ -366,6 +418,9 @@ export default function TimePlanView() { inboxTasksByRefId={targetInboxTasksByRefId} bigPlansByRefId={targetBigPlansByRefId} activityDoneness={loaderData.activityDoneness} + filterKind={selectedKinds} + filterFeasability={selectedFeasabilities} + filterDoneness={selectedDoneness} /> )} @@ -411,6 +466,9 @@ export default function TimePlanView() { inboxTasksByRefId={targetInboxTasksByRefId} bigPlansByRefId={targetBigPlansByRefId} activityDoneness={loaderData.activityDoneness} + filterKind={selectedKinds} + filterFeasability={selectedFeasabilities} + filterDoneness={selectedDoneness} /> ); @@ -479,7 +537,7 @@ export default function TimePlanView() { )}
    - + @@ -503,6 +561,9 @@ interface ActivityListProps { inboxTasksByRefId: Map; bigPlansByRefId: Map; activityDoneness: Record; + filterKind: TimePlanActivityKind[]; + filterFeasability: TimePlanActivityFeasability[]; + filterDoneness: boolean[]; } function ActivityList(props: ActivityListProps) { @@ -514,23 +575,46 @@ function ActivityList(props: ActivityListProps) { return ( - {sortedActivities.map((entry) => ( - - ))} + {sortedActivities.map((entry) => { + if ( + props.filterKind.length > 0 && + !props.filterKind.includes(entry.kind) + ) { + return null; + } + + if ( + props.filterFeasability.length > 0 && + !props.filterFeasability.includes(entry.feasability) + ) { + return null; + } + + if ( + props.filterDoneness.length > 0 && + !props.filterDoneness.includes(props.activityDoneness[entry.ref_id]) + ) { + return null; + } + + return ( + + ); + })} ); }