@jtl-software/cloud-app-template-frontend-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/Dockerfile +39 -0
- package/_package.json +35 -0
- package/eslint.config.js +25 -0
- package/index.html +13 -0
- package/manifest.json +48 -0
- package/nginx.conf +52 -0
- package/package.json +6 -0
- package/public/vite.svg +1 -0
- package/src/App.css +0 -0
- package/src/App.test.tsx +7 -0
- package/src/App.tsx +38 -0
- package/src/assets/react.svg +1 -0
- package/src/common/constants.ts +1 -0
- package/src/index.css +3 -0
- package/src/main.tsx +21 -0
- package/src/pages/erp-page/ErpPage.tsx +93 -0
- package/src/pages/erp-page/IErpPageProps.ts +5 -0
- package/src/pages/erp-page/index.ts +1 -0
- package/src/pages/hub-page/HubPage.tsx +71 -0
- package/src/pages/hub-page/IHubPageProps.ts +1 -0
- package/src/pages/hub-page/index.ts +1 -0
- package/src/pages/index.ts +5 -0
- package/src/pages/pane-page/IPanePageProps.ts +5 -0
- package/src/pages/pane-page/PanePage.tsx +86 -0
- package/src/pages/pane-page/index.ts +2 -0
- package/src/pages/setup-page/ISetupPageProps.ts +5 -0
- package/src/pages/setup-page/SetupPage.tsx +114 -0
- package/src/pages/setup-page/index.ts +1 -0
- package/src/pages/welcome-page/WelcomePage.tsx +129 -0
- package/src/pages/welcome-page/index.ts +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +4 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +20 -0
package/CHANGELOG.md
ADDED
package/Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ---------- Stage 1 & 2: Build js-core and frontend ----------
|
|
2
|
+
FROM node:23-alpine AS build
|
|
3
|
+
|
|
4
|
+
ARG VITE_API_URL
|
|
5
|
+
|
|
6
|
+
WORKDIR /work
|
|
7
|
+
|
|
8
|
+
# 1️⃣ Copy both project dirs for caching
|
|
9
|
+
COPY src/sdk/js-core/package.json src/sdk/js-core/yarn.lock ./src/sdk/js-core/
|
|
10
|
+
COPY examples/hello-world-app/packages/frontend/package.json ./examples/hello-world-app/packages/frontend/
|
|
11
|
+
|
|
12
|
+
# Yarn workspaces monorepo: single lockfile at repo root.
|
|
13
|
+
# Copy root package.json + yarn.lock so installs in /packages/frontend use the root lock.
|
|
14
|
+
COPY examples/hello-world-app/yarn.lock ./examples/hello-world-app/
|
|
15
|
+
COPY examples/hello-world-app/package.json ./examples/hello-world-app/
|
|
16
|
+
|
|
17
|
+
# 2️⃣ Install js-core deps (cacheable)
|
|
18
|
+
WORKDIR /work/src/sdk/js-core
|
|
19
|
+
RUN yarn install --frozen-lockfile --ignore-scripts
|
|
20
|
+
|
|
21
|
+
# 3️⃣ Copy full source
|
|
22
|
+
WORKDIR /work
|
|
23
|
+
COPY src/sdk/js-core ./src/sdk/js-core
|
|
24
|
+
COPY examples/hello-world-app/packages/frontend ./examples/hello-world-app/packages/frontend
|
|
25
|
+
|
|
26
|
+
# 4️⃣ Build js-core
|
|
27
|
+
WORKDIR /work/src/sdk/js-core
|
|
28
|
+
RUN yarn build
|
|
29
|
+
|
|
30
|
+
# 5️⃣ Build frontend with local dist dependency
|
|
31
|
+
WORKDIR /work/examples/hello-world-app/packages/frontend
|
|
32
|
+
RUN yarn install --frozen-lockfile && yarn build
|
|
33
|
+
|
|
34
|
+
# ---------- Stage 3: Runtime ----------
|
|
35
|
+
FROM nginx:alpine AS final
|
|
36
|
+
COPY --from=build /work/examples/hello-world-app/packages/frontend/dist /usr/share/nginx/html
|
|
37
|
+
COPY examples/hello-world-app/packages/frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
|
38
|
+
EXPOSE 80
|
|
39
|
+
CMD ["nginx", "-g", "daemon off;"]
|
package/_package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{APP_NAME}}-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@jtl-software/cloud-apps-core": "latest",
|
|
15
|
+
"@jtl-software/platform-ui-react": "latest",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.21.0",
|
|
21
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
22
|
+
"@types/react": "^19.0.10",
|
|
23
|
+
"@types/react-dom": "^19.0.4",
|
|
24
|
+
"@vitejs/plugin-react-swc": "^3.8.0",
|
|
25
|
+
"eslint": "^9.21.0",
|
|
26
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
27
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
|
28
|
+
"globals": "^15.15.0",
|
|
29
|
+
"tailwindcss": "^4.2.2",
|
|
30
|
+
"typescript": "~5.7.2",
|
|
31
|
+
"typescript-eslint": "^8.24.1",
|
|
32
|
+
"vite": "^6.2.0",
|
|
33
|
+
"vitest": "^2.1.8"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{ ignores: ['dist'] },
|
|
9
|
+
{
|
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2020,
|
|
14
|
+
globals: globals.browser,
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
'react-hooks': reactHooks,
|
|
18
|
+
'react-refresh': reactRefresh,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
...reactHooks.configs.recommended.rules,
|
|
22
|
+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{APP_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/manifest.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifestVersion": "1.0.0",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"name": {
|
|
5
|
+
"short": "{{APP_NAME}}",
|
|
6
|
+
"full": "{{APP_NAME}}"
|
|
7
|
+
},
|
|
8
|
+
"description": {
|
|
9
|
+
"short": "{{APP_DESCRIPTION}}",
|
|
10
|
+
"full": "{{APP_DESCRIPTION}}"
|
|
11
|
+
},
|
|
12
|
+
"defaultLocale": "de-DE",
|
|
13
|
+
"locales": {
|
|
14
|
+
"de-DE": {},
|
|
15
|
+
"en": {}
|
|
16
|
+
},
|
|
17
|
+
"icon": {
|
|
18
|
+
"light": "https://hub.jtl-cloud.com/assets/image-placeholder.png",
|
|
19
|
+
"dark": "https://hub.jtl-cloud.com/assets/image-placeholder.png"
|
|
20
|
+
},
|
|
21
|
+
"lifecycle": {
|
|
22
|
+
"setupUrl": "http://localhost:3004/setup"
|
|
23
|
+
},
|
|
24
|
+
"capabilities": {
|
|
25
|
+
"hub": {
|
|
26
|
+
"appLauncher": {
|
|
27
|
+
"redirectUrl": "http://localhost:3004/hub"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"erp": {
|
|
31
|
+
"menuItems": [
|
|
32
|
+
{
|
|
33
|
+
"id": "{{APP_NAME}}-menu",
|
|
34
|
+
"name": "{{APP_NAME}}",
|
|
35
|
+
"url": "http://localhost:3004/erp"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"pane": [
|
|
39
|
+
{
|
|
40
|
+
"url": "http://localhost:3004/pane",
|
|
41
|
+
"title": "{{APP_NAME}}",
|
|
42
|
+
"context": "customers",
|
|
43
|
+
"matchChildContext": true
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/nginx.conf
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 80;
|
|
3
|
+
server_name _;
|
|
4
|
+
|
|
5
|
+
root /usr/share/nginx/html;
|
|
6
|
+
index index.html;
|
|
7
|
+
|
|
8
|
+
# Gzip compression
|
|
9
|
+
gzip on;
|
|
10
|
+
gzip_static on; # serve .gz files if available
|
|
11
|
+
gzip_types
|
|
12
|
+
text/plain
|
|
13
|
+
text/css
|
|
14
|
+
application/json
|
|
15
|
+
application/javascript
|
|
16
|
+
text/javascript
|
|
17
|
+
application/x-javascript
|
|
18
|
+
application/xml
|
|
19
|
+
font/woff2
|
|
20
|
+
image/svg+xml;
|
|
21
|
+
gzip_proxied any;
|
|
22
|
+
gzip_vary on;
|
|
23
|
+
gzip_min_length 256;
|
|
24
|
+
|
|
25
|
+
# Cache aggressively for hashed assets (JS, CSS, fonts, images, etc.)
|
|
26
|
+
location ~* \.(?:js|css|woff2?|eot|ttf|otf|svg|ico|jpg|jpeg|png|gif|webp)$ {
|
|
27
|
+
expires 1y;
|
|
28
|
+
access_log off;
|
|
29
|
+
add_header Cache-Control "public, immutable";
|
|
30
|
+
try_files $uri =404;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Cache /assets/ folder (if you store fonts/images here)
|
|
34
|
+
location ^~ /assets/ {
|
|
35
|
+
expires 1y;
|
|
36
|
+
access_log off;
|
|
37
|
+
add_header Cache-Control "public, immutable";
|
|
38
|
+
try_files $uri =404;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
location / {
|
|
43
|
+
try_files $uri /index.html;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
location /static/ {
|
|
47
|
+
expires 1y;
|
|
48
|
+
add_header Cache-Control "public, immutable";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
error_page 404 /index.html;
|
|
52
|
+
}
|
package/package.json
ADDED
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
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>
|
package/src/App.css
ADDED
|
File without changes
|
package/src/App.test.tsx
ADDED
package/src/App.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AppBridge } from '@jtl-software/cloud-apps-core';
|
|
2
|
+
import './App.css';
|
|
3
|
+
import { ErpPage, HubPage, PanePage, SetupPage, WelcomePage } from './pages';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
type AppMode = 'setup' | 'erp' | 'pane' | 'hub';
|
|
7
|
+
|
|
8
|
+
const App: React.FC<{ appBridge: AppBridge | null }> = ({ appBridge }) => {
|
|
9
|
+
const mode: AppMode = location.pathname.substring(1) as AppMode;
|
|
10
|
+
|
|
11
|
+
useEffect((): void => {
|
|
12
|
+
if (appBridge) {
|
|
13
|
+
console.log('[HelloWorldApp] bridge created!');
|
|
14
|
+
}
|
|
15
|
+
}, [appBridge]);
|
|
16
|
+
|
|
17
|
+
// Hub page runs standalone (no iframe, no AppBridge)
|
|
18
|
+
if (mode === 'hub') {
|
|
19
|
+
return <HubPage />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!appBridge) {
|
|
23
|
+
return <WelcomePage />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
switch (mode) {
|
|
27
|
+
case 'setup':
|
|
28
|
+
return <SetupPage appBridge={appBridge} />;
|
|
29
|
+
case 'erp':
|
|
30
|
+
return <ErpPage appBridge={appBridge} />;
|
|
31
|
+
case 'pane':
|
|
32
|
+
return <PanePage appBridge={appBridge} />;
|
|
33
|
+
default:
|
|
34
|
+
return <WelcomePage connected />;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default App;
|
|
@@ -0,0 +1 @@
|
|
|
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="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3005';
|
package/src/index.css
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import './index.css';
|
|
4
|
+
import App from './App.tsx';
|
|
5
|
+
import { createAppBridge, AppBridge } from '@jtl-software/cloud-apps-core';
|
|
6
|
+
|
|
7
|
+
const root = createRoot(document.getElementById('root')!);
|
|
8
|
+
|
|
9
|
+
const renderApp = (appBridge: AppBridge | null) => {
|
|
10
|
+
root.render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<App appBridge={appBridge} />
|
|
13
|
+
</StrictMode>,
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
renderApp(null);
|
|
18
|
+
|
|
19
|
+
createAppBridge().then(appBridge => {
|
|
20
|
+
renderApp(appBridge);
|
|
21
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import IErpPageProps from './IErpPageProps';
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardContent,
|
|
9
|
+
Text,
|
|
10
|
+
Badge,
|
|
11
|
+
Stack,
|
|
12
|
+
Separator,
|
|
13
|
+
Box,
|
|
14
|
+
Button,
|
|
15
|
+
} from '@jtl-software/platform-ui-react';
|
|
16
|
+
import { Globe, Link as LinkIcon } from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
const manifestMappings = [{ icon: LinkIcon, field: 'capabilities.erp.menuItems[].url', description: 'Sidebar menu entry' }];
|
|
19
|
+
|
|
20
|
+
const ErpPage: React.FC<IErpPageProps> = ({ appBridge }) => {
|
|
21
|
+
const [isRequesting, setIsRequesting] = useState(false);
|
|
22
|
+
const [time, setTime] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleRequestTimestamp = useCallback(async (): Promise<void> => {
|
|
25
|
+
try {
|
|
26
|
+
setIsRequesting(true);
|
|
27
|
+
appBridge.method.expose('getCurrentTime', () => new Date());
|
|
28
|
+
const result = await appBridge.method.call<Date>('getCurrentTime');
|
|
29
|
+
setTime(result.toUTCString());
|
|
30
|
+
} finally {
|
|
31
|
+
setIsRequesting(false);
|
|
32
|
+
}
|
|
33
|
+
}, [appBridge]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Box className="flex justify-center p-12">
|
|
37
|
+
<Card className="max-w-[520px] w-full">
|
|
38
|
+
<CardHeader className="items-center">
|
|
39
|
+
<Globe size={40} color="#1a56db" strokeWidth={1.5} />
|
|
40
|
+
<CardTitle>ERP Integration</CardTitle>
|
|
41
|
+
<CardDescription className="text-center">
|
|
42
|
+
This is the main app view. It loads when the user clicks your app's menu entry in the ERP sidebar.
|
|
43
|
+
</CardDescription>
|
|
44
|
+
</CardHeader>
|
|
45
|
+
<CardContent>
|
|
46
|
+
<Stack spacing="5" direction="column">
|
|
47
|
+
<Stack spacing="3" direction="column">
|
|
48
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
49
|
+
MANIFEST MAPPING
|
|
50
|
+
</Text>
|
|
51
|
+
{manifestMappings.map(mapping => (
|
|
52
|
+
<Stack key={mapping.field} spacing="3" direction="row" itemAlign="start">
|
|
53
|
+
<mapping.icon size={16} color="#1a56db" className="shrink-0 mt-0.5" />
|
|
54
|
+
<Stack spacing="0" direction="column">
|
|
55
|
+
<Text type="inline-code">{mapping.field}</Text>
|
|
56
|
+
<Text type="xs" color="muted">
|
|
57
|
+
→ {mapping.description}
|
|
58
|
+
</Text>
|
|
59
|
+
</Stack>
|
|
60
|
+
</Stack>
|
|
61
|
+
))}
|
|
62
|
+
</Stack>
|
|
63
|
+
|
|
64
|
+
<Separator />
|
|
65
|
+
|
|
66
|
+
<Stack spacing="3" direction="column">
|
|
67
|
+
<Stack spacing="2" direction="row" itemAlign="center">
|
|
68
|
+
<Text type="small" weight="semibold">
|
|
69
|
+
DEMO: AppBridge Methods
|
|
70
|
+
</Text>
|
|
71
|
+
<Badge variant="default" label="Ready" />
|
|
72
|
+
</Stack>
|
|
73
|
+
<Text type="xs" color="muted">
|
|
74
|
+
The AppBridge lets you expose and call methods between your app and the platform.
|
|
75
|
+
</Text>
|
|
76
|
+
<Button onClick={handleRequestTimestamp} label={isRequesting ? 'Requesting...' : 'Request Current Time'} />
|
|
77
|
+
<Box className="w-full p-4 border border-dashed border-[var(--base-border)] rounded-lg flex items-center justify-center">
|
|
78
|
+
<Text type="small" color="muted">
|
|
79
|
+
{time ?? 'Click the button to test'}
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
<Text type="xs" color="muted" align="center">
|
|
83
|
+
<Text type="inline-code">{"appBridge.method.call('getCurrentTime')"}</Text>
|
|
84
|
+
</Text>
|
|
85
|
+
</Stack>
|
|
86
|
+
</Stack>
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default ErpPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ErpPage } from './ErpPage';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Text, Stack, Separator, Box, Link } from '@jtl-software/platform-ui-react';
|
|
2
|
+
import { Rocket, ExternalLink, MonitorSmartphone } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
const manifestMapping = [{ field: 'capabilities.hub.appLauncher.redirectUrl', description: 'Opens this page in a new browser tab' }];
|
|
5
|
+
|
|
6
|
+
const HubPage: React.FC = () => {
|
|
7
|
+
return (
|
|
8
|
+
<Box className="flex justify-center p-12">
|
|
9
|
+
<Card className="max-w-[520px] w-full">
|
|
10
|
+
<CardHeader className="items-center">
|
|
11
|
+
<Rocket size={40} color="#1a56db" strokeWidth={1.5} />
|
|
12
|
+
<CardTitle>Launched from Hub</CardTitle>
|
|
13
|
+
<CardDescription className="text-center">
|
|
14
|
+
This page opens when a user clicks your app card in the JTL-Cloud Hub. It runs in a full browser tab, not inside the ERP iframe.
|
|
15
|
+
</CardDescription>
|
|
16
|
+
</CardHeader>
|
|
17
|
+
<CardContent>
|
|
18
|
+
<Stack spacing="5" direction="column">
|
|
19
|
+
<Stack spacing="3" direction="column">
|
|
20
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
21
|
+
MANIFEST MAPPING
|
|
22
|
+
</Text>
|
|
23
|
+
{manifestMapping.map(mapping => (
|
|
24
|
+
<Stack key={mapping.field} spacing="3" direction="row" itemAlign="start">
|
|
25
|
+
<ExternalLink size={16} color="#1a56db" className="shrink-0 mt-0.5" />
|
|
26
|
+
<Stack spacing="0" direction="column">
|
|
27
|
+
<Text type="inline-code">{mapping.field}</Text>
|
|
28
|
+
<Text type="xs" color="muted">
|
|
29
|
+
→ {mapping.description}
|
|
30
|
+
</Text>
|
|
31
|
+
</Stack>
|
|
32
|
+
</Stack>
|
|
33
|
+
))}
|
|
34
|
+
</Stack>
|
|
35
|
+
|
|
36
|
+
<Separator />
|
|
37
|
+
|
|
38
|
+
<Stack spacing="3" direction="column">
|
|
39
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
40
|
+
HOW THIS DIFFERS
|
|
41
|
+
</Text>
|
|
42
|
+
<Stack spacing="3" direction="row" itemAlign="start">
|
|
43
|
+
<MonitorSmartphone size={16} color="#1a56db" className="shrink-0 mt-0.5" />
|
|
44
|
+
<Text type="small" color="muted">
|
|
45
|
+
Unlike <Text type="inline-code">/erp</Text> and <Text type="inline-code">/pane</Text>, this page runs outside the ERP — there is no
|
|
46
|
+
AppBridge available. Use this for standalone features like dashboards, settings, or onboarding flows.
|
|
47
|
+
</Text>
|
|
48
|
+
</Stack>
|
|
49
|
+
</Stack>
|
|
50
|
+
|
|
51
|
+
<Separator />
|
|
52
|
+
|
|
53
|
+
<Stack spacing="3" direction="column">
|
|
54
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
55
|
+
NEXT STEPS
|
|
56
|
+
</Text>
|
|
57
|
+
<Text type="small" color="muted">
|
|
58
|
+
Replace this page with your app's main standalone experience — a dashboard, configuration panel, or landing page.
|
|
59
|
+
</Text>
|
|
60
|
+
<Link url="https://hub.jtl-cloud.com/" target="_blank">
|
|
61
|
+
Open JTL-Cloud Hub
|
|
62
|
+
</Link>
|
|
63
|
+
</Stack>
|
|
64
|
+
</Stack>
|
|
65
|
+
</CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
</Box>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default HubPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default interface IHubPageProps {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as HubPage } from './HubPage';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Button, Input, Stack, Text, Separator, Box, Badge } from '@jtl-software/platform-ui-react';
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import IPanePageProps from './IPanePageProps';
|
|
4
|
+
import { PanelRight } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
const manifestFields = [
|
|
7
|
+
{ label: 'url', value: 'capabilities.erp.pane[].url' },
|
|
8
|
+
{ label: 'context', value: 'customers' },
|
|
9
|
+
{ label: 'matchChildContext', value: 'true' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const PanePage: React.FC<IPanePageProps> = ({ appBridge }) => {
|
|
13
|
+
const [customer, setCustomer] = useState<string | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const unsubscribe = appBridge.event.subscribe('CustomerChanged', (data: unknown) => {
|
|
17
|
+
return new Promise<void>(resolve => {
|
|
18
|
+
setCustomer((data as { customerId: string }).customerId);
|
|
19
|
+
resolve();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
unsubscribe();
|
|
25
|
+
};
|
|
26
|
+
}, [appBridge]);
|
|
27
|
+
|
|
28
|
+
const handleGetCurrentCustomer = useCallback(() => {
|
|
29
|
+
appBridge.method.call('getCurrentCustomerId').then(customerId => {
|
|
30
|
+
setCustomer(customerId as string);
|
|
31
|
+
});
|
|
32
|
+
}, [appBridge]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Box className="p-4">
|
|
36
|
+
<Stack spacing="4" direction="column">
|
|
37
|
+
<Stack spacing="2" direction="column" itemAlign="center">
|
|
38
|
+
<PanelRight size={32} color="#1a56db" strokeWidth={1.5} />
|
|
39
|
+
<Text type="h4" align="center">
|
|
40
|
+
Customer Pane
|
|
41
|
+
</Text>
|
|
42
|
+
<Text type="xs" color="muted" align="center">
|
|
43
|
+
This sidebar panel loads when the user views customer data. It receives context events from the ERP.
|
|
44
|
+
</Text>
|
|
45
|
+
</Stack>
|
|
46
|
+
|
|
47
|
+
<Separator />
|
|
48
|
+
|
|
49
|
+
<Stack spacing="2" direction="column">
|
|
50
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
51
|
+
MANIFEST MAPPING
|
|
52
|
+
</Text>
|
|
53
|
+
{manifestFields.map(field => (
|
|
54
|
+
<Stack key={field.label} spacing="2" direction="row" itemAlign="center">
|
|
55
|
+
<Badge variant="outline" label={field.label} />
|
|
56
|
+
<Text type="xs" color="muted">
|
|
57
|
+
{field.value}
|
|
58
|
+
</Text>
|
|
59
|
+
</Stack>
|
|
60
|
+
))}
|
|
61
|
+
</Stack>
|
|
62
|
+
|
|
63
|
+
<Separator />
|
|
64
|
+
|
|
65
|
+
<Stack spacing="3" direction="column">
|
|
66
|
+
<Stack spacing="2" direction="row" itemAlign="center">
|
|
67
|
+
<Text type="small" weight="semibold">
|
|
68
|
+
DEMO: Events
|
|
69
|
+
</Text>
|
|
70
|
+
<Badge variant="default" label="Listening" />
|
|
71
|
+
</Stack>
|
|
72
|
+
<Text type="xs" color="muted">
|
|
73
|
+
The AppBridge sends a CustomerChanged event when the user selects a different customer.
|
|
74
|
+
</Text>
|
|
75
|
+
<Input disabled value={customer} placeholder="No customer selected" />
|
|
76
|
+
<Button variant="outline" onClick={handleGetCurrentCustomer} label="Get Current Customer" />
|
|
77
|
+
<Text type="xs" color="muted" align="center">
|
|
78
|
+
<Text type="inline-code">{"appBridge.event.subscribe('CustomerChanged', ...)"}</Text>
|
|
79
|
+
</Text>
|
|
80
|
+
</Stack>
|
|
81
|
+
</Stack>
|
|
82
|
+
</Box>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default PanePage;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import ISetupPageProps from './ISetupPageProps';
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardContent,
|
|
9
|
+
Text,
|
|
10
|
+
Badge,
|
|
11
|
+
Stack,
|
|
12
|
+
Separator,
|
|
13
|
+
Box,
|
|
14
|
+
Button,
|
|
15
|
+
} from '@jtl-software/platform-ui-react';
|
|
16
|
+
import { Settings } from 'lucide-react';
|
|
17
|
+
import { apiUrl } from '../../common/constants';
|
|
18
|
+
|
|
19
|
+
const howItWorks = [
|
|
20
|
+
{ step: '1', text: 'The platform loads this page via ', code: 'lifecycle.setupUrl', suffix: ' in your manifest' },
|
|
21
|
+
{ step: '2', text: 'Your app requests a session token via ', code: "appBridge.method.call('getSessionToken')", suffix: '' },
|
|
22
|
+
{ step: '3', text: 'Call ', code: "appBridge.method.call('setupCompleted')", suffix: ' to finish installation' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const SetupPage: React.FC<ISetupPageProps> = ({ appBridge }) => {
|
|
26
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
27
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
28
|
+
const [error, setError] = useState<string | null>(null);
|
|
29
|
+
|
|
30
|
+
const handleSetupCompleted = useCallback(async (): Promise<void> => {
|
|
31
|
+
try {
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
const sessionToken = await appBridge.method.call('getSessionToken');
|
|
35
|
+
const response = await fetch(`${apiUrl}/connect-tenant`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ sessionToken }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const responseBody = await response.text();
|
|
42
|
+
console.log('Response from backend:', response.status, responseBody);
|
|
43
|
+
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
await appBridge.method.call('setupCompleted');
|
|
46
|
+
setIsComplete(true);
|
|
47
|
+
} else {
|
|
48
|
+
setError(`Backend returned ${response.status}`);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('An error occurred during setup:', err);
|
|
52
|
+
setError(String(err));
|
|
53
|
+
} finally {
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
}
|
|
56
|
+
}, [appBridge]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box className="flex justify-center p-12">
|
|
60
|
+
<Card className="max-w-[520px] w-full">
|
|
61
|
+
<CardHeader className="items-center">
|
|
62
|
+
<Settings size={40} color="#1a56db" strokeWidth={1.5} />
|
|
63
|
+
<CardTitle>App Setup</CardTitle>
|
|
64
|
+
<CardDescription className="text-center">
|
|
65
|
+
This page is shown during app installation. Complete the setup to connect your app to the platform.
|
|
66
|
+
</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent>
|
|
69
|
+
<Stack spacing="5" direction="column">
|
|
70
|
+
<Stack spacing="3" direction="column">
|
|
71
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
72
|
+
HOW IT WORKS
|
|
73
|
+
</Text>
|
|
74
|
+
{howItWorks.map(item => (
|
|
75
|
+
<Stack key={item.step} spacing="3" direction="row" itemAlign="start">
|
|
76
|
+
<Badge variant="outline" label={item.step} />
|
|
77
|
+
<Text type="small" color="muted">
|
|
78
|
+
{item.text}
|
|
79
|
+
<Text type="inline-code">{item.code}</Text>
|
|
80
|
+
{item.suffix}
|
|
81
|
+
</Text>
|
|
82
|
+
</Stack>
|
|
83
|
+
))}
|
|
84
|
+
</Stack>
|
|
85
|
+
|
|
86
|
+
<Separator />
|
|
87
|
+
|
|
88
|
+
<Stack spacing="3" direction="column">
|
|
89
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
90
|
+
TRY IT
|
|
91
|
+
</Text>
|
|
92
|
+
<Button onClick={handleSetupCompleted} label={isLoading ? 'Setting up...' : 'Complete Setup'} />
|
|
93
|
+
{isComplete && (
|
|
94
|
+
<Text type="small" color="success">
|
|
95
|
+
Setup completed successfully.
|
|
96
|
+
</Text>
|
|
97
|
+
)}
|
|
98
|
+
{error && (
|
|
99
|
+
<Text type="small" color="danger">
|
|
100
|
+
{error}
|
|
101
|
+
</Text>
|
|
102
|
+
)}
|
|
103
|
+
<Text type="xs" color="muted" align="center">
|
|
104
|
+
This will call your backend and signal setup completion to the platform.
|
|
105
|
+
</Text>
|
|
106
|
+
</Stack>
|
|
107
|
+
</Stack>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</Box>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default SetupPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as SetupPage } from './SetupPage';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Text, Badge, Stack, Separator, Box, Link } from '@jtl-software/platform-ui-react';
|
|
2
|
+
import { CircleCheck, CircleAlert, Globe, UserPlus, KeyRound, CloudDownload, Settings, PanelRight } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
const routes = [
|
|
5
|
+
{ path: '/setup', icon: Settings, description: 'Initial app setup and tenant connection' },
|
|
6
|
+
{ path: '/erp', icon: Globe, description: 'ERP integration and data display' },
|
|
7
|
+
{ path: '/pane', icon: PanelRight, description: 'Context-aware customer sidebar panel' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const gettingStartedSteps = [
|
|
11
|
+
{
|
|
12
|
+
icon: UserPlus,
|
|
13
|
+
title: 'Register your app',
|
|
14
|
+
description: 'Create your app in the Partner Portal and get your Client ID and Secret.',
|
|
15
|
+
linkUrl: 'https://partner.jtl-cloud.com/',
|
|
16
|
+
linkLabel: 'Open Partner Portal',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
icon: KeyRound,
|
|
20
|
+
title: 'Configure environment',
|
|
21
|
+
description: 'Add your credentials to packages/backend/.env',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
icon: CloudDownload,
|
|
25
|
+
title: 'Install in JTL-Cloud',
|
|
26
|
+
description: "Open the Hub, find your app under 'Apps in development', and install it.",
|
|
27
|
+
linkUrl: 'https://hub.jtl-cloud.com/',
|
|
28
|
+
linkLabel: 'Open JTL-Cloud Hub',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const RoutesList: React.FC = () => (
|
|
33
|
+
<Stack spacing="3" direction="column">
|
|
34
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
35
|
+
APP ROUTES
|
|
36
|
+
</Text>
|
|
37
|
+
{routes.map(route => (
|
|
38
|
+
<Stack key={route.path} spacing="3" direction="row" itemAlign="center">
|
|
39
|
+
<Badge variant="outline" label={route.path} />
|
|
40
|
+
<Text type="small" color="muted">
|
|
41
|
+
{route.description}
|
|
42
|
+
</Text>
|
|
43
|
+
</Stack>
|
|
44
|
+
))}
|
|
45
|
+
</Stack>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const StandaloneWelcome: React.FC = () => (
|
|
49
|
+
<Card className="max-w-[520px] w-full">
|
|
50
|
+
<CardHeader className="items-center">
|
|
51
|
+
<CircleAlert size={40} color="#f59e0b" strokeWidth={1.5} />
|
|
52
|
+
<CardTitle>App is not connected</CardTitle>
|
|
53
|
+
<CardDescription className="text-center">No AppBridge detected. The app is running locally outside the JTL Platform.</CardDescription>
|
|
54
|
+
</CardHeader>
|
|
55
|
+
<CardContent>
|
|
56
|
+
<Stack spacing="5" direction="column">
|
|
57
|
+
<Stack spacing="4" direction="column">
|
|
58
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
59
|
+
GET STARTED
|
|
60
|
+
</Text>
|
|
61
|
+
{gettingStartedSteps.map((step, i) => (
|
|
62
|
+
<Stack key={step.title} spacing="3" direction="row" itemAlign="start">
|
|
63
|
+
<Badge variant="outline" label={String(i + 1)} />
|
|
64
|
+
<Stack spacing="1" direction="column">
|
|
65
|
+
<Text type="small" weight="medium">
|
|
66
|
+
{step.title}
|
|
67
|
+
</Text>
|
|
68
|
+
<Text type="xs" color="muted">
|
|
69
|
+
{step.description}
|
|
70
|
+
</Text>
|
|
71
|
+
{step.linkUrl && (
|
|
72
|
+
<Link url={step.linkUrl} target="_blank">
|
|
73
|
+
{step.linkLabel}
|
|
74
|
+
</Link>
|
|
75
|
+
)}
|
|
76
|
+
</Stack>
|
|
77
|
+
</Stack>
|
|
78
|
+
))}
|
|
79
|
+
</Stack>
|
|
80
|
+
|
|
81
|
+
<Separator />
|
|
82
|
+
|
|
83
|
+
<RoutesList />
|
|
84
|
+
</Stack>
|
|
85
|
+
</CardContent>
|
|
86
|
+
</Card>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const ConnectedWelcome: React.FC = () => (
|
|
90
|
+
<Card className="max-w-[520px] w-full">
|
|
91
|
+
<CardHeader className="items-center">
|
|
92
|
+
<CircleCheck size={40} color="#10b981" strokeWidth={1.5} />
|
|
93
|
+
<CardTitle>App is connected</CardTitle>
|
|
94
|
+
<CardDescription className="text-center">The AppBridge is active and your app is running inside the JTL Platform.</CardDescription>
|
|
95
|
+
</CardHeader>
|
|
96
|
+
<CardContent>
|
|
97
|
+
<Stack spacing="5" direction="column">
|
|
98
|
+
<RoutesList />
|
|
99
|
+
|
|
100
|
+
<Separator />
|
|
101
|
+
|
|
102
|
+
<Stack spacing="3" direction="column">
|
|
103
|
+
<Text type="xs" weight="semibold" color="muted">
|
|
104
|
+
STATUS
|
|
105
|
+
</Text>
|
|
106
|
+
<Stack spacing="2" direction="row" itemAlign="center">
|
|
107
|
+
<Text type="small">AppBridge:</Text>
|
|
108
|
+
<Badge variant="default" label="Connected" icon="check" />
|
|
109
|
+
</Stack>
|
|
110
|
+
<Stack spacing="2" direction="row" itemAlign="center">
|
|
111
|
+
<Text type="small">Mode:</Text>
|
|
112
|
+
<Text type="small" color="muted">
|
|
113
|
+
ERP Embedded
|
|
114
|
+
</Text>
|
|
115
|
+
</Stack>
|
|
116
|
+
<Text type="xs" color="muted">
|
|
117
|
+
Navigate using the ERP menu to access app features.
|
|
118
|
+
</Text>
|
|
119
|
+
</Stack>
|
|
120
|
+
</Stack>
|
|
121
|
+
</CardContent>
|
|
122
|
+
</Card>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const WelcomePage: React.FC<{ connected?: boolean }> = ({ connected = false }) => {
|
|
126
|
+
return <Box className="flex justify-center p-12">{connected ? <ConnectedWelcome /> : <StandaloneWelcome />}</Box>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default WelcomePage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WelcomePage } from './WelcomePage';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedSideEffectImports": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["vite.config.ts"]
|
|
24
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react-swc';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
// https://vite.dev/config/
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
server: {
|
|
12
|
+
port: 3004,
|
|
13
|
+
},
|
|
14
|
+
plugins: [tailwindcss(), react()],
|
|
15
|
+
resolve: {
|
|
16
|
+
alias: {
|
|
17
|
+
'/assets': path.join(path.dirname(require.resolve('@jtl-software/platform-ui-react')), 'assets'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|