Skip to content

Commit

Permalink
Issue 1051 UI tabs issue (#1054)
Browse files Browse the repository at this point in the history
* fix bug whereby MovingWinow created with negative array length

* fix delete Tab issue

* display full placeholder when last main tab removed

* add layout query

* fix tab label generation

* ensure title is sticky
  • Loading branch information
heswell authored Dec 7, 2023
1 parent 70fdd01 commit 13570c0
Show file tree
Hide file tree
Showing 24 changed files with 248 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LayoutChangeReason,
layoutFromJson,
LayoutJSON,
layoutQuery,
layoutReducer,
LayoutReducerAction,
layoutToJSON,
Expand Down Expand Up @@ -125,6 +126,9 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => {
serializeState(state.current, getLayoutChangeReason(action));
break;
}
case "query":
return layoutQuery(action.query, action.path, state.current);

default: {
dispatchLayoutAction(action);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, Dispatch, ReactElement } from "react";
import {
DragStartAction,
LayoutReducerAction,
QueryAction,
SaveAction,
} from "../layout-reducer";

Expand All @@ -11,7 +12,7 @@ const unconfiguredLayoutProviderDispatch: LayoutProviderDispatch = (action) =>
);

export type LayoutProviderDispatch = Dispatch<
LayoutReducerAction | SaveAction | DragStartAction
LayoutReducerAction | SaveAction | DragStartAction | QueryAction
>;

export interface LayoutProviderContextProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { rectTuple, uuid } from "@finos/vuu-utils";
import React, { ReactElement } from "react";
import { DropPos } from "../drag-drop";
import { DropTarget } from "../drag-drop/DropTarget";
import { TabLabelFactory } from "../stack";
import { getProp, getProps, nextStep, resetPath, typeOf } from "../utils";
import {
createPlaceHolder,
Expand All @@ -13,7 +14,11 @@ import {
wrapIntrinsicSizeComponentWithFlexbox,
} from "./flexUtils";
import { LayoutModel } from "./layoutTypes";
import { getManagedDimension, LayoutProps } from "./layoutUtils";
import {
getDefaultTabLabel,
getManagedDimension,
LayoutProps,
} from "./layoutUtils";

type insertionPosition = "before" | "after";

Expand Down Expand Up @@ -68,13 +73,18 @@ export function insertIntoContainer(
return React.cloneElement(container, { active }, children);
}

function getDefaultTitle(index: number, container: ReactElement) {
if (typeOf(container) === "Stack") {
return `Tab ${index + 1}`;
} else {
return undefined;
}
}
const getDefaultTitle = (
containerType: string | undefined,
component: ReactElement,
index: number,
existingLabels: string[]
) =>
containerType === "Stack"
? getDefaultTabLabel(component, index, existingLabels)
: undefined;

const getChildrenTitles = (children: ReactElement[]) =>
children.map((child) => child.props.title);

function insertIntoChildren(
container: ReactElement,
Expand All @@ -83,8 +93,15 @@ function insertIntoChildren(
): [number, ReactElement[]] {
const containerPath = getProp(container, "path");
const count = containerChildren?.length;
const { id = uuid(), title = getDefaultTitle(count ?? 0, container) } =
getProps(newComponent);
const {
id = uuid(),
title = getDefaultTitle(
typeOf(container),
newComponent,
count ?? 0,
getChildrenTitles(containerChildren)
),
} = getProps(newComponent);

if (count) {
return [
Expand Down
16 changes: 9 additions & 7 deletions vuu-ui/packages/vuu-layout/src/layout-reducer/layout-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,18 @@ const dropLayoutIntoContainer = (
dropTarget: DropTarget,
newComponent: ReactElement
): ReactElement => {
const {
component: existingComponent,
pos,
clientRect,
dropRect,
} = dropTarget;
const { component, pos, clientRect, dropRect } = dropTarget;
const existingComponent = component as ReactElement;

const existingComponentPath = getProp(existingComponent, "path");

if (existingComponentPath === "0.0") {
return wrap(layoutRoot, existingComponent, newComponent, pos);
return wrap(
layoutRoot,
existingComponent as ReactElement,
newComponent,
pos
);
}

const targetContainer = followPathToParent(
Expand Down
8 changes: 7 additions & 1 deletion vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { LeftNavProps } from "packages/vuu-shell/src";
import { CSSProperties, ReactElement } from "react";
import { DragDropRect, DragInstructions } from "../drag-drop";
import { DropTarget } from "../drag-drop/DropTarget";
Expand Down Expand Up @@ -60,6 +59,7 @@ export const LayoutActionType = {
MAXIMIZE: "maximize",
MINIMIZE: "minimize",
MOVE_CHILD: "move-child",
QUERY: "query",
REMOVE: "remove",
REPLACE: "replace",
RESTORE: "restore",
Expand Down Expand Up @@ -102,6 +102,12 @@ export type MoveChildAction = {
type: typeof LayoutActionType.MOVE_CHILD;
};

export type QueryAction = {
path?: string;
query: string;
type: typeof LayoutActionType.QUERY;
};

export type RemoveAction = {
path?: string;
type: typeof LayoutActionType.REMOVE;
Expand Down
46 changes: 45 additions & 1 deletion vuu-ui/packages/vuu-layout/src/layout-reducer/layoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
isContainer,
isLayoutComponent,
} from "../registry/ComponentRegistry";
import { TabLabelFactory } from "../stack";
import {
getPersistentState,
hasPersistentState,
setPersistentState,
} from "../use-persistent-state";
import { expandFlex, getProps, typeOf } from "../utils";
import { expandFlex, followPathToParent, getProps, typeOf } from "../utils";
import { LayoutJSON, LayoutModel, layoutType } from "./layoutTypes";

export const getManagedDimension = (
Expand Down Expand Up @@ -287,3 +288,46 @@ function serializeValue(value: unknown): any {
return result;
}
}

// This is experimental and the only query we support to start off with is
// PARENT_CONTAINER
export type LayoutQuery = "PARENT_CONTAINER";

export const layoutQuery = (
query: LayoutQuery,
path?: string,
layoutRoot?: ReactElement
) => {
if (path && layoutRoot) {
const parentElement = followPathToParent(layoutRoot, path);
if (parentElement) {
const { id: parentContainerId } = getProps(parentElement);
const parentContainerType = typeOf(parentElement);
return {
parentContainerId,
parentContainerType,
};
}
return {
parentContainerType: "Stack",
parentContainerId: "blah",
};
}
};

export const getDefaultTabLabel: TabLabelFactory = (
component,
tabIndex,
existingLabels = []
): string => {
let label = component.props?.title ?? component.props?.["data-tab-title"];
if (label) {
return label;
} else {
let count = tabIndex;
do {
label = `Tab ${++count}`;
} while (existingLabels.includes(label));
return label;
}
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { ReactElement } from "react";
import { createPlaceHolder } from "./flexUtils";
import { layoutFromJson } from "./layoutUtils";
import { swapChild } from "./replace-layout-element";

import {
Expand All @@ -21,7 +22,12 @@ export function removeChild(layoutRoot: ReactElement, { path }: RemoveAction) {
return layoutRoot;
}
const { children } = getProps(targetParent);
if (children.length > 1 && allOtherChildrenArePlaceholders(children, path)) {
if (
// this is very specific to explicitly sized components
children.length > 1 &&
typeOf(targetParent) !== "Stack" &&
allOtherChildrenArePlaceholders(children, path)
) {
const {
style: { flexBasis, display, flexDirection, ...style },
} = getProps(targetParent);
Expand Down Expand Up @@ -66,7 +72,7 @@ function _removeChild(
): ReactElement {
const props = getProps(container);
const { children: componentChildren, path, preserve } = props;
let { active } = props;
let { active, id: containerId } = props;
const { idx, finalStep } = nextStep(path, getProp(child, "path"));
const type = typeOf(container) as string;
let children = componentChildren.slice() as ReactElement[];
Expand All @@ -81,9 +87,21 @@ function _removeChild(
if (children.length === 0 && preserve && type === "Stack") {
const {
path,
style: { flexBasis, display, flexDirection, ...style },
style: { flexBasis },
} = getProps(child);
const placeHolder = createPlaceHolder(path, flexBasis);
const placeHolder =
containerId === "main-tabs"
? layoutFromJson(
{
props: {
style: { flexGrow: 1, flexShrink: 1, flexBasis },
title: "Tab 1",
},
type: "Placeholder",
},
path
)
: createPlaceHolder(path, flexBasis);
children.push(placeHolder);
} else if (
children.length === 1 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const isHtmlElement = (component: LayoutModel) => {

export function wrap(
container: ReactElement,
existingComponent: any,
existingComponent: ReactElement,
newComponent: any,
pos: DropPos,
clientRect?: DropTarget["clientRect"],
Expand Down Expand Up @@ -109,7 +109,7 @@ function wrapFlexComponent(
pos: DropPos
) {
const { version = 0 } = getProps(newComponent);
const existingComponentPath = getProp(existingComponent, "path");
const { path: existingComponentPath, title } = getProps(existingComponent);
const {
type,
flexDirection,
Expand All @@ -134,6 +134,7 @@ function wrapFlexComponent(
const resizeProp = isHtmlElement(existingComponent)
? "data-resizeable"
: "resizeable";

const existingComponentProps = {
[resizeProp]: true,
style: existingComponentStyle,
Expand Down Expand Up @@ -161,6 +162,7 @@ function wrapFlexComponent(
...splitterSize,
...showTabs,
style,
title,
resizeable: getProp(existingComponent, "resizeable"),
} as LayoutProps,
targetFirst
Expand Down
1 change: 1 addition & 0 deletions vuu-ui/packages/vuu-layout/src/layout-view/View.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
flex-direction: var(--vuuView-flexDirection, column);
flex-wrap: var(--vuuView-flex-wrap, nowrap);
flex: 1;
justify-content: var(--vuuView-justify, flex-start);
overflow: hidden;
position: relative;
}
Expand Down
6 changes: 4 additions & 2 deletions vuu-ui/packages/vuu-layout/src/layout-view/ViewContext.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { SyntheticEvent, useContext } from "react";
import { ViewAction } from "./viewTypes";

export type QueryReponse = { [key: string]: unknown };

export type ViewDispatch = <Action extends ViewAction = ViewAction>(
action: Action,
evt?: SyntheticEvent
) => Promise<boolean | void>;
) => Promise<boolean | QueryReponse | void>;

/**
* This API is available to any Feature hosted within Vuu (as all Features are wrapped
Expand All @@ -13,7 +15,7 @@ export type ViewDispatch = <Action extends ViewAction = ViewAction>(
*/
export interface ViewContextAPI {
/**
* disdpatcher for View actions. These are a subset of LayoutActions, specifically for
* dispatcher for View actions. These are a subset of LayoutActions, specifically for
* View manipulation
*/
dispatch?: ViewDispatch | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { useLayoutProviderDispatch } from "../layout-provider";
import { DragStartAction } from "../layout-reducer";
import { usePersistentState } from "../use-persistent-state";
import { ViewDispatch } from "./ViewContext";
import { QueryReponse, ViewDispatch } from "./ViewContext";
import { ViewAction } from "./viewTypes";

export type ContributionLocation = "post-title" | "pre-title";
Expand Down Expand Up @@ -90,7 +90,7 @@ export const useViewActionDispatcher = (
async <A extends ViewAction = ViewAction>(
action: A,
evt?: SyntheticEvent
): Promise<boolean | void> => {
): Promise<boolean | QueryReponse | void> => {
const { type } = action;
switch (type) {
case "maximize":
Expand All @@ -105,6 +105,13 @@ export const useViewActionDispatcher = (
return updateContributions(action.location, action.content);
case "remove-toolbar-contribution":
return clearContributions();
case "query":
return dispatchLayoutAction({
type,
path: action.path,
query: action.query,
});
return;
default: {
return undefined;
}
Expand Down
2 changes: 2 additions & 0 deletions vuu-ui/packages/vuu-layout/src/layout-view/viewTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
MaximizeAction,
MinimizeAction,
MousedownViewAction,
QueryAction,
RemoveAction,
RemoveToolbarContributionViewAction,
RestoreAction,
Expand All @@ -15,6 +16,7 @@ export type ViewAction =
| MaximizeAction
| MinimizeAction
| MousedownViewAction
| QueryAction
| RemoveAction
| RestoreAction
| TearoutAction
Expand Down
Loading

0 comments on commit 13570c0

Please sign in to comment.