first commit
This commit is contained in:
14
index.html
14
index.html
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>React Redux App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
19327
package-lock.json
generated
19327
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,31 +1,17 @@
|
||||
{
|
||||
"name": "@armco/react-vite-rtk-template",
|
||||
"name": "@armco/layout",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"generate": "plop",
|
||||
"atom": "plop atom",
|
||||
"molecule": "plop molecule",
|
||||
"component": "plop component",
|
||||
"page": "plop page",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"type-check": "tsc"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.13.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^9.2.0",
|
||||
@@ -40,7 +26,6 @@
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jsdom": "^21.1.0",
|
||||
"plop": "^3.1.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-config-nick": "^1.0.2",
|
||||
"sass": "^1.63.4",
|
||||
@@ -61,11 +46,15 @@
|
||||
"react/jsx-no-target-blank": "off"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">16.8.0",
|
||||
"react-dom": ">16.8.0"
|
||||
},
|
||||
"prettier": "prettier-config-nick",
|
||||
"main": "index.tsx",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/.git"
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/layout.git"
|
||||
},
|
||||
"keywords": [
|
||||
"components",
|
||||
@@ -75,7 +64,7 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues"
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/layout/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme"
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/layout#readme"
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-{{pascalCase name}} {
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}"
|
||||
|
||||
describe("{{pascalCase name}}", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react"
|
||||
import "./{{pascalCase name}}.component.scss"
|
||||
|
||||
interface {{pascalCase name}}Props {}
|
||||
|
||||
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
|
||||
return <div className="c-{{pascalCase name}}">In Component {{pascalCase name}}</div>
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}.jsx"
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-{{pascalCase name}} {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface {{pascalCase name}}State {}
|
||||
|
||||
const initialState: {{pascalCase name}}State = {}
|
||||
|
||||
export const {{snakeCase name}}Slice = createSlice({
|
||||
name: "{{snakeCase name}}",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = {{snakeCase name}}Slice.actions
|
||||
|
||||
export default {{snakeCase name}}Slice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}"
|
||||
|
||||
describe("{{pascalCase name}}", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react"
|
||||
import "./{{pascalCase name}}.module.scss"
|
||||
|
||||
interface {{pascalCase name}}Props {}
|
||||
|
||||
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
|
||||
return <div className="c-{{pascalCase name}}">In Page {{pascalCase name}}</div>
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}.jsx"
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,5 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
}
|
||||
207
plopfile.cjs
207
plopfile.cjs
@@ -1,207 +0,0 @@
|
||||
module.exports = (plop) => {
|
||||
plop.setGenerator("component", {
|
||||
description: "Create a component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("atom", {
|
||||
description: "Create a component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./atoms/{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("molecule", {
|
||||
description: "Create a rich component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./molecules/{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("page", {
|
||||
description: "Create a page",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your page name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Page/Page.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Page/Page.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.module.scss",
|
||||
templateFile: "plop-templates/Page/Page.module.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Page/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.slice.ts",
|
||||
templateFile: "plop-templates/Page/Page.slice.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/pages/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/pages/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
55
src/BuilderLayout.component.scss
Executable file
55
src/BuilderLayout.component.scss
Executable file
@@ -0,0 +1,55 @@
|
||||
.ar-BuilderLayout {
|
||||
&.hide-controls > .ar-Layout__canvas > .ar-Layout__grid-tools {
|
||||
grid-template: "grid" auto / auto;
|
||||
}
|
||||
.ar-Layout__help-panel {
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&.show {
|
||||
max-height: 2rem;
|
||||
opacity: 1;
|
||||
background-color: var(--ar-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.ar-Layout__grid-tools {
|
||||
grid-template: ". colMod" 1.5rem "rowMod grid" 1fr / 1.5rem auto;
|
||||
|
||||
.ar-Layout__row-tools {
|
||||
grid-area: rowMod;
|
||||
}
|
||||
.ar-Layout__col-tools {
|
||||
grid-area: colMod;
|
||||
}
|
||||
.ar-Layout__grid-tools__main, .ar-Layout__col-tools, .ar-Layout__row-tools {
|
||||
background-color: var(--ar-bg);
|
||||
}
|
||||
.ar-Layout__grid {
|
||||
grid-area: grid;
|
||||
}
|
||||
.row-controller-cell, .column-controller-cell {
|
||||
.delete-row, .delete-column {
|
||||
display: none;
|
||||
}
|
||||
.delete-row {
|
||||
bottom: 0.25rem;
|
||||
left: 0.2rem;
|
||||
}
|
||||
.delete-column {
|
||||
right: 0.25rem;
|
||||
top: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
.delete-row, .delete-column {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
774
src/BuilderLayout.tsx
Executable file
774
src/BuilderLayout.tsx
Executable file
@@ -0,0 +1,774 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArSlotViewMode,
|
||||
ArThemes,
|
||||
BuilderLayoutProps,
|
||||
GridToolbarSpecs,
|
||||
SlotDescriptor,
|
||||
SlotProps,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
GridHelper,
|
||||
Slot,
|
||||
DomHelper,
|
||||
LoadableIcon,
|
||||
Tooltip,
|
||||
LayoutControlPanel,
|
||||
useAppSelector,
|
||||
getCurrentTheme,
|
||||
useStateWithHistory,
|
||||
} from "../.."
|
||||
import "./BuilderLayout.component.scss"
|
||||
|
||||
const BuilderLayout = ({
|
||||
acceptTextOnClick,
|
||||
classes,
|
||||
colWidths: externalColWidths,
|
||||
demo,
|
||||
displayMode,
|
||||
hideBuildModePaddings,
|
||||
isChild,
|
||||
lastSelected: externalLastSelected,
|
||||
mode,
|
||||
slotDropHandler,
|
||||
onLayoutChange,
|
||||
onSlotSelect: onExternalSlotSelect,
|
||||
gridSpecs,
|
||||
gridTemplate: externalGridTemplate,
|
||||
rowHeights: externalRowHeights,
|
||||
showControls: externalShowControls,
|
||||
showPanels,
|
||||
slots: externalSlots,
|
||||
slotRenderer,
|
||||
}: BuilderLayoutProps) => {
|
||||
const layoutRef = useRef<HTMLDivElement>(null)
|
||||
const ctrlPanelRef = useRef<HTMLDivElement>(null)
|
||||
const helpPanelRef = useRef<HTMLDivElement>(null)
|
||||
const specUpdateAction = useRef<boolean>(true)
|
||||
const [panelsDisplayed, displayPanels] = useState<boolean>()
|
||||
const [rowHeights, setRowHeights] = useState<string | Array<string>>()
|
||||
const [colWidths, setColWidths] = useState<string | Array<string>>()
|
||||
const [showControls, setShowControls] = useState<boolean>()
|
||||
const [gridArea, setGridArea] = useState<Array<Array<string>>>()
|
||||
const [gridTemplate, setGridTemplate] = useState<string>()
|
||||
const [gridToolbarSpecs, setGridToolbarSpecs] = useState<GridToolbarSpecs>()
|
||||
const [slots, setSlots, undoSlots, redoSlots, canUndo, canRedo] =
|
||||
useStateWithHistory<Array<SlotDescriptor>>()
|
||||
const [prevSlotCount, setPrevSlotsCount] = useState<number>()
|
||||
const [mergeEnabled, enableMerge] = useState<boolean>()
|
||||
const [splitEnabled, enableSplit] = useState<boolean>()
|
||||
const [lastSelected, setLastSelected] = useState<SlotDescriptor>()
|
||||
const [selectionOnlyModeEnabled, enableSelectionOnlyMode] =
|
||||
useState<boolean>()
|
||||
const [minMaxSelections, setMinMaxSelections] = useState<{
|
||||
minRow: number
|
||||
maxRow: number
|
||||
minColumn: number
|
||||
maxColumn: number
|
||||
}>()
|
||||
const theme = useAppSelector<string>(getCurrentTheme)
|
||||
const cmdCtrl = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<LoadableIcon icon="ai.AiFillMacCommand" size="0.8rem" /> /
|
||||
<LoadableIcon icon="md.MdOutlineKeyboardControlKey" size="0.8rem" />
|
||||
</>
|
||||
),
|
||||
[],
|
||||
)
|
||||
const isGrid = !displayMode || displayMode === "grid"
|
||||
|
||||
useEffect(() => {
|
||||
const doc = DomHelper.getDocumentElement(demo)
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (showControls) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
|
||||
event.preventDefault()
|
||||
undoSlots()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "y") {
|
||||
event.preventDefault()
|
||||
redoSlots()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "m") {
|
||||
if (mergeEnabled && slots && minMaxSelections) {
|
||||
GridHelper.mergeHandler(setSlots, slots, minMaxSelections)
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "h") {
|
||||
if (splitEnabled && slots) {
|
||||
GridHelper.splitHandler(setSlots, slots, "horizontal", "after")
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "v") {
|
||||
if (splitEnabled && slots) {
|
||||
GridHelper.splitHandler(setSlots, slots, "vertical", "after")
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "r") {
|
||||
if (splitEnabled && slots && gridToolbarSpecs) {
|
||||
GridHelper.insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
"after",
|
||||
)
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "c") {
|
||||
if (splitEnabled && slots && gridToolbarSpecs) {
|
||||
GridHelper.insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
"after",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc?.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
return () => {
|
||||
doc?.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [
|
||||
demo,
|
||||
gridToolbarSpecs,
|
||||
mergeEnabled,
|
||||
minMaxSelections,
|
||||
redoSlots,
|
||||
showControls,
|
||||
slots,
|
||||
splitEnabled,
|
||||
undoSlots,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
setRowHeights(externalRowHeights)
|
||||
setColWidths(externalColWidths || "1fr")
|
||||
}, [externalRowHeights, externalColWidths])
|
||||
|
||||
useEffect(() => {
|
||||
displayPanels(showPanels)
|
||||
}, [showPanels])
|
||||
|
||||
useEffect(() => {
|
||||
setShowControls(externalShowControls)
|
||||
}, [externalShowControls])
|
||||
|
||||
useEffect(() => setLastSelected(externalLastSelected), [externalLastSelected])
|
||||
|
||||
useEffect(() => {
|
||||
if (externalSlots) {
|
||||
JSON.stringify(externalSlots) !== JSON.stringify(slots) &&
|
||||
setSlots(externalSlots)
|
||||
} else if (externalGridTemplate) {
|
||||
const { slotConfigs } =
|
||||
GridHelper.generateSlotConfigs(externalGridTemplate)
|
||||
setSlots(slotConfigs)
|
||||
} else if (gridSpecs) {
|
||||
let parsedGridSpecs = gridSpecs
|
||||
if (typeof gridSpecs === "string") {
|
||||
try {
|
||||
parsedGridSpecs = JSON.parse(gridSpecs)
|
||||
} catch {
|
||||
console.warn("Grid Specs passed as string but incorrect JSON format")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof parsedGridSpecs === "object" &&
|
||||
"rows" in parsedGridSpecs &&
|
||||
"columns" in parsedGridSpecs
|
||||
) {
|
||||
const slots: Array<SlotDescriptor> = []
|
||||
for (let i = 0; i < parsedGridSpecs.rows; i++) {
|
||||
for (let j = 0; j < parsedGridSpecs.columns; j++) {
|
||||
const slotId = uuid()
|
||||
const gridArea = `ga-${slotId}`
|
||||
slots.push({
|
||||
slot: slotId,
|
||||
row: i,
|
||||
column: j,
|
||||
rowSpan: 1,
|
||||
colSpan: 1,
|
||||
gridArea,
|
||||
})
|
||||
}
|
||||
}
|
||||
setSlots(slots)
|
||||
}
|
||||
}
|
||||
}, [gridSpecs, externalGridTemplate, externalSlots])
|
||||
|
||||
useEffect(() => {
|
||||
if (slots) {
|
||||
if (slots.length !== prevSlotCount) {
|
||||
const { gridArea, rowHeights } = GridHelper.generateGridAreaAndSizes(
|
||||
slots,
|
||||
externalRowHeights,
|
||||
)
|
||||
setGridArea(gridArea)
|
||||
setRowHeights(rowHeights)
|
||||
setPrevSlotsCount(slots.length)
|
||||
}
|
||||
const { areAdjacent, ...rest } = GridHelper.checkIfAdjacent(slots)
|
||||
setMinMaxSelections(rest)
|
||||
enableSplit(slots.filter((sc) => sc.isSelected).length >= 1)
|
||||
enableMerge(areAdjacent)
|
||||
}
|
||||
}, [slots, prevSlotCount, externalRowHeights])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridArea) {
|
||||
const gridTemplate = GridHelper.generateGridTemplate(
|
||||
gridArea,
|
||||
rowHeights,
|
||||
colWidths,
|
||||
)
|
||||
setGridTemplate(gridTemplate)
|
||||
onLayoutChange && onLayoutChange(gridTemplate, slots)
|
||||
}
|
||||
}, [gridArea, rowHeights, colWidths])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridArea && showControls && slots) {
|
||||
if (specUpdateAction.current) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const calculatedRowHeights = GridHelper.calculateRowHeights(
|
||||
slots,
|
||||
rowHeights,
|
||||
demo,
|
||||
)
|
||||
setGridToolbarSpecs(
|
||||
GridHelper.generateGridToolbarSpecs(
|
||||
gridArea,
|
||||
calculatedRowHeights,
|
||||
colWidths,
|
||||
),
|
||||
)
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
}, [slots, gridArea, showControls, rowHeights, colWidths])
|
||||
|
||||
useEffect(() => {
|
||||
if (panelsDisplayed) {
|
||||
const thisRect = layoutRef.current?.getBoundingClientRect()
|
||||
if (thisRect) {
|
||||
if (helpPanelRef.current) {
|
||||
helpPanelRef.current.style.top = thisRect.bottom + "px"
|
||||
helpPanelRef.current.style.left = thisRect.left + "px"
|
||||
helpPanelRef.current.style.width = thisRect.width + "px"
|
||||
}
|
||||
if (ctrlPanelRef.current) {
|
||||
const winObj = DomHelper.getWindowElement(demo)
|
||||
ctrlPanelRef.current.style.bottom =
|
||||
(winObj ? winObj.innerHeight - thisRect.top : 0) + "px"
|
||||
ctrlPanelRef.current.style.left = thisRect.left + "px"
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
const ctrlPanelRect = ctrlPanelRef.current?.getBoundingClientRect()
|
||||
if (thisRect && ctrlPanelRef.current && helpPanelRef.current) {
|
||||
if (ctrlPanelRect && ctrlPanelRect.width > thisRect.width) {
|
||||
ctrlPanelRef.current.style.width = thisRect.width + "px"
|
||||
ctrlPanelRef.current.style.overflow = "auto"
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, [panelsDisplayed, showControls, demo])
|
||||
|
||||
const onSlotSelect = (slotConfig: SlotDescriptor) => {
|
||||
const slotsClone = [...(slots || [])]
|
||||
specUpdateAction.current = true
|
||||
const matchedSlotConfig = slots?.find((sc) => sc.slot === slotConfig.slot)
|
||||
matchedSlotConfig &&
|
||||
(matchedSlotConfig.isSelected = !matchedSlotConfig.isSelected)
|
||||
setLastSelected(matchedSlotConfig)
|
||||
// Add Text in empty slot
|
||||
if (acceptTextOnClick && matchedSlotConfig && !matchedSlotConfig?.content) {
|
||||
if (!matchedSlotConfig.props) {
|
||||
matchedSlotConfig.props = {}
|
||||
}
|
||||
// matchedSlotConfig.props.classes = "p-2"
|
||||
matchedSlotConfig.content = {
|
||||
name: "Text",
|
||||
description:
|
||||
"Foundational text component, that accepts classes and style attributes to create varying types of other text components",
|
||||
source: "stuffle",
|
||||
componentName: "Text",
|
||||
props: {
|
||||
classes: "w-100",
|
||||
id: matchedSlotConfig.slot,
|
||||
style: { minHeight: "1.5rem", lineHeight: "1.5rem" },
|
||||
isEditable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
setSlots(slotsClone, true)
|
||||
onExternalSlotSelect && onExternalSlotSelect(slotConfig)
|
||||
}
|
||||
|
||||
const onRowSelect = (
|
||||
slotConfig: SlotDescriptor,
|
||||
// When cross on a row tool cell is clicked, this flag will be true
|
||||
selectForDelete?: boolean,
|
||||
) => {
|
||||
specUpdateAction.current = false
|
||||
if (gridToolbarSpecs) {
|
||||
const rowSlots = gridToolbarSpecs.rowTools.slots
|
||||
const selectedRow = rowSlots.find((slot) => slot.slot === slotConfig.slot)
|
||||
if (selectedRow) {
|
||||
if (selectForDelete !== undefined)
|
||||
selectedRow.isSelectedForInlineDelete = selectForDelete
|
||||
else selectedRow.isSelected = !selectedRow.isSelected
|
||||
}
|
||||
setGridToolbarSpecs({ ...gridToolbarSpecs })
|
||||
slots &&
|
||||
setTimeout(
|
||||
() =>
|
||||
setSlots(
|
||||
GridHelper.selectCellsInSelectedRowCol(
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
selectForDelete,
|
||||
),
|
||||
true,
|
||||
),
|
||||
10,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onColumnSelect = (
|
||||
slotConfig: SlotDescriptor,
|
||||
selectForDelete?: boolean,
|
||||
) => {
|
||||
specUpdateAction.current = false
|
||||
if (gridToolbarSpecs) {
|
||||
const colSlots = gridToolbarSpecs.colTools.slots
|
||||
const selectedColumn = colSlots.find(
|
||||
(slot) => slot.slot === slotConfig.slot,
|
||||
)
|
||||
if (selectedColumn) {
|
||||
if (selectForDelete !== undefined)
|
||||
selectedColumn.isSelectedForInlineDelete = selectForDelete
|
||||
else selectedColumn.isSelected = !selectedColumn.isSelected
|
||||
}
|
||||
setGridToolbarSpecs({ ...gridToolbarSpecs })
|
||||
slots &&
|
||||
setSlots(
|
||||
GridHelper.selectCellsInSelectedRowCol(
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
selectForDelete,
|
||||
),
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onRowDelete = (isInlineDelete?: boolean) => {
|
||||
specUpdateAction.current = false
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
setSlots(
|
||||
GridHelper.removeHandler(
|
||||
slots,
|
||||
gridToolbarSpecs,
|
||||
"row",
|
||||
isInlineDelete,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const onColumnDelete = (isInlineDelete?: boolean) => {
|
||||
specUpdateAction.current = false
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
setSlots(
|
||||
GridHelper.removeHandler(
|
||||
slots,
|
||||
gridToolbarSpecs,
|
||||
"column",
|
||||
isInlineDelete,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const rowToolSlots = gridToolbarSpecs?.rowTools.slots
|
||||
const colToolSlots = gridToolbarSpecs?.colTools.slots
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-BuilderLayout mh-100 w-100 d-flex flex-column overflow-auto${
|
||||
classes ? " " + classes : ""
|
||||
}${showControls ? "" : " hide-controls"}`}
|
||||
ref={layoutRef}
|
||||
onMouseOver={(e) => {
|
||||
e.stopPropagation()
|
||||
!isChild && isGrid && displayPanels(true)
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.stopPropagation()
|
||||
!isChild && isGrid && displayPanels(false)
|
||||
}}
|
||||
>
|
||||
{!isChild && isGrid && (
|
||||
<LayoutControlPanel
|
||||
classes="position-fixed"
|
||||
demo={demo}
|
||||
ref={ctrlPanelRef}
|
||||
setShowControls={setShowControls}
|
||||
showControls={showControls}
|
||||
slots={slots}
|
||||
show={panelsDisplayed}
|
||||
splitEnabled={splitEnabled}
|
||||
mergeEnabled={mergeEnabled}
|
||||
undoEnabled={canUndo}
|
||||
redoEnabled={canRedo}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
rowDeleteEnabled={
|
||||
rowToolSlots && rowToolSlots.findIndex((s) => s.isSelected) > -1
|
||||
}
|
||||
columnDeleteEnabled={
|
||||
colToolSlots && colToolSlots.findIndex((s) => s.isSelected) > -1
|
||||
}
|
||||
horizontalSplitHandler={() =>
|
||||
slots &&
|
||||
GridHelper.splitHandler(setSlots, slots, "horizontal", "after")
|
||||
}
|
||||
verticalSplitHandler={() =>
|
||||
slots &&
|
||||
GridHelper.splitHandler(setSlots, slots, "vertical", "after")
|
||||
}
|
||||
mergeHandler={() => {
|
||||
slots &&
|
||||
minMaxSelections &&
|
||||
GridHelper.mergeHandler(setSlots, slots, minMaxSelections)
|
||||
enableMerge(false)
|
||||
}}
|
||||
rowInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
GridHelper.insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
columnInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
GridHelper.insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
rowDeleteHandler={() => onRowDelete()}
|
||||
columnDeleteHandler={() => onColumnDelete()}
|
||||
undoHandler={undoSlots}
|
||||
redoHandler={redoSlots}
|
||||
enableSelectionOnlyMode={enableSelectionOnlyMode}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`ar-Layout__canvas flex-1${
|
||||
isChild && !hideBuildModePaddings ? " p-1" : ""
|
||||
}`}
|
||||
>
|
||||
{slots && (
|
||||
<div className="ar-Layout__grid-tools d-grid h-100">
|
||||
{showControls && !isChild && isGrid && (
|
||||
<>
|
||||
<div className="ar-Layout__grid-tools__main border" />
|
||||
<div
|
||||
className="ar-Layout__row-tools d-grid"
|
||||
style={{
|
||||
gridTemplate: gridToolbarSpecs?.rowTools.gridTemplate,
|
||||
}}
|
||||
>
|
||||
{gridToolbarSpecs?.rowTools.slots.map(
|
||||
(slotConfig, index, arr) => (
|
||||
<span
|
||||
className={`row-controller-cell position-relative border${
|
||||
slotConfig.isSelected ? " selected" : ""
|
||||
}`}
|
||||
style={{ gridArea: slotConfig.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
demo={demo}
|
||||
mode={ArSlotViewMode.ROWCONTROLLER}
|
||||
config={{
|
||||
...slotConfig,
|
||||
props: {
|
||||
...(slotConfig.props || {}),
|
||||
classes: "h-100",
|
||||
},
|
||||
}}
|
||||
key={"controller-" + slotConfig.slot}
|
||||
onClick={(slotConfig: SlotDescriptor) => {
|
||||
onRowSelect(slotConfig)
|
||||
onExternalSlotSelect &&
|
||||
onExternalSlotSelect(slotConfig, "row")
|
||||
}}
|
||||
controlsEnabled={showControls}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
isLast={index === arr.length - 1}
|
||||
rowDeleteHandler={() => onRowDelete(true)}
|
||||
rowInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
GridHelper.insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
rowSelectHandler={(isEntering: boolean) =>
|
||||
onRowSelect(slotConfig, isEntering)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="ar-Layout__col-tools d-grid"
|
||||
style={{
|
||||
gridTemplate: gridToolbarSpecs?.colTools.gridTemplate,
|
||||
}}
|
||||
>
|
||||
{gridToolbarSpecs?.colTools.slots.map(
|
||||
(slotConfig, index, arr) => (
|
||||
<span
|
||||
className={`column-controller-cell position-relative border${
|
||||
slotConfig.isSelected ? " selected" : ""
|
||||
}`}
|
||||
style={{ gridArea: slotConfig.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
demo={demo}
|
||||
mode={ArSlotViewMode.COLCONTROLLER}
|
||||
config={{
|
||||
...slotConfig,
|
||||
props: {
|
||||
...(slotConfig.props || {}),
|
||||
classes: "h-100",
|
||||
},
|
||||
}}
|
||||
key={"controller-" + slotConfig.slot}
|
||||
onClick={(slotConfig: SlotDescriptor) => {
|
||||
onColumnSelect(slotConfig)
|
||||
onExternalSlotSelect &&
|
||||
onExternalSlotSelect(slotConfig, "column")
|
||||
}}
|
||||
controlsEnabled={showControls}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
isLast={index === arr.length - 1}
|
||||
columnDeleteHandler={() => onColumnDelete(true)}
|
||||
columnInsertHandler={(
|
||||
placement: "before" | "after",
|
||||
) =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
GridHelper.insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
columnSelectHandler={(isEntering: boolean) =>
|
||||
onColumnSelect(slotConfig, isEntering)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={`ar-Layout__grid border${isGrid ? " d-grid" : ""}${
|
||||
displayMode ? " d-" + displayMode : ""
|
||||
}`}
|
||||
style={gridTemplate ? { gridTemplate } : {}}
|
||||
>
|
||||
{slots.map((sConfig) => {
|
||||
const props: SlotProps = {
|
||||
demo,
|
||||
config: sConfig,
|
||||
containerDisplayMode: displayMode,
|
||||
controlsEnabled: showControls,
|
||||
hideBuildModePaddings,
|
||||
lastSelected,
|
||||
layoutMode: mode,
|
||||
mode: ArSlotViewMode.BUILD,
|
||||
onClick: onSlotSelect,
|
||||
selectionOnlyModeEnabled,
|
||||
horizontalSplitHandler: (placement: "before" | "after") =>
|
||||
slots &&
|
||||
GridHelper.splitHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
"horizontal",
|
||||
placement,
|
||||
sConfig,
|
||||
),
|
||||
verticalSplitHandler: (placement: "before" | "after") =>
|
||||
slots &&
|
||||
GridHelper.splitHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
"vertical",
|
||||
placement,
|
||||
sConfig,
|
||||
),
|
||||
mergeHandler: () => {
|
||||
slots &&
|
||||
minMaxSelections &&
|
||||
mergeEnabled &&
|
||||
GridHelper.mergeHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
minMaxSelections,
|
||||
sConfig,
|
||||
)
|
||||
enableMerge(false)
|
||||
},
|
||||
dropHandler: slotDropHandler,
|
||||
onSlotSelect: onExternalSlotSelect,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(props)
|
||||
) : (
|
||||
<Slot key={sConfig.slot} {...props} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isChild && isGrid && (
|
||||
<div
|
||||
className={`ar-Layout__help-panel position-fixed overflow-hidden d-flex px-2 justify-content-between py-1${
|
||||
panelsDisplayed ? " show" : ""
|
||||
}`}
|
||||
ref={helpPanelRef}
|
||||
>
|
||||
<span
|
||||
className="small fw-bold"
|
||||
style={{ color: theme === ArThemes.DARK1 ? "black" : "white" }}
|
||||
>
|
||||
Undo: Ctrl/Cmd + z, Redo: Ctrl/Cmd + y
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip demo={demo}>
|
||||
<LoadableIcon
|
||||
classes="me-3"
|
||||
color="white"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
icon="fa.FaKeyboard"
|
||||
/>
|
||||
<span slot={ArPopoverSlots.POPOVER} className="z-2">
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + h</div>
|
||||
<div className="col-7 fw-bold">Split Horizontally</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + v</div>
|
||||
<div className="col-7 fw-bold">Split Vertically</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + m</div>
|
||||
<div className="col-7 fw-bold">Merge Rectangle</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + r</div>
|
||||
<div className="col-7 fw-bold">Insert Row</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + c</div>
|
||||
<div className="col-7 fw-bold">Insert Column</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + d</div>
|
||||
<div className="col-7 fw-bold">Delete</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + z</div>
|
||||
<div className="col-7 fw-bold">Undo</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + y</div>
|
||||
<div className="col-7 fw-bold">Redo</div>
|
||||
</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip demo={demo}>
|
||||
<LoadableIcon
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
icon="md.MdInfoOutline"
|
||||
color={theme === ArThemes.DARK1 ? "black" : "white"}
|
||||
/>
|
||||
<span
|
||||
className="ar-Layout__help-panel__help-content small z-2"
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
<div>Hover on grid to reveal control panel.</div>
|
||||
<div>Click a cell to select it.</div>
|
||||
<div>You may select multiple cells by clicking on them.</div>
|
||||
<div>
|
||||
Click a row header to select all cells in that row, and column
|
||||
header to select all cells in that column
|
||||
</div>
|
||||
<div>
|
||||
You may hover on any selected cells on specific sections
|
||||
within cell to split vertical or horizontal.
|
||||
</div>
|
||||
<div>
|
||||
For Horizontal: Hover on top center or bottom center region
|
||||
and click
|
||||
</div>
|
||||
<div>
|
||||
For Vertical: Hover on left center or right center region and
|
||||
click
|
||||
</div>
|
||||
<div>
|
||||
You may merge cells too, at least 2 cells should be selected
|
||||
to merge and the selected cells shouls form a rectangle.
|
||||
</div>
|
||||
<div>
|
||||
To delete a row or column, hover on it's corresponding row or
|
||||
column header to reveal delete button and click on it.
|
||||
</div>
|
||||
<div>
|
||||
Undo/Redo layout changes is supported. Use Undo/Redo buttons
|
||||
in the control panel on top of grid, or use Ctrl/Cmd + z to
|
||||
undo, Ctrl/Cmd + y to redo
|
||||
</div>
|
||||
<div>Edit Mode should be enabled to use any shortcuts</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderLayout
|
||||
54
src/BuilderSlot.component.scss
Executable file
54
src/BuilderSlot.component.scss
Executable file
@@ -0,0 +1,54 @@
|
||||
.ar-Slot {
|
||||
&.build {
|
||||
&:hover {
|
||||
border: 1px dashed #86cff1 !important;
|
||||
}
|
||||
&.selected {
|
||||
border: 1px solid #86cff1 !important;
|
||||
}
|
||||
&.last-selected {
|
||||
outline: 4px solid #2e9cfd !important;
|
||||
}
|
||||
&.selected-for-delete {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
.ar-Slot__overlay-btn {
|
||||
width: calc(100% / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgba(85, 189, 237, 0.661);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ar-SlotTools {
|
||||
display: none;
|
||||
}
|
||||
&:hover .ar-SlotTools {
|
||||
display: flex;
|
||||
}
|
||||
&.row-controller {
|
||||
.ar-Slot__overlay-btn {
|
||||
height: calc((100% - 16px) / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgba(85, 189, 237, 0.661);
|
||||
}
|
||||
&.delete {
|
||||
height: 1rem;
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.col-controller {
|
||||
.ar-Slot__overlay-btn {
|
||||
width: calc((100% - 16px) / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgb(3, 169, 244, 0.5);
|
||||
}
|
||||
&.delete {
|
||||
width: 1rem;
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
96
src/BuilderSlot.tsx
Executable file
96
src/BuilderSlot.tsx
Executable file
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ArDndItemTypes, ArSlotViewMode, BuilderSlotProps } from "@armco/types"
|
||||
import { Draggable, Droppable, SlotTools } from ".."
|
||||
import "./BuilderSlot.component.scss"
|
||||
|
||||
const BuilderSlot = (props: BuilderSlotProps): JSX.Element => {
|
||||
const {
|
||||
acceptDropTypes,
|
||||
containerDisplayMode,
|
||||
content,
|
||||
dropHandler,
|
||||
lastSelected,
|
||||
onClick,
|
||||
selectionOnlyModeEnabled,
|
||||
style,
|
||||
...rest
|
||||
} = props
|
||||
const [selected, setSelected] = useState<boolean>()
|
||||
const slotRef = useRef<HTMLDivElement>(null)
|
||||
const { mode, config } = props
|
||||
const { slot, props: slotProps } = config
|
||||
const { classes, style: slotStyles, ...slotRest } = slotProps || {}
|
||||
const isToolSlot =
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ||
|
||||
mode === ArSlotViewMode.COLCONTROLLER
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(config.isSelected)
|
||||
}, [config.isSelected])
|
||||
|
||||
const finalSlotStyles = {
|
||||
gridArea: config.gridArea,
|
||||
...slotStyles,
|
||||
...(style || {}),
|
||||
}
|
||||
|
||||
if (containerDisplayMode && containerDisplayMode !== "grid" && !content) {
|
||||
finalSlotStyles.minWidth = "5rem"
|
||||
finalSlotStyles.minHeight = "5rem"
|
||||
}
|
||||
|
||||
const slotRender = (
|
||||
<div
|
||||
className={`ar-Slot position-relative${classes ? " " + classes : ""}${
|
||||
mode === "build" ? " obscure-border" : ""
|
||||
}${selected ? " selected" : ""} ${
|
||||
lastSelected && lastSelected.slot === slot ? " last-selected" : ""
|
||||
} ${mode}${
|
||||
config.isSelectedForInlineDelete ? " selected-for-delete" : ""
|
||||
}`}
|
||||
id={slot}
|
||||
key={slot}
|
||||
style={finalSlotStyles}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelected(!selected)
|
||||
onClick && onClick(config)
|
||||
}}
|
||||
ref={slotRef}
|
||||
{...slotRest}
|
||||
>
|
||||
{content}
|
||||
{props.controlsEnabled && !selectionOnlyModeEnabled && (
|
||||
<SlotTools {...rest} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return isToolSlot ? (
|
||||
slotRender
|
||||
) : (
|
||||
<Draggable
|
||||
canDrag={!!config.content && props.mode === ArSlotViewMode.BUILD}
|
||||
demo={props.demo}
|
||||
itemData={config}
|
||||
itemType={ArDndItemTypes.COMPONENTSLOT}
|
||||
>
|
||||
<Droppable
|
||||
acceptTypes={[
|
||||
ArDndItemTypes.COMPONENTSLOT,
|
||||
ArDndItemTypes.TREELISTITEM,
|
||||
ArDndItemTypes.ICON,
|
||||
...(acceptDropTypes || []),
|
||||
]}
|
||||
dropHandler={(sourceData) =>
|
||||
dropHandler && dropHandler(sourceData, config)
|
||||
}
|
||||
hideHoverEffect
|
||||
>
|
||||
{slotRender}
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderSlot
|
||||
1
src/Layout.component.scss
Executable file
1
src/Layout.component.scss
Executable file
@@ -0,0 +1 @@
|
||||
.ar-Layout {}
|
||||
6
src/Layout.test.ts
Executable file
6
src/Layout.test.ts
Executable file
@@ -0,0 +1,6 @@
|
||||
import React from "react"
|
||||
import Layout from "./Layout"
|
||||
|
||||
describe("Layout", () => {
|
||||
it("renders without error", () => {})
|
||||
})
|
||||
73
src/Layout.tsx
Executable file
73
src/Layout.tsx
Executable file
@@ -0,0 +1,73 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ArAlertType, LayoutProps, SlotDescriptor } from "@armco/types"
|
||||
import {
|
||||
BuilderLayout,
|
||||
ReleaseLayout,
|
||||
Network,
|
||||
useAppDispatch,
|
||||
notify,
|
||||
} from "../.."
|
||||
import "./Layout.component.scss"
|
||||
|
||||
const dummyGridSpecs = { rows: 5, columns: 5 }
|
||||
|
||||
const Layout = (props: LayoutProps) => {
|
||||
const { mode, demo, gridSpecs, url } = props
|
||||
const propsClone = { ...props }
|
||||
propsClone.gridSpecs = demo ? gridSpecs || dummyGridSpecs : gridSpecs
|
||||
const [localSlots, setLocalSlots] = useState<
|
||||
Array<SlotDescriptor> | undefined
|
||||
>()
|
||||
const [localGridTemplate, setLocalGridTemplate] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
propsClone.slots = localSlots
|
||||
propsClone.gridTemplate = localGridTemplate
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
JSON.stringify(props.slots) !== JSON.stringify(localSlots) &&
|
||||
setLocalSlots(props.slots)
|
||||
}, [props.slots])
|
||||
|
||||
useEffect(() => {
|
||||
setLocalGridTemplate(props.gridTemplate)
|
||||
}, [props.gridTemplate])
|
||||
|
||||
useEffect(() => {
|
||||
!(localSlots || localGridTemplate) &&
|
||||
url &&
|
||||
Network.get(url)
|
||||
.then((res) => {
|
||||
const component = Array.isArray(res.body) ? res.body[0] : res.body
|
||||
component?.descriptor?.content &&
|
||||
setLocalSlots(component.descriptor.content as Array<SlotDescriptor>)
|
||||
component?.descriptor?.gridTemplate &&
|
||||
setLocalGridTemplate(component.descriptor.layout as string)
|
||||
})
|
||||
.catch((e) =>
|
||||
dispatch(
|
||||
notify({
|
||||
message: "Failed to fetch descriptor for provided URL",
|
||||
uid: uuid(),
|
||||
type: ArAlertType.ERROR,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}, [url])
|
||||
|
||||
return mode === "build" || (!mode && demo) ? (
|
||||
<BuilderLayout {...propsClone} />
|
||||
) : (
|
||||
<ReleaseLayout
|
||||
classes={propsClone.classes}
|
||||
displayMode={propsClone.displayMode}
|
||||
slots={localSlots}
|
||||
gridTemplate={localGridTemplate}
|
||||
slotRenderer={propsClone.slotRenderer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
18
src/LayoutControlPanel.component.scss
Executable file
18
src/LayoutControlPanel.component.scss
Executable file
@@ -0,0 +1,18 @@
|
||||
.ar-LayoutControlPanel {
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&.show {
|
||||
max-height: 2.25rem;
|
||||
max-width: calc(2.25rem - 2px);
|
||||
opacity: 1;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background-color: var(--ar-color-selected);
|
||||
white-space: nowrap;
|
||||
|
||||
&.show-all-controls {
|
||||
max-width: 30rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/LayoutControlPanel.tsx
Executable file
267
src/LayoutControlPanel.tsx
Executable file
@@ -0,0 +1,267 @@
|
||||
import { Ref, forwardRef } from "react"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArThemes,
|
||||
FunctionType,
|
||||
LayoutControlPanelProps,
|
||||
} from "@armco/types"
|
||||
import { LoadableIcon, Tooltip, getCurrentTheme, useAppSelector } from "../.."
|
||||
import "./LayoutControlPanel.component.scss"
|
||||
|
||||
const getControl = (
|
||||
theme: string,
|
||||
icon: string,
|
||||
handler: FunctionType,
|
||||
tooltip: string,
|
||||
demo?: boolean,
|
||||
color?: string,
|
||||
isDisabled?: boolean,
|
||||
toggleColor?: string,
|
||||
showControls?: boolean,
|
||||
) => {
|
||||
const iconColor =
|
||||
theme === ArThemes.DARK1
|
||||
? isDisabled
|
||||
? "#7a7aa7"
|
||||
: color || "black"
|
||||
: isDisabled
|
||||
? "#cfcfcf"
|
||||
: color || "white"
|
||||
return (
|
||||
<Tooltip
|
||||
classes={showControls === undefined || showControls ? "" : " w-0"}
|
||||
demo={demo}
|
||||
>
|
||||
<span
|
||||
className={`p-1 border-radius-l2 d-inline-block${
|
||||
!isDisabled ? " cursor-pointer hover-border hover-bg dark2" : ""
|
||||
}`}
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={isDisabled ? () => {} : handler}
|
||||
style={{ backgroundColor: toggleColor }}
|
||||
>
|
||||
<LoadableIcon icon={icon} color={iconColor} />
|
||||
</span>
|
||||
<span slot={ArPopoverSlots.POPOVER} style={{ whiteSpace: "normal" }}>
|
||||
<div>{tooltip}</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
const LayoutControlPanel = forwardRef(
|
||||
(props: LayoutControlPanelProps, ref: Ref<HTMLDivElement>): JSX.Element => {
|
||||
const {
|
||||
classes,
|
||||
demo,
|
||||
showControls,
|
||||
setShowControls,
|
||||
slots,
|
||||
splitEnabled,
|
||||
mergeEnabled,
|
||||
rowDeleteEnabled,
|
||||
columnDeleteEnabled,
|
||||
undoEnabled,
|
||||
redoEnabled,
|
||||
selectionOnlyModeEnabled,
|
||||
show,
|
||||
enableSelectionOnlyMode,
|
||||
horizontalSplitHandler,
|
||||
verticalSplitHandler,
|
||||
mergeHandler,
|
||||
rowInsertHandler,
|
||||
columnInsertHandler,
|
||||
rowDeleteHandler,
|
||||
columnDeleteHandler,
|
||||
undoHandler,
|
||||
redoHandler,
|
||||
} = props
|
||||
const theme = useAppSelector<string>(getCurrentTheme)
|
||||
|
||||
const controls = [
|
||||
[
|
||||
showControls ? "md.MdEditOff" : "md.MdEdit",
|
||||
() => setShowControls(!showControls),
|
||||
"Edit Layout",
|
||||
"#ffa500",
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsVertical",
|
||||
horizontalSplitHandler,
|
||||
splitEnabled ? "Split Horizontal" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsHorizontal",
|
||||
verticalSplitHandler,
|
||||
splitEnabled ? "Split Vertical" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"fa.FaCompressArrowsAlt",
|
||||
mergeHandler,
|
||||
mergeEnabled
|
||||
? "Merge Selection"
|
||||
: "Selected at least two adjacent cells to merge",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!mergeEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiInsertRowTop",
|
||||
() => rowInsertHandler("before"),
|
||||
"Insert Row Before",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertRowBottom",
|
||||
() => rowInsertHandler("after"),
|
||||
"Insert Row After",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertColumnLeft",
|
||||
() => columnInsertHandler("before"),
|
||||
"Insert Column Before",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertColumnRight",
|
||||
() => columnInsertHandler("after"),
|
||||
"Insert Column After",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiDeleteRow",
|
||||
rowDeleteHandler,
|
||||
rowDeleteEnabled ? "Delete Row" : "Select a row to delete",
|
||||
"#ff6666",
|
||||
!rowDeleteEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiDeleteColumn",
|
||||
columnDeleteHandler,
|
||||
columnDeleteEnabled ? "Delete Column" : "Select a column to delete",
|
||||
"#ff6666",
|
||||
!columnDeleteEnabled,
|
||||
],
|
||||
[
|
||||
"fc.FcUndo",
|
||||
undoHandler,
|
||||
undoEnabled ? "Undo Last" : "Nothing to undo",
|
||||
"",
|
||||
!undoEnabled,
|
||||
],
|
||||
[
|
||||
"fc.FcRedo",
|
||||
redoHandler,
|
||||
redoEnabled ? "Redo Last Undone" : "Nothing to redo",
|
||||
"",
|
||||
!redoEnabled,
|
||||
],
|
||||
[
|
||||
"gr.GrSelect",
|
||||
() => enableSelectionOnlyMode(!selectionOnlyModeEnabled),
|
||||
!selectionOnlyModeEnabled
|
||||
? "Enable Selection Only Mode"
|
||||
: "Disable Selection Only Mode",
|
||||
selectionOnlyModeEnabled ? "" : "#17dc81",
|
||||
false,
|
||||
selectionOnlyModeEnabled ? "#17dc81" : "",
|
||||
],
|
||||
]
|
||||
return (
|
||||
<div
|
||||
className={`ar-LayoutControlPanel p-1${show ? " show" : ""}${
|
||||
classes ? " " + classes : ""
|
||||
}${showControls ? " show-all-controls" : ""}`}
|
||||
style={{ overflow: "hidden" }}
|
||||
ref={ref}
|
||||
key={"layout-control-panel"}
|
||||
>
|
||||
{getControl(
|
||||
theme,
|
||||
controls[0][0] as string,
|
||||
controls[0][1] as FunctionType,
|
||||
controls[0][2] as string,
|
||||
demo,
|
||||
controls[0][3] as string,
|
||||
)}
|
||||
{showControls && (
|
||||
<>
|
||||
{slots && (
|
||||
<>
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(1, 4)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(4, 10)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(10, 12)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(12)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default LayoutControlPanel
|
||||
30
src/ReleaseLayout.tsx
Executable file
30
src/ReleaseLayout.tsx
Executable file
@@ -0,0 +1,30 @@
|
||||
import { ArSlotViewMode, ReleaseLayoutProps, SlotProps } from "@armco/types"
|
||||
import { Slot } from ".."
|
||||
import "./ReleaseLayout.component.scss"
|
||||
|
||||
const ReleaseLayout = (props: ReleaseLayoutProps): JSX.Element => {
|
||||
const { classes, displayMode, gridTemplate, slots, slotRenderer } = props
|
||||
return (
|
||||
<div
|
||||
className={`ar-ReleaseLayout__grid h-100 ${
|
||||
displayMode ? "d-" + displayMode : "d-grid"
|
||||
}${classes ? " " + classes : ""}`}
|
||||
style={gridTemplate ? { gridTemplate } : {}}
|
||||
>
|
||||
{slots?.map((sConfig) => {
|
||||
const props: SlotProps = {
|
||||
config: sConfig,
|
||||
containerDisplayMode: displayMode,
|
||||
mode: ArSlotViewMode.RELEASE,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(props)
|
||||
) : (
|
||||
<Slot key={sConfig.slot} {...props} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReleaseLayout
|
||||
25
src/ReleaseSlot.tsx
Executable file
25
src/ReleaseSlot.tsx
Executable file
@@ -0,0 +1,25 @@
|
||||
import { ReleaseSlotProps } from "@armco/types"
|
||||
import "./ReleaseSlot.component.scss"
|
||||
|
||||
const ReleaseSlot = (props: ReleaseSlotProps): JSX.Element => {
|
||||
const { config, content } = props
|
||||
const { slot, props: slotProps } = config
|
||||
const {
|
||||
classes: slotClasses,
|
||||
style: slotStyles,
|
||||
...slotRest
|
||||
} = slotProps || {}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-Slot${slotClasses ? " " + slotClasses : ""}`}
|
||||
key={slot}
|
||||
style={{ gridArea: config.gridArea, ...slotStyles }}
|
||||
{...slotRest}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReleaseSlot
|
||||
54
src/Resizable.component.scss
Executable file
54
src/Resizable.component.scss
Executable file
@@ -0,0 +1,54 @@
|
||||
.ar-Resizable {
|
||||
.handle {
|
||||
&.n, &.s {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
&.e, &.w {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
&.ne, &.nw, &.se, &.sw {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
&.n {
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.s {
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.e {
|
||||
left: calc(100% - 0.5rem);
|
||||
}
|
||||
&.w {
|
||||
right: calc(100% - 0.5rem);
|
||||
}
|
||||
&.ne {
|
||||
left: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.nw {
|
||||
right: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.se {
|
||||
left: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.sw {
|
||||
right: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.n, &.s, &.e, &.w, &.ne, &.nw, &.se, &.sw {
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/Resizable.tsx
Executable file
121
src/Resizable.tsx
Executable file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
ComponentType,
|
||||
ForwardRefExoticComponent,
|
||||
LazyExoticComponent,
|
||||
PropsWithoutRef,
|
||||
ReactElement,
|
||||
RefAttributes,
|
||||
Suspense,
|
||||
cloneElement,
|
||||
lazy,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { ArDirections, ResizableProps } from "@armco/types"
|
||||
import "./Resizable.component.scss"
|
||||
|
||||
type ComponentOrForwardRef =
|
||||
| ComponentType<any>
|
||||
| ForwardRefExoticComponent<PropsWithoutRef<any> & RefAttributes<any>>
|
||||
|
||||
const components: {
|
||||
[key: string]: LazyExoticComponent<ComponentOrForwardRef>
|
||||
} = {
|
||||
Slot: lazy(() => import("../Slot")),
|
||||
// Add more components as needed
|
||||
}
|
||||
|
||||
const Resizable = (props: ResizableProps): JSX.Element => {
|
||||
const {
|
||||
reClasses,
|
||||
directions,
|
||||
display,
|
||||
componentName,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
} = props
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const mouseDownY = useRef<number>(0)
|
||||
const mouseDownX = useRef<number>(0)
|
||||
const childRef = useRef<HTMLElement>(null)
|
||||
|
||||
const mouseMoveHandler = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (resizing && childRef.current) {
|
||||
const childRect = childRef.current.getBoundingClientRect()
|
||||
// const initialPosition = childRect.left + childRect.width
|
||||
// const dragOffset = e.clientX - initialPosition
|
||||
|
||||
// const newWidth = childRect.width + dragOffset
|
||||
const newWidth = e.clientX - childRect.left
|
||||
const newFrValue = newWidth / childRect.width
|
||||
|
||||
// setFrValue(newFrValue)
|
||||
}
|
||||
},
|
||||
[resizing, childRef],
|
||||
)
|
||||
const mouseUpHandler = useCallback(() => setResizing(false), [])
|
||||
const mouseDownHandler = useCallback(
|
||||
(
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
direction: ArDirections,
|
||||
) => {
|
||||
setResizing(true)
|
||||
mouseDownY.current = e.clientY
|
||||
// TODO: handle direction
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", mouseMoveHandler)
|
||||
document.addEventListener("mouseup", mouseUpHandler)
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler)
|
||||
document.removeEventListener("mouseup", mouseUpHandler)
|
||||
}
|
||||
}, [mouseMoveHandler, mouseUpHandler])
|
||||
|
||||
useEffect(() => {
|
||||
if (childRef.current) {
|
||||
const rect = childRef.current.getBoundingClientRect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const Component = (componentName && components[componentName]) as
|
||||
| React.ComponentType<any>
|
||||
| undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-Resizable position-relative${
|
||||
reClasses ? " " + reClasses : ""
|
||||
}${display ? " " + display : ""}`}
|
||||
style={style}
|
||||
>
|
||||
{directions?.map((direction) => (
|
||||
<div
|
||||
key={direction}
|
||||
className={`handle position-absolute ${direction}`}
|
||||
onMouseDown={(e) => mouseDownHandler(e, direction)}
|
||||
/>
|
||||
))}
|
||||
{Component ? (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Component childRef={childRef} {...rest} />
|
||||
</Suspense>
|
||||
) : (
|
||||
children &&
|
||||
cloneElement(children as ReactElement, {
|
||||
childRef: childRef,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Resizable
|
||||
95
src/Slot.tsx
Executable file
95
src/Slot.tsx
Executable file
@@ -0,0 +1,95 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
ArComponentResources,
|
||||
ArSlotViewMode,
|
||||
LibRepoType,
|
||||
SlotDescriptor,
|
||||
SlotProps,
|
||||
} from "@armco/types"
|
||||
import * as atoms from ".."
|
||||
import * as molecules from "../../molecules"
|
||||
import { BuilderSlot, GridHelper, Layout, ReleaseSlot } from "../.."
|
||||
import "./Slot.component.scss"
|
||||
|
||||
const Slot = (props: SlotProps): JSX.Element => {
|
||||
const { mode = ArSlotViewMode.PREVIEW, layoutMode = "preview" } = props
|
||||
const [content, setContent] = useState<ReactNode>()
|
||||
const config = props.config
|
||||
const repo: LibRepoType = useMemo(() => ({ ...atoms, ...molecules }), [])
|
||||
|
||||
useEffect(() => {
|
||||
const content = config.content as {
|
||||
componentName?: string
|
||||
source?: ArComponentResources
|
||||
descriptor?: { content: Array<SlotDescriptor> }
|
||||
props?: { [key: string]: any }
|
||||
}
|
||||
if (content) {
|
||||
if (content.componentName && repo[content.componentName]) {
|
||||
// const source = content.source || ArComponentResources.STUFFLE
|
||||
const Component = repo[content.componentName]
|
||||
const contentProps = content.props || {}
|
||||
if (content.componentName === "Layout") {
|
||||
contentProps.displayMode = config.props?.containerDisplayMode
|
||||
}
|
||||
setContent(
|
||||
<Component
|
||||
demo={mode === ArSlotViewMode.BUILD}
|
||||
activeId={props.lastSelected?.slot}
|
||||
{...{ ...(contentProps || {}) }}
|
||||
/>,
|
||||
)
|
||||
} else if (
|
||||
content.descriptor?.content &&
|
||||
Array.isArray(content.descriptor.content)
|
||||
) {
|
||||
const gaResponse =
|
||||
content?.descriptor?.content &&
|
||||
GridHelper.generateGridAreaAndSizes(
|
||||
content?.descriptor?.content,
|
||||
"auto",
|
||||
)
|
||||
|
||||
const layout =
|
||||
gaResponse &&
|
||||
GridHelper.generateGridTemplate(gaResponse.gridArea, "auto")
|
||||
|
||||
setContent(
|
||||
<Layout
|
||||
displayMode={config.props?.containerDisplayMode}
|
||||
slots={content?.descriptor?.content}
|
||||
gridTemplate={layout}
|
||||
hideBuildModePaddings={props.hideBuildModePaddings}
|
||||
mode={layoutMode}
|
||||
isChild={true}
|
||||
onSlotSelect={props.onSlotSelect}
|
||||
lastSelected={props.lastSelected}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setContent(null)
|
||||
}
|
||||
}, [
|
||||
config.content,
|
||||
layoutMode,
|
||||
mode,
|
||||
repo,
|
||||
props.lastSelected,
|
||||
props.hideBuildModePaddings,
|
||||
])
|
||||
|
||||
return mode !== ArSlotViewMode.PREVIEW && mode !== ArSlotViewMode.RELEASE ? (
|
||||
<BuilderSlot
|
||||
key={"builder-slot-" + props.config.slot}
|
||||
{...{ ...props, content }}
|
||||
/>
|
||||
) : (
|
||||
<ReleaseSlot
|
||||
key={"builder-slot-" + props.config.slot}
|
||||
{...{ ...props, content }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Slot
|
||||
172
src/SlotTools.tsx
Executable file
172
src/SlotTools.tsx
Executable file
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArSlotViewMode,
|
||||
SlotControlProps,
|
||||
SlotToolsProps,
|
||||
} from "@armco/types"
|
||||
import { Tooltip } from ".."
|
||||
import "./SlotTools.component.scss"
|
||||
|
||||
const staticSlotTools: Array<SlotControlProps> = [
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-up d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Above",
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{ class: "ar-Slot__overlay-btn cursor-pointer delete", isToolSlotOnly: true },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-left d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Before",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot merge d-flex cursor-pointer",
|
||||
tooltip: "Merge at this cell",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-right d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell After",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-down d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Below",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
]
|
||||
|
||||
const SlotTools = (props: SlotToolsProps): JSX.Element => {
|
||||
const {
|
||||
config,
|
||||
controlsEnabled,
|
||||
demo,
|
||||
mode = ArSlotViewMode.PREVIEW,
|
||||
horizontalSplitHandler,
|
||||
mergeHandler,
|
||||
rowInsertHandler,
|
||||
columnInsertHandler,
|
||||
rowDeleteHandler,
|
||||
columnDeleteHandler,
|
||||
verticalSplitHandler,
|
||||
rowSelectHandler,
|
||||
columnSelectHandler,
|
||||
} = props
|
||||
const [slotTools, setSlotTools] =
|
||||
useState<Array<SlotControlProps>>(staticSlotTools)
|
||||
|
||||
useEffect(() => {
|
||||
const slotToolsClone: Array<SlotControlProps> = JSON.parse(
|
||||
JSON.stringify(slotTools),
|
||||
)
|
||||
if (
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ||
|
||||
mode === ArSlotViewMode.COLCONTROLLER
|
||||
) {
|
||||
const isRowController = mode === ArSlotViewMode.ROWCONTROLLER
|
||||
const handler = isRowController ? rowInsertHandler : columnInsertHandler
|
||||
slotToolsClone[0].onClick = () =>
|
||||
controlsEnabled && handler && handler("before")
|
||||
slotToolsClone[0].tooltip = isRowController
|
||||
? "Insert Row Above"
|
||||
: "Insert Column Before"
|
||||
slotToolsClone[0].class.indexOf("slot") === -1 &&
|
||||
(slotToolsClone[0].class += " slot cursor-pointer")
|
||||
slotToolsClone[1].tooltip = ""
|
||||
slotToolsClone[2].onClick = () =>
|
||||
controlsEnabled && handler && handler("after")
|
||||
slotToolsClone[2].tooltip = isRowController
|
||||
? "Insert Row Below"
|
||||
: "Insert Column After"
|
||||
slotToolsClone[2].class.indexOf("slot") === -1 &&
|
||||
(slotToolsClone[2].class += " slot cursor-pointer")
|
||||
slotToolsClone[3].tooltip = isRowController
|
||||
? "Delete Row"
|
||||
: "Delete Column"
|
||||
slotToolsClone[3].onClick = isRowController
|
||||
? rowDeleteHandler
|
||||
: columnDeleteHandler
|
||||
slotToolsClone[3].onMouseEnter = isRowController
|
||||
? () => rowSelectHandler && rowSelectHandler(true)
|
||||
: () => columnSelectHandler && columnSelectHandler(true)
|
||||
slotToolsClone[3].onMouseLeave = isRowController
|
||||
? () => rowSelectHandler && rowSelectHandler(false)
|
||||
: () => columnSelectHandler && columnSelectHandler(false)
|
||||
} else {
|
||||
slotToolsClone[1].onClick = () =>
|
||||
controlsEnabled &&
|
||||
horizontalSplitHandler &&
|
||||
horizontalSplitHandler("before")
|
||||
slotToolsClone[4].onClick = () =>
|
||||
controlsEnabled &&
|
||||
verticalSplitHandler &&
|
||||
verticalSplitHandler("before")
|
||||
slotToolsClone[5].onClick = () =>
|
||||
controlsEnabled && mergeHandler && mergeHandler()
|
||||
slotToolsClone[6].onClick = () =>
|
||||
controlsEnabled && verticalSplitHandler && verticalSplitHandler("after")
|
||||
slotToolsClone[8].onClick = () =>
|
||||
controlsEnabled &&
|
||||
horizontalSplitHandler &&
|
||||
horizontalSplitHandler("after")
|
||||
}
|
||||
setSlotTools([...slotToolsClone])
|
||||
}, [
|
||||
controlsEnabled,
|
||||
config,
|
||||
horizontalSplitHandler,
|
||||
verticalSplitHandler,
|
||||
mergeHandler,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-SlotTools position-absolute h-100 w-100 flex-wrap top-0 start-0 ${
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ? "flex-column" : ""
|
||||
}`}
|
||||
>
|
||||
{slotTools
|
||||
.map(
|
||||
(slotTool, index) =>
|
||||
(!slotTool.isGridOnly || mode === ArSlotViewMode.BUILD) &&
|
||||
(!slotTool.isToolSlotOnly ||
|
||||
[
|
||||
ArSlotViewMode.ROWCONTROLLER,
|
||||
ArSlotViewMode.COLCONTROLLER,
|
||||
].indexOf(mode) > -1) && (
|
||||
<span
|
||||
className={slotTool.class}
|
||||
key={"slotTool-" + index}
|
||||
onClick={(e) => {
|
||||
slotTool.tooltip && e.stopPropagation()
|
||||
slotTool.onClick && slotTool.onClick()
|
||||
}}
|
||||
onMouseEnter={slotTool.onMouseEnter}
|
||||
onMouseLeave={slotTool.onMouseLeave}
|
||||
>
|
||||
{slotTool.tooltip ? (
|
||||
<Tooltip classes="w-100" demo={demo}>
|
||||
<span
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
className="d-flex h-100 w-100"
|
||||
/>
|
||||
<span slot={ArPopoverSlots.POPOVER}>
|
||||
{slotTool.tooltip}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={`${slotTool.class}`} />
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
)
|
||||
.filter((s) => s)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlotTools
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useRoutes } from "react-router-dom"
|
||||
import * as pages from "./pages"
|
||||
import Helper from "./utils/helper"
|
||||
import ROUTES from "./routes"
|
||||
|
||||
Helper.populateComponentsInRoutes(ROUTES, pages)
|
||||
|
||||
interface RouterProps {}
|
||||
|
||||
const Router = (props: RouterProps): JSX.Element | null => useRoutes(ROUTES)
|
||||
|
||||
export default Router
|
||||
@@ -1,6 +0,0 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import type { RootState, AppDispatch } from "./store"
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-Home {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface HomeState {}
|
||||
|
||||
const initialState: HomeState = {}
|
||||
|
||||
export const homeSlice = createSlice({
|
||||
name: "home",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = homeSlice.actions
|
||||
|
||||
export default homeSlice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import Home from "./Home"
|
||||
|
||||
describe("Home", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
import "./Home.module.scss"
|
||||
|
||||
interface HomeProps {}
|
||||
|
||||
const Home = props => {
|
||||
return (
|
||||
<div className="c-Home">
|
||||
In Page Home
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -1,3 +0,0 @@
|
||||
import Home from "./Home.jsx"
|
||||
|
||||
export default Home
|
||||
@@ -1,7 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Home from "./Home"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Home,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const ROUTES = [
|
||||
{
|
||||
path: "/",
|
||||
class: "landing",
|
||||
element: "Home",
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
@@ -1,18 +0,0 @@
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
|
||||
import counterReducer from "../features/counter/counterSlice"
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
counter: counterReducer,
|
||||
},
|
||||
})
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>
|
||||
6
src/app/types/route.d.ts
vendored
6
src/app/types/route.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
interface RouteConfig {
|
||||
path: String
|
||||
class?: String
|
||||
element: String | JSX.Element | null
|
||||
children?: Array<RouteConfig>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
class Helper {
|
||||
static populateComponentsInRoutes(routes: RouteConfig[], components: any) {
|
||||
routes &&
|
||||
routes.forEach((route) => {
|
||||
const Component: JSX.ElementType =
|
||||
components[route.element as keyof object]
|
||||
route.element = <Component />
|
||||
if (route.children) {
|
||||
Helper.populateComponentsInRoutes(route.children, components)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Helper
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "../../app/hooks"
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
incrementIfOdd,
|
||||
selectCount,
|
||||
} from "./counterSlice"
|
||||
|
||||
export function Counter() {
|
||||
const count = useAppSelector(selectCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const [incrementAmount, setIncrementAmount] = useState("2")
|
||||
|
||||
const incrementValue = Number(incrementAmount) || 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="value">{count}</span>
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<input
|
||||
className="textbox"
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className="asyncButton"
|
||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||
>
|
||||
Add If Odd
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// A mock function to mimic making an async request for data
|
||||
export function fetchCount(amount = 1) {
|
||||
return new Promise<{ data: number }>((resolve) =>
|
||||
setTimeout(() => resolve({ data: amount }), 500),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import counterReducer, {
|
||||
CounterState,
|
||||
increment,
|
||||
decrement,
|
||||
incrementByAmount,
|
||||
} from "./counterSlice"
|
||||
|
||||
describe("counter reducer", () => {
|
||||
const initialState: CounterState = {
|
||||
value: 3,
|
||||
status: "idle",
|
||||
}
|
||||
it("should handle initial state", () => {
|
||||
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
|
||||
value: 0,
|
||||
status: "idle",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle increment", () => {
|
||||
const actual = counterReducer(initialState, increment())
|
||||
expect(actual.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should handle decrement", () => {
|
||||
const actual = counterReducer(initialState, decrement())
|
||||
expect(actual.value).toEqual(2)
|
||||
})
|
||||
|
||||
it("should handle incrementByAmount", () => {
|
||||
const actual = counterReducer(initialState, incrementByAmount(2))
|
||||
expect(actual.value).toEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { RootState, AppThunk } from "../../app/store"
|
||||
import { fetchCount } from "./counterAPI"
|
||||
|
||||
export interface CounterState {
|
||||
value: number
|
||||
status: "idle" | "loading" | "failed"
|
||||
}
|
||||
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
status: "idle",
|
||||
}
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched. Thunks are
|
||||
// typically used to make async requests.
|
||||
export const incrementAsync = createAsyncThunk(
|
||||
"counter/fetchCount",
|
||||
async (amount: number) => {
|
||||
const response = await fetchCount(amount)
|
||||
// The value we return becomes the `fulfilled` action payload
|
||||
return response.data
|
||||
},
|
||||
)
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState,
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn"t actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// including actions generated by createAsyncThunk or in other slices.
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(incrementAsync.pending, (state) => {
|
||||
state.status = "loading"
|
||||
})
|
||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
state.status = "idle"
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(incrementAsync.rejected, (state) => {
|
||||
state.status = "failed"
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they"re used instead of
|
||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||
export const selectCount = (state: RootState) => state.counter.value
|
||||
|
||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||
// Here"s an example of conditionally dispatching actions based on current state.
|
||||
export const incrementIfOdd =
|
||||
(amount: number): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
const currentValue = selectCount(getState())
|
||||
if (currentValue % 2 === 1) {
|
||||
dispatch(incrementByAmount(amount))
|
||||
}
|
||||
}
|
||||
|
||||
export default counterSlice.reducer
|
||||
3
src/index.ts
Executable file
3
src/index.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
import Layout from "./Layout"
|
||||
|
||||
export default Layout
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { store } from "./app/store"
|
||||
import Router from "./app/Router"
|
||||
import "./app/static/styles/global.scss"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<Router />
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user