Skip to content

Commit

Permalink
chore: canvas template added (#4227)
Browse files Browse the repository at this point in the history
* chore: canvas template added
* chore: set pentaho as app default theme
* chore: add missing theme to HvBaseTheme
* chore: leverage css vars inside base node
* chore: clean up base hook sample
  • Loading branch information
MEsteves22 authored Jul 15, 2024
1 parent fd75cf6 commit 2cff9c6
Show file tree
Hide file tree
Showing 35 changed files with 1,590 additions and 869 deletions.
2 changes: 1 addition & 1 deletion apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const App = () => {
return (
<HvProvider
themes={[ds3, ds5, pentahoPlus]}
theme="ds5"
theme="pentahoPlus"
rootElementId="hv-root"
cssTheme="scoped"
>
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/common/Container/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const Container = ({
const spacing = useHeaderSpacing();

return (
<div className="flex pb-6 min-h-screen" style={{ paddingTop: spacing }}>
<div className="flex pb-2 min-h-screen" style={{ paddingTop: spacing }}>
<HvContainer component="main" maxWidth={maxWidth} {...others}>
<Suspense fallback={<Loading {...loadingProps} />}>{children}</Suspense>
</HvContainer>
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/generator/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const CodeEditor = ({

const onResetHandler = () => {
updateCustomTheme({}, { isReset: true, updateThemeChanges: false });
changeTheme("ds5", "dawn");
changeTheme("pentahoPlus", "dawn");
};

const codeChangedHandler = (code?: string) => {
Expand All @@ -69,7 +69,7 @@ const CodeEditor = ({
try {
const parsed = JSON5.parse(snippet);
if (customTheme.base !== parsed.base) {
if (parsed.base === "ds3" || parsed.base === "ds5") {
if (["pentahoPlus", "ds3", "ds5"].includes(parsed.base)) {
changeTheme(parsed.base, selectedMode);
updateCustomTheme(
{ ...parsed },
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/generator/GeneratorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const GeneratorContext = createContext<GeneratorContextValue | null>(
null,
);

const initialTheme = createTheme({ name: "customTheme", base: "ds5" });
const initialTheme = createTheme({ name: "customTheme", base: "pentahoPlus" });

const GeneratorProvider = ({ children }: { children: React.ReactNode }) => {
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -80,7 +80,7 @@ const GeneratorProvider = ({ children }: { children: React.ReactNode }) => {
if (isReset) {
setThemeChanges({});
newTheme = createTheme({
base: "ds5",
base: "pentahoPlus",
name: "customTheme",
});
return newTheme;
Expand Down
5 changes: 5 additions & 0 deletions apps/app/src/lib/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const navigation = [
label: "Kanban Board",
path: "/templates/kanban-board",
},
{
id: "canvas",
label: "Canvas",
path: "/templates/canvas",
},
],
},
{ id: "flow", label: "Flow", path: "/flow" },
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const DetailsView = lazy(() => import("../../../templates/DetailsView"));
const Dashboard = lazy(() => import("../../../templates/Dashboard"));
const Welcome = lazy(() => import("../../../templates/Welcome"));
const KanbanBoard = lazy(() => import("../../../templates/KanbanBoard"));
const Canvas = lazy(() => import("../../../templates/Canvas"));

export const routes: RouteObject[] = [
{
Expand All @@ -29,6 +30,7 @@ export const routes: RouteObject[] = [
{ path: "form", element: <Form /> },
{ path: "details-view", element: <DetailsView /> },
{ path: "kanban-board", element: <KanbanBoard /> },
{ path: "canvas", element: <Canvas /> },
],
},
{ path: "/*", lazy: () => import("~/pages/NotFound") },
Expand Down
34 changes: 34 additions & 0 deletions docs/templates/Canvas.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { css } from "@emotion/css";
import { StoryObj } from "@storybook/react";
import {
canvasPanelClasses,
canvasToolbarClasses,
} from "@hitachivantara/uikit-react-pentaho";

import Canvas from "../../templates/Canvas";
import CanvasRaw from "../../templates/Canvas?raw";

export default {
title: "Templates/Canvas",
};

const classes = {
// Needed to override the styles specific to the app but can't be used in Storybook
root: css({
"& > div": { height: "calc(100vh - 40px)" },
[`& .${canvasToolbarClasses.root}`]: { top: 8 },
[`& .${canvasPanelClasses.root}`]: { top: 8, height: "calc(100% - 8px)" },
}),
};

export const Main: StoryObj = {
parameters: {
docs: {
source: {
code: CanvasRaw,
},
},
},
decorators: [(Story) => <div className={classes.root}>{Story()}</div>],
render: () => <Canvas />,
};
5 changes: 5 additions & 0 deletions docs/templates/TemplateItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ const templates = [
img: "https://i.imgur.com/QQ0WvmN.png",
href: "./?path=/docs/templates-welcome--docs",
},
{
id: "Canvas",
img: "https://i.imgur.com/fViaBkg.png",
href: "./?path=/docs/templates-canvas--docs",
},
];

export const TemplateItems = () => {
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/templates/Canvas/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
createContext,
Dispatch,
SetStateAction,
useContext,
useMemo,
useState,
} from "react";

interface SelectedTable {
id: string;
label: React.ReactNode;
}

interface CanvasContextValue {
selectedTable: string;
setSelectedTable?: Dispatch<SetStateAction<string>>;
openedTables?: SelectedTable[];
setOpenedTables?: Dispatch<SetStateAction<SelectedTable[] | undefined>>;
}

const CanvasContext = createContext<CanvasContextValue>({
selectedTable: "none",
});

interface CanvasProviderProps {
children?: React.ReactNode;
}

export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const [openedTables, setOpenedTables] = useState<SelectedTable[]>();
const [selectedTable, setSelectedTable] = useState<string>("none");

const value = useMemo(
() => ({
openedTables,
setOpenedTables,
selectedTable,
setSelectedTable,
}),
[openedTables, selectedTable],
);

return (
<CanvasContext.Provider value={value}>{children}</CanvasContext.Provider>
);
};

export const useCanvasContext = () => useContext(CanvasContext);
189 changes: 189 additions & 0 deletions packages/cli/src/templates/Canvas/ListView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { forwardRef, useRef, useState } from "react";
import { useDraggable } from "@dnd-kit/core";
import { css, cx } from "@emotion/css";
import {
HvInput,
HvInputProps,
HvListContainer,
HvListItem,
HvListItemProps,
HvTypography,
outlineStyles,
theme,
useForkRef,
useUniqueId,
} from "@hitachivantara/uikit-react-core";
import { Drag } from "@hitachivantara/uikit-react-icons";

import { NodeData } from "./Node";
import { iconsMapping, iconsMappingKeys } from "./utils";

const classes = {
root: css({ display: "flex", flexDirection: "column", gap: theme.space.sm }),
item: css({
display: "flex",
justifyContent: "space-between",
alignItems: "center",
border: `1px solid ${theme.colors.atmo3}`,
borderRadius: "12px",
backgroundColor: theme.colors.atmo1,
padding: theme.space.xs,
height: "unset",
"&:focus-visible": {
...outlineStyles,
},
}),
itemTitle: css({
display: "flex",
alignItems: "center",
gap: theme.space.xs,
}),
icon: css({
borderRadius: theme.radii.round,
backgroundColor: theme.colors.cat6_60,
}),
dragging: css({
border: `2px solid ${theme.colors.primary_80}`,
}),
};

const items = iconsMappingKeys.map((key, index) => ({
id: `item${index + 1}`,
title: `Item ${index + 1}`,
subtitle: `Description ${index + 1}`,
icon: key,
}));

interface ItemProps {
id: string;
title: string;
subtitle: string;
icon: string;
}

interface ItemCardProps
extends Omit<HvListItemProps, "title">,
Omit<ItemProps, "id"> {
isDragging?: boolean;
}

const ItemCard = forwardRef<HTMLLIElement, ItemCardProps>(
({ icon, title, subtitle, isDragging, ...others }, ref) => {
return (
<HvListItem
ref={ref}
classes={{ root: cx(classes.item, { [classes.dragging]: isDragging }) }}
{...others}
>
<div className={classes.itemTitle}>
<div className={classes.icon}>{iconsMapping[icon]}</div>
<HvTypography variant="label">{title}</HvTypography>
</div>
<Drag />
</HvListItem>
);
},
);

const DraggableItemCard = ({ icon, title, id, subtitle }: ItemProps) => {
const itemRef = useRef<HTMLLIElement>(null);

const { attributes, listeners, setNodeRef, isDragging, transform } =
useDraggable({
id,
data: {
// Data needed to be dropped in HvFlow
hvFlow: {
// HvFlow will use this value to populate the node's data.nodeLabel
label: title,
// Node type from nodeTypes property provided to HvFlow
type: "node",
// Item position: used by HvFlow to position the node when dropped
x: itemRef.current?.getBoundingClientRect().x,
y: itemRef.current?.getBoundingClientRect().y,
// Values to be added to the node's data
data: {
subtitle,
color: "cat6_60",
icon,
output: {
id: "data",
label: "Data",
},
input: {
id: "data",
label: "Data",
},
} satisfies NodeData,
},
// Data needed for the DragOverlay component
dragOverlay: {
component: (
<ItemCard
icon={icon}
title={title}
subtitle={subtitle}
isDragging
/>
),
},
},
});

const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;

const forkedRef = useForkRef(itemRef, setNodeRef);

return (
<ItemCard
ref={forkedRef}
style={style}
icon={icon}
title={title}
subtitle={subtitle}
isDragging={isDragging}
{...attributes}
{...listeners}
/>
);
};

export const ListView = () => {
const [listItems, setListItems] = useState(items);

const listId = useUniqueId();

const handleSearch: HvInputProps["onChange"] = (event, value) => {
if (value) {
setListItems(
items.filter((item) =>
item.title.toLowerCase().trim().includes(value.toLowerCase().trim()),
),
);
} else {
setListItems(items);
}
};

return (
<div className={classes.root}>
<HvInput
type="search"
placeholder="Search for a node..."
aria-controls={listId}
aria-owns={listId}
onChange={handleSearch}
inputProps={{ autoComplete: "off" }}
/>
<HvListContainer id={listId}>
{listItems.map((item) => (
<DraggableItemCard key={item.id} {...item} />
))}
</HvListContainer>
</div>
);
};
Loading

0 comments on commit 2cff9c6

Please sign in to comment.