snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

24
apps/uikit-playground/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
<!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>UiKit-Playground</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
{
"name": "@rocket.chat/uikit-playground",
"version": "0.7.16",
"private": true,
"type": "module",
"scripts": {
".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./dist ../../.preview/uikit-playground",
"build-preview": "tsc && vite build",
"dev": "vite",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-json": "^6.0.2",
"@hello-pangea/dnd": "^17.0.0",
"@lezer/highlight": "^1.2.3",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/css-in-js": "^0.33.1",
"@rocket.chat/fuselage": "^0.89.0",
"@rocket.chat/fuselage-hooks": "^0.43.1",
"@rocket.chat/fuselage-toastbar": "~0.36.1",
"@rocket.chat/fuselage-tokens": "^0.34.0",
"@rocket.chat/fuselage-ui-kit": "workspace:~",
"@rocket.chat/icons": "^0.49.0",
"@rocket.chat/logo": "^0.33.2",
"@rocket.chat/styled": "~0.34.1",
"@rocket.chat/ui-avatar": "workspace:^",
"@rocket.chat/ui-contexts": "workspace:~",
"codemirror": "^6.0.2",
"eslint4b-prebuilt": "^6.7.2",
"prettier": "~3.9.6",
"rc-scrollbars": "^1.1.6",
"react": "~19.2.8",
"react-dom": "~19.2.8",
"react-router-dom": "^6.30.6",
"react-split-pane": "^3.2.0",
"react-virtuoso": "~4.18.12",
"reactflow": "^11.11.4"
},
"devDependencies": {
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
"@types/lodash": "~4.17.25",
"@types/react": "~19.2.18",
"@types/react-dom": "~19.2.5",
"@vitejs/plugin-react": "~6.0.5",
"eslint": "~9.39.5",
"typescript": "~5.9.3",
"vite": "^8.0.16"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,8 @@
.App {
text-align: center;
}
#toastBarRoot{
position: fixed;
z-index: 9999;
}

View File

@@ -0,0 +1,58 @@
import { Box } from '@rocket.chat/fuselage';
import { useMediaQueries } from '@rocket.chat/fuselage-hooks';
import { ToastBarProvider } from '@rocket.chat/fuselage-toastbar';
import { useContext, useEffect } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { HomeLayout } from './Components/Routes/HomeLayout';
import { ProjectSpecificLayout } from './Components/Routes/ProjectSpecificLayout';
import { context, isMobileAction, isTabletAction } from './Context';
import FlowDiagram from './Pages/FlowDiagram';
import Home from './Pages/Home';
import Playground from './Pages/Playground';
import Prototype from './Pages/Prototype';
import SignInToWorkspace from './Pages/SignInSignUp';
import routes from './Routes/Routes';
import './App.css';
import './_global.css';
import './cssVariables.css';
function App() {
const { dispatch } = useContext(context);
const [isMobile, isTablet] = useMediaQueries('(max-width: 630px)', '(max-width: 1050px)');
useEffect(() => {
dispatch(isMobileAction(isMobile));
}, [isMobile, dispatch]);
useEffect(() => {
dispatch(isTabletAction(isTablet));
}, [isTablet, dispatch]);
return (
<Box width='100vw' height='100vh' display='flex' flexDirection='column'>
<ToastBarProvider>
<BrowserRouter>
<Routes>
<Route element={<HomeLayout />}>
<Route path={routes.login} element={<SignInToWorkspace route={routes.login} />} />
<Route path={routes.signup} element={<SignInToWorkspace route={routes.signup} />} />
</Route>
{/* <Route element={<ProtectedLayout />}> */}
<Route path={routes.home} element={<Home />} />
<Route path={routes.projectId} element={<ProjectSpecificLayout />}>
<Route path={routes.flow} element={<FlowDiagram />} />
<Route path={routes.project} element={<Playground />} />
<Route path={routes.prototype} element={<Prototype />} />
</Route>
<Route path='*' element={<Home />} />
{/* </Route> */}
</Routes>
</BrowserRouter>
</ToastBarProvider>
</Box>
);
}
export default App;

View File

@@ -0,0 +1,55 @@
/* eslint-disable react-hooks/exhaustive-deps */
import type { Extension } from '@codemirror/state';
import { Box } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useEffect, useContext } from 'react';
import { updatePayloadAction, context } from '../../Context';
import type { IPayload } from '../../Context/initialState';
import useCodeMirror from '../../hooks/useCodeMirror';
import useFormatCodeMirrorValue from '../../hooks/useFormatCodeMirrorValue';
import intendCode from '../../utils/intendCode';
type CodeMirrorProps = {
extensions?: Extension[];
};
const BlockEditor = ({ extensions }: CodeMirrorProps) => {
const {
state: { screens, activeScreen },
dispatch,
} = useContext(context);
const { editor, changes, setValue } = useCodeMirror(extensions, intendCode(screens[activeScreen]?.payload));
const debounceValue = useDebouncedValue(changes, 1500);
useFormatCodeMirrorValue((parsedCode: IPayload, prettifiedCode: { formatted: string; cursorOffset: number }) => {
dispatch(
updatePayloadAction({
blocks: parsedCode.blocks,
surface: parsedCode.surface,
}),
);
setValue(prettifiedCode.formatted, {
cursor: prettifiedCode.cursorOffset,
});
}, debounceValue);
useEffect(() => {
if (!screens[activeScreen]?.changedByEditor) {
setValue(intendCode(screens[activeScreen]?.payload), {});
}
}, [screens[activeScreen]?.payload.blocks, screens[activeScreen]?.payload.surface, activeScreen]);
useEffect(() => {
setValue(intendCode(screens[activeScreen]?.payload), {});
}, [activeScreen]);
return (
<>
<Box display='grid' height='100%' width='100%' ref={editor} />
</>
);
};
export default BlockEditor;

View File

@@ -0,0 +1,25 @@
import { json } from '@codemirror/lang-json';
import { EditorView } from 'codemirror';
import highlightStyle from './HighlightStyle';
import basicSetup from './basicSetup';
import jsonLinter from './jsonLinter';
// import payloadLinter from './payloadLinter';
import theme from './theme';
export const actionBlockExtensions = [
highlightStyle,
json(),
jsonLinter,
basicSetup,
// payloadLinter,
...theme,
];
export const actionPreviewExtensions = [
EditorView.contentAttributes.of({ contenteditable: 'false' }),
highlightStyle,
json(),
basicSetup,
...theme,
];

View File

@@ -0,0 +1,15 @@
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
const highLightStyle = () => {
const style = HighlightStyle.define([
{ tag: t.literal, color: 'var(--RCPG-primary-color)' },
{ tag: t.bool, color: 'var(--RCPG-tertary-color)' },
{ tag: t.number, color: 'var(--RCPG-secondary-color)' },
{ tag: t.null, color: 'var(--RCPG-tertary-color)' },
]);
return syntaxHighlighting(style);
};
export default highLightStyle();

View File

@@ -0,0 +1,35 @@
import { completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language';
import { lintKeymap } from '@codemirror/lint';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import type { Extension } from '@codemirror/state';
import { keymap, drawSelection, dropCursor, rectangularSelection, crosshairCursor, lineNumbers, EditorView } from '@codemirror/view';
const basicSetup: Extension = (() => [
lineNumbers(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
indentOnInput(),
EditorView.lineWrapping,
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
rectangularSelection(),
crosshairCursor(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
indentWithTab,
]),
])();
export default basicSetup;

View File

@@ -0,0 +1,9 @@
import { javascript } from '@codemirror/lang-javascript';
import highlightStyle from './HighlightStyle';
import basicSetup from './basicSetup';
import theme from './theme';
const extensions = [highlightStyle, javascript(), basicSetup, ...theme];
export default extensions;

View File

@@ -0,0 +1,4 @@
import { jsonParseLinter } from '@codemirror/lang-json';
import { lintGutter, linter } from '@codemirror/lint';
export default [lintGutter(), linter(jsonParseLinter())];

View File

@@ -0,0 +1,60 @@
import { syntaxTree } from '@codemirror/language';
import type { Diagnostic } from '@codemirror/lint';
import { linter } from '@codemirror/lint';
import type { EditorView } from 'codemirror';
import parsePayload from '../Parser';
const payloadLinter = linter((view: EditorView) => {
const diagnostics: Diagnostic[] = [];
const tree = syntaxTree(view.state);
let head = tree.topNode.firstChild;
if (!head?.matchContext(['Script'])) {
diagnostics.push({
from: 0,
to: 0,
message: 'Expecting a Script',
severity: 'error',
});
return diagnostics;
}
head = head.firstChild;
if (!head?.matchContext(['ExpressionStatement'])) {
diagnostics.push({
from: 0,
to: 0,
message: 'Expecting an expression statement',
severity: 'error',
});
return diagnostics;
}
head = head.firstChild;
if (!head?.matchContext(['ArrayExpression'])) {
diagnostics.push({
from: 0,
to: 0,
message: 'Expecting an array expression',
severity: 'error',
});
return diagnostics;
}
// while (head.nextSibling && head.nextSibling.name !== ']') {
// }
do {
if (head.name !== '[' && head.name !== ',' && head.name !== ']' && head.name !== 'ObjectExpression') {
diagnostics.push({
from: head.from,
to: head.to,
message: 'Expecting an Object expression',
severity: 'error',
});
return diagnostics;
}
if (head.name === 'ObjectExpression') parsePayload(head, view);
head = head.nextSibling;
} while (head);
return diagnostics;
});
export default payloadLinter;

View File

@@ -0,0 +1,41 @@
import type { Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
const gutters: Extension = EditorView.theme({
'.cm-gutters': {
backgroundColor: 'transparent',
border: 'none',
userSelect: 'none',
minWidth: '32px',
display: 'flex',
justifyContent: 'flex-end',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
},
});
const selection: Extension = EditorView.theme({
'.cm-selectionBackground': {
backgroundColor: 'var(--RCPG-secondary-color) !important',
opacity: 0.3,
},
'.cm-selectionMatch': {
backgroundColor: '#74808930 !important',
},
'.cm-matchingBracket': {
backgroundColor: 'transparent !important',
border: '1px solid #1d74f580',
},
});
const line: Extension = EditorView.theme({
'.cm-activeLine': {
backgroundColor: 'transparent !important',
},
});
export default [gutters, selection, line] as const;

View File

@@ -0,0 +1 @@
export { default } from './parsePayload';

View File

@@ -0,0 +1,7 @@
import type { EditorView } from 'codemirror';
const parsePayload = (head: { from: number; to: number }, view: EditorView) => {
JSON.parse(view.state.doc.toString().slice(head.from, head.to));
};
export default parsePayload;

View File

@@ -0,0 +1,31 @@
import type { Extension } from '@codemirror/state';
import { Box } from '@rocket.chat/fuselage';
import { useEffect, useContext } from 'react';
import { context } from '../../Context';
import useCodeMirror from '../../hooks/useCodeMirror';
import intendCode from '../../utils/intendCode';
type CodeMirrorProps = {
extensions?: Extension[];
};
const PreviewEditor = ({ extensions }: CodeMirrorProps) => {
const {
state: { screens, activeScreen },
} = useContext(context);
const { editor, setValue } = useCodeMirror(extensions, intendCode(screens[activeScreen]?.actionPreview));
useEffect(() => {
setValue(intendCode(screens[activeScreen]?.actionPreview), {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screens[activeScreen]?.actionPreview]);
return (
<>
<Box display='grid' height='100%' width='100%' ref={editor} />
</>
);
};
export default PreviewEditor;

View File

@@ -0,0 +1,68 @@
import type { Extension } from '@codemirror/state';
import { Box } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import json5 from 'json5';
import { useEffect, useContext } from 'react';
import { updatePayloadAction, context } from '../../Context';
import type { ILayoutBlock } from '../../Context/initialState';
import useCodeMirror from '../../hooks/useCodeMirror';
import codePrettier from '../../utils/codePrettier';
type CodeMirrorProps = {
extensions?: Extension[];
};
const CodeEditor = ({ extensions }: CodeMirrorProps) => {
const {
state: { screens, activeScreen },
dispatch,
} = useContext(context);
const { editor, changes, setValue } = useCodeMirror(extensions, json5.stringify(screens[activeScreen].payload, undefined, 4));
const debounceValue = useDebouncedValue(changes?.value, 1500);
useEffect(() => {
if (!changes?.isDispatch) {
try {
const parsedCode: ILayoutBlock[] = json5.parse(changes.value);
dispatch(
updatePayloadAction({
blocks: parsedCode,
changedByEditor: false,
}),
);
dispatch(updatePayloadAction({ blocks: parsedCode }));
} catch (e) {
// do nothing
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [changes?.value]);
useEffect(() => {
if (!changes?.isDispatch) {
void codePrettier(changes.value, changes.cursor || 0).then((prettierCode) => {
setValue(prettierCode.formatted, {
cursor: prettierCode.cursorOffset,
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debounceValue]);
useEffect(() => {
if (!screens[activeScreen].changedByEditor) {
setValue(JSON.stringify(screens[activeScreen].payload, undefined, 4), {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screens[activeScreen].payload]);
return (
<>
<Box display='grid' height='100%' width='100%' ref={editor} />
</>
);
};
export default CodeEditor;

View File

@@ -0,0 +1,23 @@
import { css } from '@rocket.chat/css-in-js';
import { Scrollable, Box } from '@rocket.chat/fuselage';
import BlocksTree from '../../Payload/actionBlock/BlocksTree';
import DropDown from '../DropDown';
const ScrollableSideBar = () => (
<Scrollable vertical>
<Box
width='100%'
marginBlockStart='39px'
borderBlockStart='var(--default-border)'
backgroundColor='var(--primaryBackgroundColor)'
className={css`
box-shadow: var(--dropdown-box-shadow);
`}
>
<DropDown blocksTree={BlocksTree} />
</Box>
</Scrollable>
);
export default ScrollableSideBar;

View File

@@ -0,0 +1,37 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useEffect, useContext } from 'react';
import ScrollableSideBar from './ScrollableSideBar';
import SliderBtn from './SliderBtn';
import { context, sidebarToggleAction } from '../../Context';
const SideBar = () => {
const { state, dispatch } = useContext(context);
useEffect(() => {
dispatch(sidebarToggleAction(false));
}, [state?.isMobile, dispatch]);
const slide = state?.isMobile
? css`
width: 100%;
user-select: none;
transform: translateX(${state?.sideBarToggle ? '0' : '-100%'});
transition: var(--animation-default);
`
: css`
width: var(--sidebar-width);
user-select: none;
transition: var(--animation-default);
`;
return (
<Box position='absolute' height='100%' display='flex' zIndex={4} backgroundColor='#fff' className={[slide]}>
<SliderBtn />
<ScrollableSideBar />
</Box>
);
};
export default SideBar;

View File

@@ -0,0 +1,90 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Label } from '@rocket.chat/fuselage';
import { useContext, useMemo } from 'react';
import { context, sidebarToggleAction } from '../../Context';
const SliderBtn = () => {
const {
state: { sideBarToggle, isMobile },
dispatch,
} = useContext(context);
const slideBtnAnimation = sideBarToggle
? css`
clip-path: polygon(10% 0, 50% 40%, 90% 0, 100% 10%, 60% 50%, 100% 90%, 90% 100%, 50% 60%, 10% 100%, 0 90%, 40% 50%, 0 10%);
cursor: pointer;
transition: var(--animation-default);
`
: css`
clip-path: polygon(32% 35%, 32% 35%, 79% 0, 87% 10%, 32% 50%, 87% 90%, 79% 100%, 32% 64%, 32% 65%, 13% 50%, 13% 50%, 13% 50%);
transform: rotate(180deg);
transition: var(--animation-default);
`;
const toggleStyle = useMemo(() => {
if (!isMobile) {
return css`
left: 0px;
`;
}
if (sideBarToggle) {
return css`
right: 0;
transition: var(--animation-default);
`;
}
return css`
right: 0;
transform: translateX(100%);
cursor: pointer;
transition: var(--animation-default);
`;
}, [isMobile, sideBarToggle]);
return (
<Box
position='absolute'
width={sideBarToggle ? '100%' : '130px'}
paddingInlineStart='20px'
height='40px'
display='flex'
alignItems='center'
justifyContent='space-between'
onClick={() => !sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle))}
zIndex={1}
className={toggleStyle}
>
<Label
width='80px'
className={
isMobile && !sideBarToggle
? css`
cursor: pointer;
`
: ''
}
>
Components
</Label>
{isMobile && (
<Box
width='40px'
height='40px'
display='flex'
alignItems='center'
justifyContent='center'
onClick={() => sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle))}
className={css`
cursor: pointer;
`}
>
<Box size='50%' backgroundColor='info' className={slideBtnAnimation} />
</Box>
)}
</Box>
);
};
export default SliderBtn;

View File

@@ -0,0 +1 @@
export { default } from './SideBar';

View File

@@ -0,0 +1,103 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Button, Icon, Scrollable } from '@rocket.chat/fuselage';
import { useOutsideClick, useMergedRefs } from '@rocket.chat/fuselage-hooks';
import { useContext, useRef } from 'react';
import ScreenThumbnail from './ScreenThumbnail';
import { context } from '../../Context';
import { openCreateNewScreenAction, createNewScreenAction } from '../../Context/action';
import { useHorizontalScroll } from '../../hooks/useHorizontalScroll';
const CreateNewScreenContainer = () => {
const {
state: { projects, screens, activeProject, openCreateNewScreen },
dispatch,
} = useContext(context);
const ref = useRef(null);
const onClosehandler = () => {
dispatch(openCreateNewScreenAction(false));
};
useOutsideClick([ref], onClosehandler);
const scrollRef = useHorizontalScroll();
const mergedRef = useMergedRefs(scrollRef, ref);
const createNewScreenhandler = () => {
dispatch(createNewScreenAction());
};
return (
<Scrollable horizontal>
<Box
ref={mergedRef}
width='100%'
height='250px'
borderBlockEnd='var(--default-border)'
position='fixed'
paddingInline='40px'
className={css`
top: -255px;
left: 0;
z-index: 10;
align-items: center;
display: flex;
justify-content: center;
background-color: var(--primaryBackgroundColor);
box-shadow: var(--elements-box-shadow);
transform: translateY(${openCreateNewScreen ? '255px' : '0px'});
transition: var(--animation-default);
`}
>
<Button
position='fixed'
square
tiny
className={css`
top: 20px;
right: 20px;
`}
onClick={onClosehandler}
>
<Icon name='cross' size='x15' />
</Button>
{openCreateNewScreen && (
<Box
width='max-content'
display='flex'
className={css`
gap: 50px;
align-items: center;
`}
>
{projects[activeProject]?.screens
.map((id) => screens[id])
.map((screen, i) => (
<ScreenThumbnail
screen={screen}
key={i}
disableDelete={projects[activeProject]?.screens.map((id) => screens[id]).length <= 1}
/>
))}
<Icon
onClick={createNewScreenhandler}
size='60px'
height='60px'
name='plus'
className={css`
cursor: pointer;
transition: var(--animation-default);
&:hover {
scale: 1.1;
transition: var(--animation-default);
}
`}
/>
</Box>
)}
</Box>
</Scrollable>
);
};
export default CreateNewScreenContainer;

View File

@@ -0,0 +1,73 @@
import { Box } from '@rocket.chat/fuselage';
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
import type { ChangeEvent, MouseEvent } from 'react';
import { useContext, useState } from 'react';
import { context, renameScreenAction } from '../../Context';
import { activeScreenAction } from '../../Context/action/activeScreenAction';
import { deleteScreenAction } from '../../Context/action/deleteScreenAction';
import { duplicateScreenAction } from '../../Context/action/duplicateScreenAction';
import type { ScreenType } from '../../Context/initialState';
import renderPayload from '../RenderPayload/RenderPayload';
import EditMenu from '../ScreenThumbnail/EditMenu/EditMenu';
import ScreenThumbnailWrapper from '../ScreenThumbnail/ScreenThumbnailWrapper';
import Thumbnail from '../ScreenThumbnail/Thumbnail';
export type ScreenThumbnailProps = { screen: ScreenType; disableDelete: boolean };
const ScreenThumbnail = ({ screen, disableDelete }: ScreenThumbnailProps) => {
const { dispatch } = useContext(context);
const [name, setName] = useState<string>(screen?.name);
const toast = useToastBarDispatch();
const activateScreenHandler = (e: MouseEvent) => {
e.stopPropagation();
dispatch(activeScreenAction(screen?.id));
};
const duplicateScreenHandler = () => {
dispatch(duplicateScreenAction({ id: screen?.id }));
};
const onChangeNameHandler = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.currentTarget.value);
};
const nameSaveHandler = () => {
if (!name.trim()) {
setName(screen.name);
return toast({
type: 'error',
message: 'Cannot rename screen to empty name.',
});
}
dispatch(renameScreenAction({ id: screen.id, name }));
};
const deleteScreenHandler = () => {
if (disableDelete)
return toast({
type: 'info',
message: 'Cannot delete last screen.',
});
dispatch(deleteScreenAction(screen?.id));
};
return (
<Box position='relative'>
<EditMenu
name={name}
date={screen.date}
onChange={onChangeNameHandler}
onDuplicate={duplicateScreenHandler}
onDelete={deleteScreenHandler}
onBlur={nameSaveHandler}
labelProps={{ fontScale: 'h5' }}
/>
<ScreenThumbnailWrapper onClick={activateScreenHandler}>
<Thumbnail of={renderPayload({ blocks: screen.payload.blocks })} />
</ScreenThumbnailWrapper>
</Box>
);
};
export default ScreenThumbnail;

View File

@@ -0,0 +1 @@
export { default } from './CreateNewScreenContainer';

View File

@@ -0,0 +1,35 @@
import type { OnDragEndResponder } from '@hello-pangea/dnd';
import { DragDropContext, Droppable } from '@hello-pangea/dnd';
import { memo } from 'react';
import DraggableListItem from './DraggableListItem';
import type { ILayoutBlock } from '../../Context/initialState';
import { SurfaceOptions } from '../Preview/Display/Surface/constant';
export type Block = {
id: string;
payload: ILayoutBlock;
};
export type DraggableListProps = {
blocks: Block[];
surface?: SurfaceOptions;
onDragEnd: OnDragEndResponder;
};
const DraggableList = ({ blocks, surface, onDragEnd }: DraggableListProps) => (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='droppable-list'>
{(provided) => (
<div style={{ padding: '10px' }} ref={provided.innerRef} {...provided.droppableProps}>
{blocks.map((block, index) => (
<DraggableListItem surface={surface || SurfaceOptions.Message} block={block} index={index} key={block.id} />
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
export default memo(DraggableList);

View File

@@ -0,0 +1,27 @@
import { Draggable } from '@hello-pangea/dnd';
import type { Block } from './DraggableList';
import DeleteElementBtn from '../Preview/Display/UiKitElementWrapper/DeleteElementBtn';
import UiKitElementWrapper from '../Preview/Display/UiKitElementWrapper/UiKitElementWrapper';
import RenderPayload from '../RenderPayload/RenderPayload';
export type DraggableListItemProps = {
block: Block;
surface: number;
index: number;
};
const DraggableListItem = ({ block, surface, index }: DraggableListItemProps) => (
<Draggable draggableId={block.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<UiKitElementWrapper key={index}>
<DeleteElementBtn elementIndex={index} />
<RenderPayload surface={surface} blocks={[block.payload]} />
</UiKitElementWrapper>
</div>
)}
</Draggable>
);
export default DraggableListItem;

View File

@@ -0,0 +1,31 @@
import { Box } from '@rocket.chat/fuselage';
import { Fragment } from 'react';
import Items from './Items';
import type { Item, ItemBranch } from './types';
export type DropDownProps = {
readonly blocksTree: Item;
};
const DropDown = ({ blocksTree }: DropDownProps) => {
const layer = 1;
const recursiveComponentTree = (branch: ItemBranch, layer: number) => (
<Items layer={layer} label={branch.label} payload={branch.payload}>
{branch.branches?.map((branch: ItemBranch, index: number) => (
<Fragment key={index}>{recursiveComponentTree(branch, layer + 1)}</Fragment>
))}
</Items>
);
return (
<Box paddingBlockStart='15px' paddingBlockEnd='30px'>
{blocksTree.map((branch: ItemBranch, i: number) => (
<Box key={i}>{recursiveComponentTree(branch, layer)}</Box>
))}
</Box>
);
};
export default DropDown;

View File

@@ -0,0 +1,62 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Label, Chevron } from '@rocket.chat/fuselage';
import { useState, useContext } from 'react';
import ItemsIcon from './ItemsIcon';
import { itemStyle, labelStyle } from './itemsStyle';
import type { ItemProps } from './types';
import { context, updatePayloadAction } from '../../Context';
import getUniqueId from '../../utils/getUniqueId';
const Items = ({ label, children, layer, payload }: ItemProps) => {
const [isOpen, toggleItemOpen] = useState<boolean>(layer === 1);
const [hover, setHover] = useState<boolean>(false);
const { state, dispatch } = useContext(context);
const itemClickHandler = () => {
toggleItemOpen(!isOpen);
if (!payload) return;
dispatch(
updatePayloadAction({
blocks: [...state.screens[state.activeScreen].payload.blocks, { actionId: getUniqueId(), ...payload[0] }],
changedByEditor: false,
}),
);
};
return (
<Box marginBlockEnd={layer === 1 ? '10px' : '0px'}>
<Box
display='flex'
alignItems='center'
className={itemStyle(layer, hover)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={itemClickHandler}
>
<Box size='x16' display='flex' alignItems='center'>
{children && children.length > 0 && (
<Box
size='x16'
display='flex'
alignItems='center'
className={css`
transform: rotate(${!isOpen ? '-90deg' : '0deg'});
transition: var(--animation-very-fast);
`}
>
<Chevron width='12px' color={hover ? '#fff' : '#000'} />
</Box>
)}
</Box>
<Box height='25px' display='flex' alignItems='center'>
<ItemsIcon layer={layer} hover={hover} lastNode={children === undefined} />
<Label className={labelStyle(layer, hover)}>{label}</Label>
</Box>
</Box>
<Box>{isOpen && children}</Box>
</Box>
);
};
export default Items;

View File

@@ -0,0 +1,18 @@
import { Icon } from '@rocket.chat/fuselage';
export type ItemsIconProps = { layer: number; lastNode: boolean; hover: boolean };
const ItemsIcon = ({ layer, lastNode, hover }: ItemsIconProps) => {
const selectIcon = (layer: number, hover: boolean) => {
if (layer === 1) {
return <Icon name='folder' size='x12' color={hover ? '#fff' : '#1d74f5'} />;
}
if (lastNode) {
return <Icon name='cube' size='x12' color={hover ? '#fff' : '#f5455c'} />;
}
return <Icon name='burger' size='x12' color={hover ? '#fff' : '#19ac7c'} />;
};
return <>{selectIcon(layer, hover)}</>;
};
export default ItemsIcon;

View File

@@ -0,0 +1 @@
export { default } from './DropDown';

View File

@@ -0,0 +1,45 @@
import { css } from '@rocket.chat/css-in-js';
export const itemStyle = (layer: number, hover: boolean) => {
const style = css`
cursor: pointer !important;
padding-left: ${10 + (layer - 1) * 16}px !important;
background-color: ${hover ? 'var(--RCPG-primary-color) !important' : 'transparent !important'};
`;
return style;
};
export const labelStyle = (layer: number, hover: boolean) => {
let customStyle;
const basicStyle = css`
cursor: pointer !important;
padding-left: 4px !important;
`;
switch (layer) {
case 1:
customStyle = css`
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: 0.3px !important;
color: ${hover ? '#fff !important' : '#999 !important'};
text-transform: uppercase !important;
`;
break;
case 2:
customStyle = css`
letter-spacing: 0.1px !important;
font-size: 12px !important;
color: ${hover ? '#fff !important' : '#555 !important'};
text-transform: capitalize !important;
`;
break;
default:
customStyle = css`
font-size: 12px !important;
color: ${hover ? '#fff !important' : '#555 !important'};
text-transform: capitalize !important;
`;
break;
}
return [customStyle, basicStyle];
};

View File

@@ -0,0 +1,17 @@
import type { LayoutBlock } from '@rocket.chat/ui-kit';
import type { JSX } from 'react';
export type ItemProps = {
label: string;
layer: number;
payload?: readonly LayoutBlock[];
children?: ReadonlyArray<JSX.Element>;
};
export type ItemBranch = {
label: string;
branches?: Item;
payload?: readonly LayoutBlock[];
};
export type Item = ItemBranch[];

View File

@@ -0,0 +1,27 @@
const ConnectionLine = ({
fromX,
fromY,
toX,
toY,
}: {
fromX: number;
fromY: number;
fromPosition: string;
toX: number;
toY: number;
toPosition: string;
connectionLineType: string;
}) => (
<g>
<path
fill='none'
stroke='#222'
strokeWidth={1.5}
className='animated'
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill='#fff' r={2} stroke='#222' strokeWidth={1.5} />
</g>
);
export default ConnectionLine;

View File

@@ -0,0 +1,7 @@
import { Controls } from 'reactflow';
const ControlButtons = () => {
return <Controls></Controls>;
};
export default ControlButtons;

View File

@@ -0,0 +1 @@
export { default } from './ControlButtons';

View File

@@ -0,0 +1,115 @@
import { useCallback, useContext, useMemo, useRef, useState } from 'react';
import ReactFlow, {
MiniMap,
Background,
addEdge,
updateEdge,
type Node,
type Viewport,
type ReactFlowInstance,
useReactFlow,
type Connection,
type Edge,
} from 'reactflow';
import 'reactflow/dist/style.css';
import ConnectionLine from './ConnectionLine';
import ControlButton from './ControlButtons';
import UIKitWrapper from './UIKitWrapper/UIKitWrapper';
import { FlowParams } from './utils';
import { context } from '../../Context';
import { updateNodesAndViewPortAction } from '../../Context/action/updateNodesAndViewPortAction';
import { useNodesAndEdges } from '../../hooks/useNodesAndEdges';
const FlowContainer = () => {
const { dispatch } = useContext(context);
const { nodes, edges, Viewport, onNodesChange, onEdgesChange, setEdges } = useNodesAndEdges();
const { setViewport } = useReactFlow();
const nodeTypes = useMemo(
() => ({
custom: UIKitWrapper,
}),
// used to rerender edge lines on reorder payload
// eslint-disable-next-line react-hooks/exhaustive-deps
[edges],
);
const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
const edgeUpdateSuccessful = useRef(true);
const onConnect = useCallback(
(connection: Connection) => {
if (connection.source === connection.target) return;
const newEdge = {
...connection,
type: FlowParams.edgeType,
markerEnd: FlowParams.markerEnd,
style: FlowParams.style,
};
setEdges((eds) => addEdge(newEdge, eds));
},
[setEdges],
);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
const onEdgeUpdate = useCallback(
(oldEdge: Edge<unknown>, newConnection: Connection) => {
edgeUpdateSuccessful.current = true;
setEdges((els) => updateEdge(oldEdge, newConnection, els));
},
[setEdges],
);
const onEdgeUpdateEnd = useCallback(
(_: MouseEvent | TouchEvent, edge: Edge<unknown>) => {
if (!edgeUpdateSuccessful.current) {
setEdges((eds) => {
return eds.filter((e) => e.id !== edge.id);
});
}
edgeUpdateSuccessful.current = true;
},
[setEdges],
);
const onNodeDragStop = () => {
if (!rfInstance?.toObject()) return;
const { nodes, viewport }: { nodes: Node[]; viewport: Viewport } = rfInstance.toObject();
dispatch(updateNodesAndViewPortAction({ nodes, viewport }));
};
const onInit = (instance: ReactFlowInstance) => {
setRfInstance(instance);
if (Viewport) setViewport(Viewport);
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onEdgeUpdate={onEdgeUpdate}
onNodeDragStop={onNodeDragStop}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
onConnect={onConnect}
fitView
nodeTypes={nodeTypes}
minZoom={0.1}
connectionLineComponent={ConnectionLine}
>
<MiniMap zoomable pannable />
<ControlButton />
<Background color='#aaa' gap={16} />
</ReactFlow>
);
};
export default FlowContainer;

View File

@@ -0,0 +1,7 @@
.rc-scrollbars-view{
position: relative !important;
overflow: hidden !important;
width: 100% !important;
z-index: 1000 !important;
margin: 0 !important;
}

View File

@@ -0,0 +1,36 @@
import { Box } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import { Handle, Position } from 'reactflow';
import './UIKitWrapper.scss';
import { context } from '../../../Context';
import type { idType } from '../../../Context/initialState';
import SurfaceRender from '../../Preview/Display/Surface/SurfaceRender';
import RenderPayload from '../../RenderPayload/RenderPayload';
export type UIKitWrapperProps = { id: string; data: idType };
const UIKitWrapper = ({ id, data }: UIKitWrapperProps) => {
const {
state: { screens },
} = useContext(context);
if (!screens[data]) return null;
const { blocks, surface } = screens[data].payload;
return (
<Box padding='10px' border='var(--default-border)' backgroundColor='white' borderRadius='10px'>
<Handle type='target' className='react-flow-targetHandle' position={Position.Left} id={`${id}`} />
<SurfaceRender type={surface}>
{blocks.map((block, index) => (
<Box key={index} paddingInlineEnd='6px' className='uiKitWrapper'>
<Box position='relative' border='var(--default-border)' padding='10px'>
<RenderPayload blocks={[block]} surface={surface} />
<Handle type='source' className='react-flow-sourceHandle' position={Position.Right} id={block.actionId} />
</Box>
</Box>
))}
</SurfaceRender>
</Box>
);
};
export default UIKitWrapper;

View File

@@ -0,0 +1 @@
export { default } from './UIKitWrapper';

View File

@@ -0,0 +1 @@
export { default } from './FlowContainer';

View File

@@ -0,0 +1,34 @@
import { MarkerType } from 'reactflow';
import type { ScreenType } from '../../Context/initialState';
export function createNodesAndEdges(screens: ScreenType[]) {
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
const nodes = screens.map((screen, i) => {
const degrees = i * (360 / 8);
const radians = degrees * (Math.PI / 180);
const x = 250 * Math.cos(radians) + center.x;
const y = 250 * Math.sin(radians) + center.y;
return {
id: screen.id,
type: 'custom',
position: { x, y },
data: screen,
};
});
return { nodes };
}
export const FlowParams = {
edgeType: 'smoothstep',
markerEnd: {
type: MarkerType.Arrow,
},
style: {
strokeWidth: 2,
stroke: 'var(--RCPG-primary-color)',
},
};

View File

@@ -0,0 +1,32 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Label } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import ProjectsList from './ProjectsList/ProjectsList';
import { context, createNewProjectAction } from '../../Context';
import CreateNewScreenButton from '../ScreenThumbnail/CreateNewScreenButton';
const HomeContainer = () => {
const { dispatch } = useContext(context);
return (
<Box minWidth='100%' display='flex' justifyContent='center' height='var(--content-height)' paddingBlockStart='30px'>
<Box
width='max-content'
minWidth='70%'
display='flex'
flexDirection='column'
className={css`
gap: 30px;
`}
>
<Label fontScale='h1'>Projects</Label>
<Label fontScale='h3'>Start a new project</Label>
<CreateNewScreenButton name='plus' onClick={() => dispatch(createNewProjectAction())} />
<Label fontScale='h4'>Existing Projects</Label>
<ProjectsList />
</Box>
</Box>
);
};
export default HomeContainer;

View File

@@ -0,0 +1,35 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import ProjectsThumbnail from './ProjectsThumbnail';
import { context } from '../../../Context';
const ProjectsList = () => {
const {
state: { screens, projects },
} = useContext(context);
return (
<Box
className={css`
display: grid;
grid-template-columns: repeat(auto-fill, 200px);
gap: 30px;
`}
paddingBlockEnd='30px'
>
{Object.values(projects).map((project) => (
<ProjectsThumbnail
key={project.id}
id={project.id}
name={project.name}
date={project.date}
blocks={screens[project.screens[0]].payload.blocks}
/>
))}
</Box>
);
};
export default ProjectsList;

View File

@@ -0,0 +1,78 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
import { type ChangeEvent, useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { activeProjectAction, context, renameProjectAction } from '../../../Context';
import { deleteProjectAction } from '../../../Context/action/deleteProjectAction';
import type { ILayoutBlock } from '../../../Context/initialState';
import routes from '../../../Routes/Routes';
import { formatDate } from '../../../utils/formatDate';
import RenderPayload from '../../RenderPayload/RenderPayload';
import EditMenu from '../../ScreenThumbnail/EditMenu';
import EditableLabel from '../../ScreenThumbnail/EditableLabel/EditableLabel';
import ScreenThumbnailWrapper from '../../ScreenThumbnail/ScreenThumbnailWrapper';
import Thumbnail from '../../ScreenThumbnail/Thumbnail';
export type ProjectsThumbnailProps = { id: string; name: string; date: string; blocks: ILayoutBlock[] };
const ProjectsThumbnail = ({ id, name: _name, date, blocks }: ProjectsThumbnailProps) => {
const [name, setName] = useState<string>(_name);
const navigate = useNavigate();
const { dispatch } = useContext(context);
const toast = useToastBarDispatch();
const activeProjectHandler = () => {
dispatch(activeProjectAction(id));
navigate(`/${id}/${routes.project}`);
};
const deleteScreenHandler = () => {
dispatch(deleteProjectAction(id));
};
const onChangeNameHandler = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.currentTarget.value);
};
const nameSaveHandler = () => {
if (!name.trim()) {
setName(_name);
return toast({
type: 'error',
message: 'Cannot rename project to empty name.',
});
}
dispatch(renameProjectAction({ id, name }));
};
return (
<Box
display='flex'
flexDirection='column'
className={css`
gap: 5px;
`}
position='relative'
>
<EditMenu
name={name}
date={date}
onChange={onChangeNameHandler}
onDelete={deleteScreenHandler}
onBlur={nameSaveHandler}
labelProps={{ fontScale: 'h5' }}
/>
<ScreenThumbnailWrapper onClick={activeProjectHandler} width='200px' height='260px' padding='30px'>
<Thumbnail of={<RenderPayload blocks={blocks} />} />
<Box onClick={(e) => e.stopPropagation()}></Box>
</ScreenThumbnailWrapper>
<EditableLabel value={name} onChange={onChangeNameHandler} fontScale='h5' onBlur={nameSaveHandler} />
<Box withTruncatedText fontScale='p2'>
{formatDate(date)}
</Box>
</Box>
);
};
export default ProjectsThumbnail;

View File

@@ -0,0 +1 @@
export { default } from './ProjectsList';

View File

@@ -0,0 +1 @@
export { default } from './HomeContainer';

View File

@@ -0,0 +1,27 @@
import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
import type { ReactNode } from 'react';
import { useContext } from 'react';
import Line from './Line';
import Wrapper from './Wrapper';
import { context } from '../../../Context';
export type BurgerIconProps = { children?: ReactNode };
const BurgerIcon = ({ children }: BurgerIconProps) => {
const isReducedMotionPreferred = usePrefersReducedMotion();
const {
state: { navMenuToggle },
} = useContext(context);
return (
<Wrapper>
<Line animated={!isReducedMotionPreferred} moved={navMenuToggle} />
<Line animated={!isReducedMotionPreferred} moved={navMenuToggle} />
<Line animated={!isReducedMotionPreferred} moved={navMenuToggle} />
{children}
</Wrapper>
);
};
export default BurgerIcon;

View File

@@ -0,0 +1,40 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
export type LineProps = { animated: boolean; moved?: boolean };
const Line = ({ animated, moved }: LineProps) => {
const animatedStyle = animated
? css`
will-change: transform;
transition: transform 0.1s ease-out;
`
: '';
const movedStyle = moved
? css`
&:nth-child(1),
&:nth-child(3) {
transform-origin: 50%, 50%, 0;
}
&:nth-child(1) {
transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1);
}
[dir='rtl'] &:nth-child(1) {
transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1);
}
&:nth-child(3) {
transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1);
}
[dir='rtl'] &:nth-child(3) {
transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1);
}
`
: '';
return (
<Box is='span' width='x20' height='x2' backgroundColor='currentColor' className={[animatedStyle, movedStyle]} aria-hidden='true' />
);
};
export default Line;

View File

@@ -0,0 +1,27 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type WrapperProps = { children: ReactNode };
const Wrapper = ({ children }: WrapperProps) => (
<Box
is='span'
display='inline-flex'
flexDirection='column'
alignItems='center'
justifyContent='space-between'
paddingBlock={4}
paddingInline={2}
verticalAlign='middle'
height='x24'
className={css`
cursor: pointer;
`}
width='x24'
>
{children}
</Box>
);
export default Wrapper;

View File

@@ -0,0 +1 @@
export { default } from './BurgerIcon';

View File

@@ -0,0 +1,5 @@
import { Box } from '@rocket.chat/fuselage';
const Divider = () => <Box height='70%' width='1px' backgroundColor='#c7c7c7' />;
export default Divider;

View File

@@ -0,0 +1,12 @@
import { Box } from '@rocket.chat/fuselage';
import { RocketChatLogo } from '@rocket.chat/logo';
const Logo = () => (
<Box display='flex' justifyContent='center' height='100%' width='var(--sidebar-width)'>
<Box height='100%' width='80%'>
<RocketChatLogo />
</Box>
</Box>
);
export default Logo;

View File

@@ -0,0 +1,95 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Tile, FlexContainer, ButtonGroup, Button, Icon } from '@rocket.chat/fuselage';
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import RightNavBtn from './RightNavBtn';
import { context, updatePayloadAction } from '../../Context';
import { openCreateNewScreenAction } from '../../Context/action/openCreateNewScreenAction';
import routes from '../../Routes/Routes';
const NabBar = () => {
const {
state: { isMobile, screens, activeScreen },
dispatch,
} = useContext(context);
const toast = useToastBarDispatch();
const navigate = useNavigate();
return (
<FlexContainer alignItems='center'>
<Box
position='relative'
width='100%'
height='var(--navbar-height)'
is={Tile}
padding={0}
zIndex={3}
elevation='2'
className={css`
user-select: none;
`}
>
<Button primary onClick={() => navigate(routes.home)} marginInlineStart='20px' small square>
<Icon name='home' size='x24' />
</Button>
{!isMobile && (
<Button
marginInlineStart='12px'
small
success
onClick={() => {
dispatch(openCreateNewScreenAction(true));
}}
>
Screens
</Button>
)}
<Box flexGrow={1} minWidth='15px' display='flex' justifyContent='flex-end'>
<Box display='flex' height='100%'>
{!isMobile && (
<ButtonGroup>
<Button
secondary
success
small
onClick={async () => {
await navigator.clipboard.writeText(JSON.stringify(screens[activeScreen]?.payload));
toast({
type: 'success',
message: 'Payload Copied',
});
}}
>
Copy Payload
</Button>
<Button
small
danger
onClick={() => {
dispatch(
updatePayloadAction({
blocks: [],
changedByEditor: false,
}),
);
toast({
type: 'success',
message: 'All Blocks Cleared',
});
}}
>
Clear Blocks
</Button>
</ButtonGroup>
)}
</Box>
</Box>
<RightNavBtn />
</Box>
</FlexContainer>
);
};
export default NabBar;

View File

@@ -0,0 +1,17 @@
import { Box, Button } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import BurgerIcon from './BurgerIcon';
import { context, navMenuToggleAction } from '../../Context';
const RightNavBtn = () => {
const { state, dispatch } = useContext(context);
return (
<Box marginInlineEnd='15px' onClick={() => state.isMobile && dispatch(navMenuToggleAction(true))}>
{state.isMobile ? <BurgerIcon /> : <Button primary>Send to RocketChat</Button>}
</Box>
);
};
export default RightNavBtn;

View File

@@ -0,0 +1 @@
export { default } from './NavBar';

View File

@@ -0,0 +1,24 @@
import { type ComponentProps, Fragment, useContext, useEffect } from 'react';
import { context } from '../../Context';
import persistStore from '../../utils/persistStore';
const PersistStore = (props: ComponentProps<'div'>) => {
const { state } = useContext(context);
useEffect(() => {
const handleBeforeUnload = () => {
persistStore(state);
};
window.onbeforeunload = handleBeforeUnload;
return () => {
window.onbeforeunload = null;
};
}, [state]);
return <Fragment {...props} />;
};
export default PersistStore;

View File

@@ -0,0 +1 @@
export { default } from './PersistStore';

View File

@@ -0,0 +1,13 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import Surface from './Surface/Surface';
const Display = () => (
<Scrollable vertical>
<Box height='100%' width='100%' borderInlineStart='var(--default-border)'>
<Surface />
</Box>
</Scrollable>
);
export default Display;

View File

@@ -0,0 +1,8 @@
import { Banner, Icon } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type MessageSurfaceProps = { children: ReactNode };
const MessageSurface = ({ children }: MessageSurfaceProps) => <Banner icon={<Icon name='info' size='x20' />}>{children}</Banner>;
export default MessageSurface;

View File

@@ -0,0 +1,62 @@
import {
Avatar,
Box,
Button,
ButtonGroup,
Contextualbar,
ContextualbarAction,
ContextualbarFooter,
ContextualbarHeader,
ContextualbarTitle,
} from '@rocket.chat/fuselage';
import { Scrollbars } from 'rc-scrollbars';
import type { ReactNode } from 'react';
export type ContextualBarSurfaceProps = { children: ReactNode };
const ContextualBarSurface = ({ children }: ContextualBarSurfaceProps) => (
<Contextualbar>
<ContextualbarHeader>
<Avatar url='data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' />
<ContextualbarTitle>Contextual Bar</ContextualbarTitle>
<ContextualbarAction data-qa='ContextualbarActionClose' title='Close' name='cross' />
</ContextualbarHeader>
<Box height='100%' padding='12px'>
<Box height='100%' display='flex' flexShrink={1} flexDirection='column' flexGrow={1}>
<Scrollbars
autoHide
autoHideTimeout={2000}
autoHideDuration={500}
style={{
width: '100%',
height: '100%',
flexGrow: 1,
overflowY: 'hidden',
}}
renderThumbVertical={({ style, ...props }) => (
<div
{...props}
style={{
...style,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: '7px',
}}
/>
)}
>
<div>{children}</div>
</Scrollbars>
</Box>
</Box>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button>Cancel</Button>
<Button primary>Submit</Button>
</ButtonGroup>
</ContextualbarFooter>
</Contextualbar>
);
export default ContextualBarSurface;

View File

@@ -0,0 +1,49 @@
import {
Message,
MessageLeftContainer,
MessageContainer,
MessageHeader,
MessageName,
MessageUsername,
MessageRole,
MessageTimestamp,
MessageBody,
MessageToolbarWrapper,
MessageToolbar,
MessageToolbarItem,
Avatar,
} from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type MessageSurfaceProps = { children: ReactNode };
const MessageSurface = ({ children }: MessageSurfaceProps) => (
<Message>
<MessageLeftContainer>
<Avatar
url='data:image/jpeg;base64,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'
size='x36'
/>
</MessageLeftContainer>
<MessageContainer>
<MessageHeader>
<MessageName>Vivek Srivastava</MessageName>
<MessageUsername>@vivek.srivastava</MessageUsername>
<MessageRole>Admin</MessageRole>
<MessageRole>User</MessageRole>
<MessageRole>Owner</MessageRole>
<MessageTimestamp>12:00 PM</MessageTimestamp>
</MessageHeader>
<MessageBody>{children}</MessageBody>
</MessageContainer>
<MessageToolbarWrapper>
<MessageToolbar>
<MessageToolbarItem icon='quote' />
<MessageToolbarItem icon='clock' />
<MessageToolbarItem icon='thread' />
</MessageToolbar>
</MessageToolbarWrapper>
</Message>
);
export default MessageSurface;

View File

@@ -0,0 +1,40 @@
import { css } from '@rocket.chat/css-in-js';
import {
Modal,
ModalHeader,
ModalThumb,
ModalTitle,
ModalClose,
ModalContent,
ModalFooter,
ButtonGroup,
Button,
} from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type ModalSurfaceProps = { children: ReactNode };
const ModalSurface = ({ children }: ModalSurfaceProps) => (
<Modal>
<ModalHeader>
<ModalThumb url='data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==' />
<ModalTitle>Modal Header</ModalTitle>
<ModalClose />
</ModalHeader>
<ModalContent
className={css`
overflow: visible;
`}
>
{children}
</ModalContent>
<ModalFooter>
<ButtonGroup align='end'>
<Button>Cancel</Button>
<Button primary>Submit</Button>
</ButtonGroup>
</ModalFooter>
</Modal>
);
export default ModalSurface;

View File

@@ -0,0 +1,7 @@
export const reorder = <T>(list: T[], startIndex: number, endIndex: number): T[] => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};

View File

@@ -0,0 +1,95 @@
import type { DropResult } from '@hello-pangea/dnd';
import { Box } from '@rocket.chat/fuselage';
import { UiKitContext } from '@rocket.chat/fuselage-ui-kit';
import { useContext, useState, useEffect } from 'react';
import { reorder } from './Reorder';
import SurfaceRender from './SurfaceRender';
import { SurfaceOptions } from './constant';
import { context, updatePayloadAction, actionPreviewAction } from '../../../../Context';
import generateActionPreview from '../../../../Payload/actionPreview/generateActionPreview';
import type { Block } from '../../../Draggable/DraggableList';
import DraggableList from '../../../Draggable/DraggableList';
const Surface = () => {
const {
state: { screens, activeScreen, user },
dispatch,
} = useContext(context);
const [uniqueBlocks, setUniqueBlocks] = useState<{
block: Block[];
isChangeByDnd: boolean;
}>({
block: screens[activeScreen]?.payload?.blocks?.map((block, i) => ({
id: `${i}`,
payload: block,
})),
isChangeByDnd: false,
});
const preview = generateActionPreview({
type: 'Action Block',
data: {},
surface: screens[activeScreen]?.payload.surface,
blocks: screens[activeScreen]?.payload.blocks,
user,
});
useEffect(() => {
setUniqueBlocks({
block: screens[activeScreen]?.payload?.blocks?.map((block, i) => ({
id: `${i}`,
payload: block,
})),
isChangeByDnd: false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screens[activeScreen]?.payload.blocks]);
useEffect(() => {
if (uniqueBlocks.isChangeByDnd) {
dispatch(
updatePayloadAction({
blocks: uniqueBlocks.block.map((block) => block.payload),
changedByEditor: false,
}),
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uniqueBlocks]);
const onDragEnd = ({ destination, source }: DropResult) => {
if (!destination) return;
const newBlocks = reorder(uniqueBlocks.block, source.index, destination.index);
setUniqueBlocks({ block: newBlocks, isChangeByDnd: true });
};
return (
<Box width='100%' height='100%' padding='20px'>
<UiKitContext.Provider
value={{
action: (a) => {
preview.action = a;
dispatch(actionPreviewAction({ ...preview }));
},
updateState: (s) => {
preview.state = s;
dispatch(actionPreviewAction({ ...preview }));
},
values: {},
appId: 'core',
}}
>
<SurfaceRender type={screens[activeScreen]?.payload.surface}>
<DraggableList
surface={screens[activeScreen]?.payload.surface || SurfaceOptions.Message}
blocks={uniqueBlocks.block || []}
onDragEnd={onDragEnd}
/>
</SurfaceRender>
</UiKitContext.Provider>
</Box>
);
};
export default Surface;

View File

@@ -0,0 +1,19 @@
import type { ReactNode } from 'react';
import BannerSurface from './BannerSurface';
import ContextualBarSurface from './ContextualBarSurface';
import MessageSurface from './MessageSurface';
import ModalSurface from './ModalSurface';
import { SurfaceOptions } from './constant';
export type SurfaceRenderProps = { type?: SurfaceOptions; children: ReactNode };
const SurfaceRender = ({ type = SurfaceOptions.Message, children }: SurfaceRenderProps) => (
<>
{SurfaceOptions.Message === type && <MessageSurface>{children}</MessageSurface>}
{SurfaceOptions.Banner === type && <BannerSurface>{children}</BannerSurface>}
{SurfaceOptions.Modal === type && <ModalSurface>{children}</ModalSurface>}
{SurfaceOptions.ContextualBar === type && <ContextualBarSurface>{children}</ContextualBarSurface>}
</>
);
export default SurfaceRender;

View File

@@ -0,0 +1,6 @@
export enum SurfaceOptions {
Message = 1,
Banner,
Modal,
ContextualBar,
}

View File

@@ -0,0 +1 @@
export { default } from './Surface';

View File

@@ -0,0 +1,24 @@
import './UiKitElementWrapper.scss';
import { Icon } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import { context, updatePayloadAction } from '../../../../Context';
export type DisplayProps = { elementIndex: number };
const Display = ({ elementIndex }: DisplayProps) => {
const { state, dispatch } = useContext(context);
const deleteElement = () => {
const { screens, activeScreen } = state;
const blocks = [...screens[activeScreen].payload.blocks];
blocks.splice(elementIndex, 1);
dispatch(updatePayloadAction({ blocks: [...blocks], changedByEditor: false }));
};
return (
<button className='uikit-element-delete-btn' onClick={deleteElement}>
<Icon name='cross' size='x20' />
</button>
);
};
export default Display;

View File

@@ -0,0 +1,65 @@
@use '../../../../cssVariables.css';
.uikit-element-wrapper{
position: relative;
box-sizing: border-box;
outline: 1px solid transparent;
height: fit-content;
align-items: center;
transition: var(--animation-fast);
&::before {
content: '';
display: block;
position: absolute;
inset: -8px;
border-radius: 4px;
outline: var(--elements-border) !important;
box-shadow: 0px 0px 8px 1px #ddd;
opacity: 0;
transition: var(--animation-fast);
}
&:hover {
> * {
position: relative;
}
&::before {
opacity: 1;
}
> .uikit-element-delete-btn {
visibility: visible !important;
}
}
&:active {
> * {
position: relative;
}
&::before {
background: white;
opacity: 1;
}
}
}
.uikit-element-delete-btn{
all: initial;
appearance: none;
padding: 0;
position: absolute !important;
top: 50%;
right: 0;
width: 20px;
height: 20px;
visibility: hidden;
cursor: pointer;
z-index: 1;
background-color: white;
border-radius: 4px;
border: var(--elements-border);
display: grid;
place-items: center;
transform: translateY(-50%);
}

View File

@@ -0,0 +1,7 @@
import './UiKitElementWrapper.scss';
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const ElementWrapper = (props: ComponentProps<typeof Box>) => <Box className='uikit-element-wrapper' {...props} />;
export default ElementWrapper;

View File

@@ -0,0 +1 @@
export { default } from './UiKitElementWrapper';

View File

@@ -0,0 +1 @@
export { default } from './Display';

View File

@@ -0,0 +1,23 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import BlockEditor from '../../CodeEditor/BlockEditor';
import { actionBlockExtensions } from '../../CodeEditor/Extensions/Extensions';
const ActionBlockEditor = () => (
<Scrollable vertical>
<Box
position='relative'
height='100%'
width='100%'
zIndex={1}
backgroundColor='#f6f9fc'
display='flex'
borderInlineStart='var(--default-border)'
overflow='auto'
>
<BlockEditor extensions={actionBlockExtensions} />
</Box>
</Scrollable>
);
export default ActionBlockEditor;

View File

@@ -0,0 +1,23 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import { actionPreviewExtensions } from '../../CodeEditor/Extensions/Extensions';
import PreviewEditor from '../../CodeEditor/PreviewEditor';
const ActionPreviewEditor = () => (
<Scrollable vertical>
<Box
position='relative'
height='100%'
width='100%'
zIndex={1}
backgroundColor='#f6f9fc'
display='flex'
borderInlineStart='var(--default-border)'
overflow='auto'
>
<PreviewEditor extensions={actionPreviewExtensions} />
</Box>
</Scrollable>
);
export default ActionPreviewEditor;

View File

@@ -0,0 +1,88 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { Fragment, useContext } from 'react';
import ActionBlockEditor from './ActionBlockEditor';
import ActionPreviewEditor from './ActionPreviewEditor';
import { context, editorTabsToggleAction } from '../../../Context';
import FlowDiagram from '../../../Pages/FlowDiagram';
import PrototypeContainer from '../../PtototypeContainer/PrototypeContainer';
import ToggleTabs from '../../ToggleTabs';
enum TabsItem {
ActionBlock,
ActionPreview,
FlowDiagram,
Prototype,
}
const tabsItem = {
[TabsItem.ActionBlock]: {
name: 'Action Block',
Container: ActionBlockEditor,
},
[TabsItem.ActionPreview]: {
name: 'Action Preview',
Container: ActionPreviewEditor,
},
[TabsItem.FlowDiagram]: {
name: 'Flow Diagram',
Container: FlowDiagram,
},
[TabsItem.Prototype]: { name: 'Prototype', Container: PrototypeContainer },
} as const;
const EditorPanel = () => {
const {
state: { editorTabsToggle },
dispatch,
} = useContext(context);
const toggleTabsHandler = (index: number) => {
dispatch(editorTabsToggleAction(index));
};
const tabChangeStyle = (index: number) => {
return css`
transition: 0.5s ease;
left: calc(-100% * ${index});
`;
};
return (
<Box width='100%' height='100%'>
<Box
position='relative'
width='100%'
height='100%'
overflow='hidden'
className={[
css`
user-select: none;
`,
]}
>
<ToggleTabs
tabsItem={Object.values(tabsItem).map((item) => item.name)}
onChange={toggleTabsHandler}
selectedTab={editorTabsToggle}
/>
<Box position='relative' width='100%' height='calc(100% - 40px)' flexDirection='column'>
<Box
position='absolute'
width={`calc(100% * ${Object.values(tabsItem).length})`}
height='100%'
display='flex'
borderBlockStart='var(--default-border)'
className={tabChangeStyle(editorTabsToggle)}
>
{Object.values(tabsItem).map(({ Container: ContainerComponent }, index) => (
<Fragment key={index}>{index === editorTabsToggle ? <ContainerComponent /> : <Box width='100%' height='100%' />}</Fragment>
))}
</Box>
</Box>
</Box>
</Box>
);
};
export default EditorPanel;

View File

@@ -0,0 +1 @@
export { default } from './EditorPanel';

View File

@@ -0,0 +1,47 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Button } from '@rocket.chat/fuselage';
import { useContext } from 'react';
import { context, previewTabsToggleAction, templatesToggleAction } from '../../../Context';
import SurfaceSelect from '../../SurfaceSelect';
import ToggleTabs from '../../ToggleTabs';
const NavPanel = () => {
const {
state: { isMobile, isTablet, previewTabsToggle },
dispatch,
} = useContext(context);
const toggleTabsHandler = (index: number) => {
dispatch(previewTabsToggleAction(index));
};
const tabsItem: string[] = ['Preview', 'Editor'];
return (
<Box
width='100%'
height='40px'
borderBlockEnd='var(--default-border)'
display='flex'
alignItems='center'
zIndex={1}
justifyContent={isMobile ? 'flex-end' : 'space-between'}
backgroundColor='alternative'
className={css`
user-select: none;
`}
>
{!isMobile && (
<Box display='flex' alignItems='center' flexGrow={0} paddingInlineStart={4}>
<SurfaceSelect />
<Button marginInlineStart='10px' small warning height='max-content' onClick={() => dispatch(templatesToggleAction(true))}>
Templates
</Button>
</Box>
)}
{isTablet && <ToggleTabs tabsItem={tabsItem} onChange={toggleTabsHandler} selectedTab={previewTabsToggle} />}
</Box>
);
};
export default NavPanel;

View File

@@ -0,0 +1 @@
export { default } from './NavPanel';

View File

@@ -0,0 +1,38 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useEffect, useContext } from 'react';
import NavPanel from './NavPanel';
import Wrapper from './Wrapper';
import { context, previewTabsToggleAction } from '../../Context';
const Preview = () => {
const {
state: { isMobile, isTablet },
dispatch,
} = useContext(context);
useEffect(() => {
dispatch(previewTabsToggleAction(0));
}, [isTablet, dispatch]);
return (
<Box
display='flex'
flexGrow={1}
backgroundColor='#fff'
zIndex={3}
height='100%'
flexDirection='column'
paddingInlineStart={isMobile ? '' : 'var(--sidebar-width)'}
className={css`
transition: 0.5s ease;
`}
>
<NavPanel />
<Wrapper />
</Box>
);
};
export default Preview;

View File

@@ -0,0 +1,48 @@
import './splitPlane.css';
import { useEffect, useContext } from 'react';
import { Pane, SplitPane } from 'react-split-pane';
import { context, previewTabsToggleAction } from '../../../Context';
import Display from '../Display';
import EditorPanel from '../Editor';
type PreviewSizeType = {
blockSize: number;
inlineSize: number;
};
export type SplitPlaneContainerProps = {
previewSize: Partial<PreviewSizeType>;
};
const SplitPlaneContainer = ({ previewSize }: SplitPlaneContainerProps) => {
const {
state: { isTablet },
dispatch,
} = useContext(context);
useEffect(() => {
dispatch(previewTabsToggleAction(0));
}, [isTablet, dispatch]);
const minSize = 300;
const maxSize = (previewSize.inlineSize || 1) - 350;
return isTablet ? (
<>
<Display />
<EditorPanel />
</>
) : (
<SplitPane resizable={!isTablet}>
<Pane minSize={minSize} maxSize={maxSize}>
<Display />
</Pane>
<Pane minSize={minSize} maxSize={maxSize}>
<EditorPanel />
</Pane>
</SplitPane>
);
};
export default SplitPlaneContainer;

View File

@@ -0,0 +1 @@
export { default } from './SplitPlaneContainer';

View File

@@ -0,0 +1,54 @@
.Resizer {
background: #000;
opacity: 0.2;
z-index: 10;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer:hover {
-webkit-transition: all 2s ease;
transition: all 2s ease;
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
}
.Resizer.vertical:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.pane1Style{
width: 40%
}

View File

@@ -0,0 +1,41 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import { useContext } from 'react';
import { context } from '../../Context';
import SplitPlaneContainer from './SplitPlaneContainer/SplitPlaneContainer';
const Wrapper = () => {
const { ref, contentBoxSize } = useResizeObserver();
const {
state: { isTablet, previewTabsToggle },
} = useContext(context);
const tabChangeStyle = () => {
switch (previewTabsToggle) {
case 0:
return css`
transition: 0.5s ease;
left: 0;
`;
case 1:
return css`
transition: 0.5s ease;
left: -100%;
`;
default:
return undefined;
}
};
return (
<Box position='relative' width='100%' flexGrow={1}>
<Box position='absolute' width={isTablet ? '200%' : '100%'} height='100%' display='flex' ref={ref} className={tabChangeStyle()}>
<SplitPlaneContainer previewSize={contentBoxSize} />
</Box>
</Box>
);
};
export default Wrapper;

View File

@@ -0,0 +1 @@
export { default } from './Preview';

View File

@@ -0,0 +1,41 @@
@use '../../cssVariables.css';
.rc-prototype-renderer {
background: white;
padding: 20px !important;
.rc-prototype_action{
position: relative;
}
.rcx-message{
&:hover{
background: none !important;
}
}
}
.rc-prototype_action-glow {
position: absolute;
top: 0;
left: 0;
animation: glow 1s ease-out both;
width: 100%;
height: 100%;
z-index: 1000;
background-color: var(--RCPG-tertary-color);
}
@keyframes glow {
0% {
opacity: 0;
}
2% {
opacity: 0.3;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,72 @@
import './PrototypeRender.scss';
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import { useRef, useState } from 'react';
import type { Edge } from 'reactflow';
import type { ILayoutBlock, idType } from '../../Context/initialState';
import SurfaceRender from '../Preview/Display/Surface/SurfaceRender';
import type { SurfaceOptions } from '../Preview/Display/Surface/constant';
import RenderPayload from '../RenderPayload/RenderPayload';
const PrototypeRender = ({
blocks,
surface,
flowEdges,
activeActions,
onSelectAction,
}: {
blocks: ILayoutBlock[];
surface: SurfaceOptions;
flowEdges: Edge[];
activeActions: idType[];
onSelectAction: (id: idType) => void;
}) => {
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const [glowActive, setGlowActive] = useState(false);
const actionClickHandler = (id: idType) => {
const edge = flowEdges.find((edge) => edge.sourceHandle === id);
if (edge) return onSelectAction(edge.target);
clearTimeout(timerRef.current);
setGlowActive(false);
setTimeout(() => {
setGlowActive(true);
}, 0);
timerRef.current = setTimeout(() => {
setGlowActive(false);
}, 1000);
};
return (
<Box height='max-content' marginBlock='auto' className='rc-prototype-renderer'>
<SurfaceRender type={surface}>
{blocks.map((action, id) => (
<Box
key={id}
className={css`
cursor: pointer;
`}
onClick={() => actionClickHandler(action.actionId)}
>
<Box
position='relative'
width='100%'
height='100%'
className={css`
pointer-events: none;
user-select: none;
`}
>
{glowActive && activeActions.includes(action.actionId) && <Box className='rc-prototype_action-glow' />}
<RenderPayload blocks={[action]} surface={surface} />
</Box>
</Box>
))}
</SurfaceRender>
</Box>
);
};
export default PrototypeRender;

View File

@@ -0,0 +1 @@
export { default } from './PrototypeRender';

View File

@@ -0,0 +1,38 @@
import { Box, Scrollable } from '@rocket.chat/fuselage';
import { useContext, useMemo, useState } from 'react';
import { context } from '../../Context';
import PrototypeRender from '../PrototypeRender/PrototypeRender';
const PrototypeContainer = () => {
const {
state: { projects, activeProject, screens },
} = useContext(context);
const [currentScreenID, setCurrentScreenID] = useState<string>(projects[activeProject].screens[0]);
const activeActions = useMemo(() => {
return projects[activeProject]?.flowEdges.map((edge) => edge.sourceHandle);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeProject, projects, projects[activeProject].flowEdges]);
if (!projects[activeProject].screens.length) return null;
const { surface, blocks } = screens[currentScreenID]?.payload || screens[projects[activeProject].screens[0]].payload;
return (
<Scrollable vertical>
<Box width='100%' height='100%'>
<PrototypeRender
surface={surface}
blocks={blocks}
activeActions={activeActions as string[]}
flowEdges={projects[activeProject].flowEdges}
onSelectAction={setCurrentScreenID}
/>
</Box>
</Scrollable>
);
};
export default PrototypeContainer;

View File

@@ -0,0 +1 @@
export { default } from './PrototypeContainer';

View File

@@ -0,0 +1,37 @@
import {
UiKitModal as uiKitModal,
UiKitBanner as uiKitBanner,
UiKitMessage as uiKitMessage,
UiKitContextualBar as uiKitContextualBar,
} from '@rocket.chat/fuselage-ui-kit';
import type { ILayoutBlock } from '../../Context/initialState';
import { SurfaceOptions } from '../Preview/Display/Surface/constant';
const RenderPayload = ({
blocks,
surface = SurfaceOptions.Message,
}: {
index?: number;
blocks: ILayoutBlock[];
surface?: SurfaceOptions;
}) => {
switch (surface) {
case SurfaceOptions.Message:
return uiKitMessage(blocks);
case SurfaceOptions.Banner:
return uiKitBanner(blocks);
case SurfaceOptions.Modal:
return uiKitModal(blocks);
case SurfaceOptions.ContextualBar:
return uiKitContextualBar(blocks);
default:
return null;
}
};
export default RenderPayload;

View File

@@ -0,0 +1 @@
export { default } from './RenderPayload';

View File

@@ -0,0 +1,15 @@
import { Navigate, useOutlet } from 'react-router-dom';
import routes from '../../Routes/Routes';
import { useAuth } from '../../hooks/useAuth';
export const HomeLayout = () => {
const { user } = useAuth();
const outlet = useOutlet();
if (user) {
return <Navigate to={`${routes.home}`} replace />;
}
return <>{outlet}</>;
};

View File

@@ -0,0 +1,23 @@
import { useContext, useEffect } from 'react';
import { useNavigate, useOutlet, useParams } from 'react-router-dom';
import { activeProjectAction, context } from '../../Context';
import routes from '../../Routes/Routes';
export const ProjectSpecificLayout = () => {
const {
state: { projects },
dispatch,
} = useContext(context);
const { projectId } = useParams();
const navigate = useNavigate();
const outlet = useOutlet();
useEffect(() => {
if (!projectId || !projects[projectId]) navigate(routes.home);
else dispatch(activeProjectAction(projectId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <>{outlet}</>;
};

View File

@@ -0,0 +1,15 @@
import { Navigate, useOutlet } from 'react-router-dom';
import routes from '../../Routes/Routes';
import { useAuth } from '../../hooks/useAuth';
export const ProtectedLayout = () => {
const { user } = useAuth();
const outlet = useOutlet();
if (!user) {
return <Navigate to={`${routes.login}`} />;
}
return <>{outlet}</>;
};

View File

@@ -0,0 +1,31 @@
import { css } from '@rocket.chat/css-in-js';
import { Icon, Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const CreateNewScreenButton = ({
size = '60px',
name = 'plus',
...props
}: {
size?: ComponentProps<typeof Icon>['size'];
} & ComponentProps<typeof Icon>) => {
return (
<Box width={size} height={size}>
<Icon
{...props}
size={size}
name={name}
className={css`
cursor: pointer;
transition: var(--animation-default);
&:hover {
scale: 1.1;
transition: var(--animation-default);
}
`}
/>
</Box>
);
};
export default CreateNewScreenButton;

View File

@@ -0,0 +1,83 @@
import { Box, Icon, Button, Divider, Option } from '@rocket.chat/fuselage';
import { useOutsideClick } from '@rocket.chat/fuselage-hooks';
import { type ComponentProps, useRef, useState, type ChangeEvent } from 'react';
import { formatDate } from '../../../utils/formatDate';
import EditableLabel from '../EditableLabel/EditableLabel';
const EditMenu = ({
name,
date,
onChange,
onBlur,
onDuplicate,
onDelete,
labelProps,
}: {
name: string;
date: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onDuplicate?: () => void;
onDelete?: () => void;
onBlur: () => void;
labelProps?: ComponentProps<typeof EditableLabel>;
}) => {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useOutsideClick([containerRef], () => {
setIsOpen(false);
onBlur();
});
const duplicatehandler = (e: { stopPropagation: () => void }) => {
e.stopPropagation();
setIsOpen(false);
onDuplicate?.();
};
const deleteHandler = (e: { stopPropagation: () => void }) => {
e.stopPropagation();
setIsOpen(false);
onDelete?.();
};
return (
<Box
position='absolute'
insetBlockStart='10px'
insetInlineEnd='10px'
zIndex={100}
ref={containerRef}
className='rc-edit-menu'
onClick={(e) => {
e.stopPropagation();
setIsOpen(true);
}}
>
<Button ref={buttonRef} square mini>
<Icon name='cog' size='x16' />
</Button>
{isOpen && (
<Box position='absolute'>
<Box elevation='1' paddingBlock='10px' backgroundColor='white'>
<Box marginInline='10px'>
<EditableLabel value={name} onChange={onChange} onBlur={onBlur} {...labelProps} />
<Box withTruncatedText marginBlockStart='2px' fontScale='p2'>
{formatDate(date)}
</Box>
</Box>
<Divider marginBlockStart='12px' />
<Option disabled={!onDuplicate} onClick={duplicatehandler}>
Duplicate
</Option>
<Option disabled={!onDelete} onClick={deleteHandler}>
Delete
</Option>
</Box>
</Box>
)}
</Box>
);
};
export default EditMenu;

View File

@@ -0,0 +1 @@
export { default } from './EditMenu';

Some files were not shown because too many files have changed in this diff Show More