Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SY-1221 Temporarily Focus a Layout #834

Merged
merged 13 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Empty file added console/src/fs/useFileDrop.ts
Empty file.
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 p = useSelect(layoutKey);
const [, focused] = useSelectFocused();
const handleClose = useRemover(layoutKey);
const type = p?.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";
179 changes: 179 additions & 0 deletions console/src/layout/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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 { FC, 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 d = useDispatch();
const windowKey = useSelectWindowKey() as string;
return (
<Menu.Item
itemKey="focus"
startIcon={<Icon.Focus />}
onClick={() => d(setFocus({ windowKey: windowKey, key: layoutKey }))}
trigger={["Control", "F"]}
>
Focus
</Menu.Item>
);
};

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

export const useMoveIntoMainWindow = () => {
const s = useStore();
return (layoutKey: string) => {
s.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 => {
return (
<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 d = 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={() =>
d(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 => {
return (
<>
<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
Loading