Skip to content

Commit

Permalink
Merged branch feature/better-view-of-big-plans-in-timeplans into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
horia141 committed Jun 21, 2024
1 parent 714bfd9 commit 4fc28e8
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
TimePlanActivityTarget,
)
from jupiter.core.framework.base.entity_id import EntityId
from jupiter.core.framework.errors import InputValidationError
from jupiter.core.framework.use_case import (
ProgressReporter,
)
Expand Down Expand Up @@ -69,6 +70,9 @@ async def _perform_transactional_mutation(
args: TimePlanAssociateWithActivitiesArgs,
) -> TimePlanAssociateWithActivitiesResult:
"""Execute the command's actions."""
if len(args.activity_ref_ids) == 0:
raise InputValidationError("You must specifiy some activities")

time_plan = await uow.get_for(TimePlan).load_by_id(args.ref_id)

activities = await uow.get_for(TimePlanActivity).find_all(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from jupiter.core.domain.time_plans.time_plan_activity_kind import TimePlanActivityKind
from jupiter.core.framework.base.entity_id import EntityId
from jupiter.core.framework.errors import InputValidationError
from jupiter.core.framework.use_case import (
ProgressReporter,
)
Expand Down Expand Up @@ -59,6 +60,9 @@ async def _perform_transactional_mutation(
args: TimePlanAssociateWithBigPlansArgs,
) -> TimePlanAssociateWithBigPlansResult:
"""Execute the command's actions."""
if len(args.big_plan_ref_ids) == 0:
raise InputValidationError("You must specifiy some big plans")

workspace = context.workspace

time_plan = await uow.get_for(TimePlan).load_by_id(args.ref_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from jupiter.core.domain.time_plans.time_plan_activity_kind import TimePlanActivityKind
from jupiter.core.framework.base.entity_id import EntityId
from jupiter.core.framework.errors import InputValidationError
from jupiter.core.framework.use_case import (
ProgressReporter,
)
Expand Down Expand Up @@ -67,6 +68,9 @@ async def _perform_transactional_mutation(
args: TimePlanAssociateWithInboxTasksArgs,
) -> TimePlanAssociateWithInboxTasksResult:
"""Execute the command's actions."""
if len(args.inbox_task_ref_ids) == 0:
raise InputValidationError("You must specifiy some inbox tasks")

workspace = context.workspace

time_plan = await uow.get_for(TimePlan).load_by_id(args.ref_id)
Expand Down
253 changes: 239 additions & 14 deletions src/webui/app/components/infra/section-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface NavSingleDesc {
kind: "nav-single";
text: string;
link: string;
highlight?: boolean;
icon?: JSX.Element;
gatedOn?: WorkspaceFeature;
}
Expand All @@ -42,6 +43,21 @@ interface NavMultipleDesc {
navs: Array<NavSingleDesc>;
}

interface ActionSingleDesc {
kind: "action-single";
text: string;
value: string;
highlight?: boolean;
icon?: JSX.Element;
gatedOn?: WorkspaceFeature;
}

interface ActionMultipleDesc {
kind: "action-multiple";
approach: "spread" | "compact";
actions: Array<ActionSingleDesc>;
}

interface FilterOption<K> {
value: K;
text: string;
Expand All @@ -66,23 +82,17 @@ interface FilterManyOptionsDesc<K> {
}

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
| NavSingleDesc // A single button, as a navigation
| NavMultipleDesc // A group of buttons, as a navigation
| ActionSingleDesc // A single button, as an action
| ActionMultipleDesc // A group of buttons, as an action
| FilterFewOptionsDesc<any> // A group to filter on, can be a navigation or a callback
| FilterManyOptionsDesc<any>; // A group to filter on, with many options

export function NavSingle(
text: string,
link: string,
icon?: JSX.Element,
gatedOn?: WorkspaceFeature
): NavSingleDesc {
export function NavSingle(desc: Omit<NavSingleDesc, "kind">): NavSingleDesc {
return {
kind: "nav-single",
text,
link,
icon,
gatedOn,
...desc,
};
}

Expand All @@ -96,6 +106,25 @@ export function NavMultipleSpread(
};
}

export function ActionSingle(
desc: Omit<ActionSingleDesc, "kind">
): ActionSingleDesc {
return {
kind: "action-single",
...desc,
};
}

export function ActionMultipleSpread(
...actions: Array<ActionSingleDesc>
): ActionMultipleDesc {
return {
kind: "action-multiple",
approach: "spread",
actions: actions,
};
}

export function NavMultipleCompact(
...navs: Array<NavSingleDesc>
): NavMultipleDesc {
Expand All @@ -107,12 +136,13 @@ export function NavMultipleCompact(
}

export function FilterFewOptions<K>(
defaultOption: K,
options: Array<FilterOption<K>>,
onSelect: (selected: K) => void
): FilterFewOptionsDesc<K> {
return {
kind: "filter-few-options",
defaultOption: options[0].value,
defaultOption: defaultOption,
options: options,
onSelect: onSelect,
hideIfOneOption: true,
Expand Down Expand Up @@ -261,6 +291,25 @@ function ActionView(props: ActionViewProps) {
/>
);

case "action-single":
return (
<ActionSingleView
topLevelInfo={props.topLevelInfo}
inputsEnabled={props.inputsEnabled}
action={props.action}
/>
);

case "action-multiple":
return (
<ActionMultipleView
topLevelInfo={props.topLevelInfo}
inputsEnabled={props.inputsEnabled}
orientation={props.orientation}
action={props.action}
/>
);

case "filter-few-options":
return (
<FilterFewOptionsView
Expand Down Expand Up @@ -359,7 +408,7 @@ function NavMultipleSpreadView(props: NavMultipleViewProps) {
function NavMultipleCompactView(props: NavMultipleViewProps) {
const [open, setOpen] = useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const theme = useTheme();
const isBigScreen = useBigScreen();

Expand Down Expand Up @@ -456,6 +505,182 @@ function NavMultipleCompactView(props: NavMultipleViewProps) {
);
}

interface ActionSingleViewProps {
topLevelInfo: TopLevelInfo;
inputsEnabled: boolean;
action: ActionSingleDesc;
}

function ActionSingleView(props: ActionSingleViewProps) {
if (props.action.gatedOn) {
const workspace = props.topLevelInfo.workspace;
if (!isWorkspaceFeatureAvailable(workspace, props.action.gatedOn)) {
return <></>;
}
}

return (
<Button
variant={props.action.highlight ? "contained" : "outlined"}
disabled={!props.inputsEnabled}
startIcon={props.action.icon}
type="submit"
name="intent"
value={props.action.value}
>
{props.action.text}
</Button>
);
}

interface ActionMultipleViewProps {
topLevelInfo: TopLevelInfo;
inputsEnabled: boolean;
orientation: "horizontal" | "vertical";
action: ActionMultipleDesc;
}

function ActionMultipleView(props: ActionMultipleViewProps) {
switch (props.action.approach) {
case "spread":
return <ActionMultipleSpreadView {...props} />;
case "compact":
return <ActionMultipleCompactView {...props} />;
}
}

function ActionMultipleSpreadView(props: ActionMultipleViewProps) {
return (
<ButtonGroup orientation={props.orientation}>
{props.action.actions.map((action, index) => {
if (action.gatedOn) {
const workspace = props.topLevelInfo.workspace;
if (!isWorkspaceFeatureAvailable(workspace, action.gatedOn)) {
return (
<React.Fragment key={`action-multiple-${index}`}></React.Fragment>
);
}
}

return (
<Button
key={`action-multiple-${index}`}
variant={action.highlight ? "contained" : "outlined"}
disabled={!props.inputsEnabled}
startIcon={action.icon}
type="submit"
name="intent"
value={action.value}
>
{action.text}
</Button>
);
})}
</ButtonGroup>
);
}

function ActionMultipleCompactView(props: ActionMultipleViewProps) {
const [open, setOpen] = useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(0);
const theme = useTheme();
const isBigScreen = useBigScreen();

const realActions: ActionSingleDesc[] = [];
for (const action of props.action.actions) {
if (action.gatedOn) {
const workspace = props.topLevelInfo.workspace;
if (!isWorkspaceFeatureAvailable(workspace, action.gatedOn)) {
continue;
}
}
realActions.push(action);
}

function handleMenuItemClick(
event: React.MouseEvent<HTMLElement, MouseEvent>,
index: number
) {
setSelectedIndex(index);
setOpen(false);
}

function handleClose(event: Event) {
if (
anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement)
) {
return;
}

setOpen(false);
}

if (realActions.length === 0) {
return <></>;
}

return (
<>
<ButtonGroup ref={anchorRef}>
<Button
disabled={!props.inputsEnabled}
startIcon={realActions[selectedIndex].icon}
type="submit"
name="intent"
value={realActions[selectedIndex].value}
>
{realActions[selectedIndex].text}
</Button>
<Button
size="small"
disabled={!props.inputsEnabled}
onClick={() => setOpen((prevOpen) => !prevOpen)}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
sx={{
zIndex: theme.zIndex.appBar + 20,
backgroundColor: theme.palette.background.paper,
}}
open={open}
anchorEl={anchorRef.current}
disablePortal={!isBigScreen}
transition
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" autoFocusItem>
{realActions.map((option, index) => (
<MenuItem
key={`action-multiple-${index}`}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option.text}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</>
);
}

interface FilterFewOptionsViewProps<K> {
topLevelInfo: TopLevelInfo;
inputsEnabled: boolean;
Expand Down
Loading

0 comments on commit 4fc28e8

Please sign in to comment.