Skip to content

Commit

Permalink
SY-1221 Temporarily Focus a Layout (#834)
Browse files Browse the repository at this point in the history
* [console] - experiments with focusing

* [console] - ability to temp focus a layout

* [client/py] - removed file

* [console] - updated overview page

* [console] - improved collapse and expand tooling for multiple windows

* [console] - minor aesthetic adjustments

* [console] - adjusted tab selector

* [console] - adjustements to portal based rendering

* style: Remove dead code + more descriptive variable names

* [pluto] - added 'double' option to triggers to allow for detecting double press/click

* [console] - consolidated Mosaic component

* [console] - fixed Control + R to rename

---------

Co-authored-by: pjdotson <patrick@synnaxlabs.com>
  • Loading branch information
emilbon99 and pjdotson committed Sep 26, 2024
1 parent 6f824b2 commit 674825d
Show file tree
Hide file tree
Showing 96 changed files with 2,181 additions and 634 deletions.
10 changes: 8 additions & 2 deletions console/src/cluster/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ export const List = (): ReactElement => {
);

return (
<Align.Pack className={CSS.B("cluster-list")} direction="y">
<Align.Pack direction="x" justify="spaceBetween" size="large" grow>
<Align.Pack borderShade={4} className={CSS.B("cluster-list")} direction="y">
<Align.Pack
borderShade={4}
direction="x"
justify="spaceBetween"
size="large"
grow
>
<Align.Space
className={CSS.B("cluster-list-title")}
direction="y"
Expand Down
15 changes: 12 additions & 3 deletions console/src/components/nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// included in the file licenses/APL.txt.

import { Menu as PMenu, Nav } from "@synnaxlabs/pluto";
import { CSS as PCSS } from "@synnaxlabs/pluto";
import { Text } from "@synnaxlabs/pluto/text";
import { location } from "@synnaxlabs/x";
import { type ReactElement } from "react";
Expand All @@ -26,15 +27,23 @@ export const NAV_DRAWERS: Layout.NavDrawerItem[] = [
Task.Toolbar,
];

interface NavMenuProps extends Omit<PMenu.MenuProps, "children"> {
children: Layout.NavMenuItem[];
activeItem?: Layout.NavDrawerItem;
}

export const NavMenu = ({
children,
activeItem,
...props
}: {
children: Layout.NavMenuItem[];
} & Omit<PMenu.MenuProps, "children">): ReactElement => (
}: NavMenuProps): ReactElement => (
<PMenu.Menu {...props}>
{children.map(({ key, tooltip, icon }) => (
<PMenu.Item.Icon
className={CSS(
CSS.BE("main-nav", "item"),
PCSS.selected(activeItem?.key === key),
)}
key={key}
itemKey={key}
size="large"
Expand Down
1 change: 1 addition & 0 deletions console/src/hardware/ni/task/AnalogRead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const configureAnalogReadLayout = (
key: id.id(),
type: ANALOG_READ_TYPE,
windowKey: ANALOG_READ_TYPE,
icon: "Logo.NI",
location: "mosaic",
args,
});
Expand Down
1 change: 1 addition & 0 deletions console/src/hardware/ni/task/DigitalRead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const configureDigitalReadLayout = (
name: "Configure NI Digital Read Task",
type: DIGITAL_READ_TYPE,
key: id.id(),
icon: "Logo.NI",
windowKey: DIGITAL_READ_TYPE,
location: "mosaic",
args,
Expand Down
1 change: 1 addition & 0 deletions console/src/hardware/ni/task/DigitalWrite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const configureDigitalWriteLayout = (
): Layout.State<TaskLayoutArgs<DigitalWritePayload>> => ({
name: "Configure NI Digital Write Task",
key: id.id(),
icon: "Logo.NI",
type: DIGITAL_WRITE_TYPE,
windowKey: DIGITAL_WRITE_TYPE,
location: "mosaic",
Expand Down
1 change: 1 addition & 0 deletions console/src/hardware/opc/task/ReadTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const configureReadLayout = (
key: uuid(),
type: READ_TYPE,
windowKey: READ_TYPE,
icon: "Logo.OPC",
location: "mosaic",
window: {
resizable: true,
Expand Down
1 change: 1 addition & 0 deletions console/src/hardware/opc/task/WriteTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const configureWriteLayout = (
type: WRITE_TYPE,
windowKey: WRITE_TYPE,
location: "mosaic",
icon: "Logo.OPC",
window: {
resizable: true,
size: { width: 1200, height: 900 },
Expand Down
33 changes: 24 additions & 9 deletions console/src/layout/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { memo, type ReactElement } from "react";

import { useOptionalRenderer } from "@/layout/context";
import { useRemover } from "@/layout/hooks";
import { useSelect } from "@/layout/selectors";
import { useSelect, useSelectFocused } from "@/layout/selectors";

/** LayoutContentProps are the props for the LayoutContent component. */
export interface ContentProps {
layoutKey: string;
forceHidden?: boolean;
}

/**
Expand All @@ -25,12 +26,26 @@ export interface ContentProps {
* @param props.layoutKey - The key of the layout to render. The key must exist in the store,
* and a renderer for the layout type must be registered in the LayoutContext.
*/
export const Content = memo(({ layoutKey }: ContentProps): ReactElement | null => {
const p = useSelect(layoutKey);
const handleClose = useRemover(layoutKey);
const type = p?.type ?? "";
const Renderer = useOptionalRenderer(type);
if (Renderer == null) throw new Error(`layout renderer ${type} not found`);
return <Renderer key={layoutKey} layoutKey={layoutKey} onClose={handleClose} />;
});
export const Content = memo(
({ layoutKey, forceHidden }: ContentProps): ReactElement | null => {
const layout = useSelect(layoutKey);
const [, focused] = useSelectFocused();
const handleClose = useRemover(layoutKey);
const type = layout?.type ?? "";
const Renderer = useOptionalRenderer(type);
if (Renderer == null) throw new Error(`layout renderer ${type} not found`);
const isFocused = focused === layoutKey;
let visible = focused == null || isFocused;
if (forceHidden) visible = false;
return (
<Renderer
key={layoutKey}
layoutKey={layoutKey}
onClose={handleClose}
visible={visible}
focused={isFocused}
/>
);
},
);
Content.displayName = "LayoutContent";
184 changes: 184 additions & 0 deletions console/src/layout/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { MAIN_WINDOW } from "@synnaxlabs/drift";
import { useSelectWindowKey } from "@synnaxlabs/drift/react";
import { Icon } from "@synnaxlabs/media";
import { Menu, Mosaic, Text } from "@synnaxlabs/pluto";
import { direction } from "@synnaxlabs/x";
import { type FC, type ReactElement } from "react";
import { useDispatch, useStore } from "react-redux";

import { usePlacer, useRemover } from "@/layout/hooks";
import { useSelectMosaic } from "@/layout/selectors";
import {
createMosaicWindow,
moveMosaicTab,
setFocus,
splitMosaicNode,
} from "@/layout/slice";

export interface FocusMenuItemProps {
layoutKey: string;
}

export const FocusMenuItem = ({ layoutKey }: FocusMenuItemProps): ReactElement => {
const dispatch = useDispatch();
const windowKey = useSelectWindowKey() as string;
return (
<Menu.Item
itemKey="focus"
startIcon={<Icon.Focus />}
onClick={() => dispatch(setFocus({ windowKey: windowKey, key: layoutKey }))}
trigger={["Control", "F"]}
>
Focus
</Menu.Item>
);
};

export const useOpenInNewWindow = () => {
const dispatch = useDispatch();
const placer = usePlacer();
return (layoutKey: string) => {
const { key } = placer(createMosaicWindow({}));
dispatch(
moveMosaicTab({
windowKey: key,
key: 1,
tabKey: layoutKey,
loc: "center",
}),
);
};
};

export const useMoveIntoMainWindow = () => {
const store = useStore();
return (layoutKey: string) => {
store.dispatch(
moveMosaicTab({
windowKey: MAIN_WINDOW,
tabKey: layoutKey,
loc: "center",
}),
);
};
};

export const OpenInNewWindowMenuItem = ({
layoutKey,
}: FocusMenuItemProps): ReactElement | null => {
const openInNewWindow = useOpenInNewWindow();
const isMain = useSelectWindowKey() === MAIN_WINDOW;
if (!isMain) return null;
return (
<Menu.Item
itemKey="openInNewWindow"
startIcon={<Icon.OpenInNewWindow />}
onClick={() => openInNewWindow(layoutKey)}
trigger={["Control", "O"]}
>
Open in New Window
</Menu.Item>
);
};

export const MoveToMainWindowMenuItem = ({
layoutKey,
}: FocusMenuItemProps): ReactElement | null => {
const moveIntoMainWindow = useMoveIntoMainWindow();
const windowKey = useSelectWindowKey();
if (windowKey === MAIN_WINDOW) return null;
return (
<Menu.Item
itemKey="moveIntoMainWindow"
startIcon={<Icon.OpenInNewWindow />}
onClick={() => moveIntoMainWindow(layoutKey)}
>
Move to Main Window
</Menu.Item>
);
};

export const CloseMenuItem = ({ layoutKey }: FocusMenuItemProps): ReactElement => {
const remove = useRemover();
return (
<Menu.Item
itemKey="close"
startIcon={<Icon.Close />}
onClick={() => remove(layoutKey)}
trigger={["Control", "W"]}
>
Close
</Menu.Item>
);
};

export const RenameMenuItem = ({ layoutKey }: FocusMenuItemProps): ReactElement => (
<Menu.Item
itemKey="rename"
startIcon={<Icon.Rename />}
onClick={() => Text.edit(`pluto-tab-${layoutKey}`)}
trigger={["Control", "R"]}
>
Rename
</Menu.Item>
);

const splitMenuItemFactory = (
direction: direction.Direction,
): FC<FocusMenuItemProps & { children?: ReactElement }> => {
const C = ({
layoutKey,
children,
}: FocusMenuItemProps & { children?: ReactElement }) => {
const dispatch = useDispatch();
const [windowKey, mosaic] = useSelectMosaic();
const canSplit = Mosaic.canSplit(mosaic, layoutKey);
if (!canSplit) return null;
return (
<>
{children}
<Menu.Item
itemKey={`split${direction}`}
startIcon={direction === "x" ? <Icon.SplitX /> : <Icon.SplitY />}
onClick={() =>
dispatch(splitMosaicNode({ windowKey, tabKey: layoutKey, direction }))
}
>
Split {direction === "x" ? "Horizontally" : "Vertically"}
</Menu.Item>
</>
);
};
C.displayName = `Split${direction.toUpperCase()}MenuItem`;
return C;
};
export const SplitXMenuItem = splitMenuItemFactory("x");
export const SplitYMenuItem = splitMenuItemFactory("y");

export interface MenuItems {
layoutKey: string;
}

export const MenuItems = ({ layoutKey }: MenuItems): ReactElement => (
<>
<RenameMenuItem layoutKey={layoutKey} />
<CloseMenuItem layoutKey={layoutKey} />
<Menu.Divider />
<FocusMenuItem layoutKey={layoutKey} />
<OpenInNewWindowMenuItem layoutKey={layoutKey} />
<MoveToMainWindowMenuItem layoutKey={layoutKey} />
<SplitXMenuItem layoutKey={layoutKey}>
<Menu.Divider />
</SplitXMenuItem>
<SplitYMenuItem layoutKey={layoutKey} />
</>
);
67 changes: 67 additions & 0 deletions console/src/layout/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2024 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import "@/layout/Modals.css";

import { Icon } from "@synnaxlabs/media";
import { Breadcrumb, Button, Menu, Modal as Core, Nav } from "@synnaxlabs/pluto";
import { CSSProperties } from "react";

import { Content } from "@/layout/Content";
import { State, WindowProps } from "@/layout/slice";
import { DefaultContextMenu } from "@/layout/Window";

const layoutCSS = (window?: WindowProps): CSSProperties => ({
width: "100%",
height: "100%",
maxWidth: window?.size?.width,
maxHeight: window?.size?.height,
minWidth: window?.minSize?.width,
minHeight: window?.minSize?.height,
});

interface ModalProps {
state: State;
remove: (key: string) => void;
centered?: boolean;
root?: string;
}

export const Modal = ({ state, remove, centered, root }: ModalProps) => {
const { key, name, window, icon } = state;
const menuProps = Menu.useContextMenu();
return (
<Menu.ContextMenu menu={() => <DefaultContextMenu />} {...menuProps}>
<Core.Modal
key={key}
centered={centered}
visible
close={() => remove(key)}
style={layoutCSS(window)}
root={root}
>
{window?.navTop && (
<Nav.Bar location="top" size="6rem">
{(window?.showTitle ?? true) && (
<Nav.Bar.Start style={{ paddingLeft: "2rem" }}>
<Breadcrumb.Breadcrumb icon={icon}>{name}</Breadcrumb.Breadcrumb>
</Nav.Bar.Start>
)}
<Nav.Bar.End style={{ paddingRight: "1rem" }}>
<Button.Icon onClick={() => remove(key)} size="small">
<Icon.Close style={{ color: "var(--pluto-gray-l8)" }} />
</Button.Icon>
</Nav.Bar.End>
</Nav.Bar>
)}
<Content layoutKey={key} />
</Core.Modal>
</Menu.ContextMenu>
);
};
Loading

0 comments on commit 674825d

Please sign in to comment.