@jtl-software/cloud-app-template-frontend-react 0.0.7 → 0.0.8
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 +6 -0
- package/manifest.json +5 -0
- package/package.json +3 -1
- package/src/App.tsx +4 -2
- package/src/pages/graphql-demo-page/GraphqlDemoPage.tsx +202 -0
- package/src/pages/graphql-demo-page/IGraphqlDemoPageProps.ts +5 -0
- package/src/pages/graphql-demo-page/index.ts +1 -0
- package/src/pages/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @jtl/cloud-app-template-frontend-react
|
|
2
2
|
|
|
3
|
+
## 0.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#20](https://github.com/jtl-software/cloud-apps-cli/pull/20) [`ab20518`](https://github.com/jtl-software/cloud-apps-cli/commit/ab20518f077cb18d8aca24944ca182e3142d16a6) Thanks [@tobilen](https://github.com/tobilen)! - Add GraphQL example implementation
|
|
8
|
+
|
|
3
9
|
## 0.0.7
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/cloud-app-template-frontend-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@jtl-software/cloud-apps-core": "latest",
|
|
23
23
|
"@jtl-software/platform-ui-react": "latest",
|
|
24
|
+
"graphql": "^16.13.2",
|
|
25
|
+
"graphql-request": "^7.4.0",
|
|
24
26
|
"react": "^19.0.0",
|
|
25
27
|
"react-dom": "^19.0.0"
|
|
26
28
|
},
|
package/src/App.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AppBridge } from '@jtl-software/cloud-apps-core';
|
|
2
2
|
import './App.css';
|
|
3
|
-
import { ErpPage, HubPage, PanePage, SetupPage, WelcomePage } from './pages';
|
|
3
|
+
import { ErpPage, GraphqlDemoPage, HubPage, PanePage, SetupPage, WelcomePage } from './pages';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
5
|
|
|
6
|
-
type AppMode = 'setup' | 'erp' | 'pane' | 'hub';
|
|
6
|
+
type AppMode = 'setup' | 'erp' | 'pane' | 'hub' | 'graphql-demo';
|
|
7
7
|
|
|
8
8
|
const App: React.FC<{ appBridge: AppBridge | null }> = ({ appBridge }) => {
|
|
9
9
|
const mode: AppMode = location.pathname.substring(1) as AppMode;
|
|
@@ -30,6 +30,8 @@ const App: React.FC<{ appBridge: AppBridge | null }> = ({ appBridge }) => {
|
|
|
30
30
|
return <ErpPage appBridge={appBridge} />;
|
|
31
31
|
case 'pane':
|
|
32
32
|
return <PanePage appBridge={appBridge} />;
|
|
33
|
+
case 'graphql-demo':
|
|
34
|
+
return <GraphqlDemoPage appBridge={appBridge} />;
|
|
33
35
|
default:
|
|
34
36
|
return <WelcomePage connected />;
|
|
35
37
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import IGraphqlDemoPageProps from './IGraphqlDemoPageProps';
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardContent,
|
|
9
|
+
Text,
|
|
10
|
+
Stack,
|
|
11
|
+
Separator,
|
|
12
|
+
Box,
|
|
13
|
+
Button,
|
|
14
|
+
} from '@jtl-software/platform-ui-react';
|
|
15
|
+
import { Database, Package, ShoppingCart, BarChart3 } from 'lucide-react';
|
|
16
|
+
import { GraphQLClient, gql } from 'graphql-request';
|
|
17
|
+
import { apiUrl } from '../../common/constants';
|
|
18
|
+
|
|
19
|
+
function createClient(sessionToken: string) {
|
|
20
|
+
return new GraphQLClient(`${apiUrl}/graphql`, {
|
|
21
|
+
headers: { 'X-Session-Token': sessionToken },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const QUERIES = {
|
|
26
|
+
topItems: gql`query TopItems {
|
|
27
|
+
QueryItems(first: 5, order: [{ stockInOrders: DESC }]) {
|
|
28
|
+
totalCount
|
|
29
|
+
nodes {
|
|
30
|
+
sku
|
|
31
|
+
name
|
|
32
|
+
stockInOrders
|
|
33
|
+
salesPriceGross
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}`,
|
|
37
|
+
recentOrders: gql`query RecentOrders {
|
|
38
|
+
QuerySalesOrders(first: 5, order: [{ salesOrderDate: DESC }]) {
|
|
39
|
+
totalCount
|
|
40
|
+
nodes {
|
|
41
|
+
salesOrderNumber
|
|
42
|
+
salesOrderDate
|
|
43
|
+
totalGrossAmount
|
|
44
|
+
currencyIso
|
|
45
|
+
companyName
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}`,
|
|
49
|
+
stockOverview: gql`query StockOverview {
|
|
50
|
+
QueryItems(first: 5, order: [{ stockTotal: DESC }]) {
|
|
51
|
+
totalCount
|
|
52
|
+
nodes {
|
|
53
|
+
sku
|
|
54
|
+
name
|
|
55
|
+
stockTotal
|
|
56
|
+
stockAvailable
|
|
57
|
+
stockInOrders
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type QueryKey = keyof typeof QUERIES;
|
|
64
|
+
|
|
65
|
+
const QUERY_META: Record<QueryKey, { label: string; icon: React.FC<{ size?: number; color?: string; strokeWidth?: number; className?: string }> }> = {
|
|
66
|
+
topItems: { label: 'Top Items by Orders', icon: ShoppingCart },
|
|
67
|
+
recentOrders: { label: 'Recent Sales Orders', icon: BarChart3 },
|
|
68
|
+
stockOverview: { label: 'Stock Overview', icon: Package },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const GraphqlDemoPage: React.FC<IGraphqlDemoPageProps> = ({ appBridge }) => {
|
|
72
|
+
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
const [loading, setLoading] = useState<QueryKey | null>(null);
|
|
74
|
+
const [results, setResults] = useState<Record<string, unknown>>({});
|
|
75
|
+
|
|
76
|
+
const runQuery = useCallback(
|
|
77
|
+
async (key: QueryKey) => {
|
|
78
|
+
setLoading(key);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const sessionToken = await appBridge.method.call<string>('getSessionToken');
|
|
82
|
+
const client = createClient(sessionToken);
|
|
83
|
+
const data = await client.request(QUERIES[key]);
|
|
84
|
+
setResults(prev => ({ ...prev, [key]: data }));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(null);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[appBridge],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const formatValue = (value: unknown): string => {
|
|
95
|
+
if (value == null) return '–';
|
|
96
|
+
if (typeof value === 'number') return value.toLocaleString('de-DE');
|
|
97
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
98
|
+
return new Date(value).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
99
|
+
}
|
|
100
|
+
return String(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const renderResult = (key: QueryKey) => {
|
|
104
|
+
const data = results[key] as Record<string, { totalCount: number; nodes: Record<string, unknown>[] }> | undefined;
|
|
105
|
+
if (!data) return null;
|
|
106
|
+
|
|
107
|
+
const queryResult = Object.values(data)[0];
|
|
108
|
+
if (!queryResult?.nodes?.length) {
|
|
109
|
+
return (
|
|
110
|
+
<Text type="small" color="muted">
|
|
111
|
+
No results found.
|
|
112
|
+
</Text>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const columns = Object.keys(queryResult.nodes[0]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Stack spacing="2" direction="column">
|
|
120
|
+
<Text type="xs" color="muted">
|
|
121
|
+
{queryResult.totalCount} total results — showing first {queryResult.nodes.length}
|
|
122
|
+
</Text>
|
|
123
|
+
<Box className="w-full overflow-x-auto">
|
|
124
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
|
125
|
+
<thead>
|
|
126
|
+
<tr>
|
|
127
|
+
{columns.map(col => (
|
|
128
|
+
<th key={col} style={{ textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--base-border)', fontWeight: 600 }}>
|
|
129
|
+
{col}
|
|
130
|
+
</th>
|
|
131
|
+
))}
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
{queryResult.nodes.map((node, i) => (
|
|
136
|
+
<tr key={i}>
|
|
137
|
+
{columns.map(col => (
|
|
138
|
+
<td key={col} style={{ padding: '6px 8px', borderBottom: '1px solid var(--base-border)' }}>
|
|
139
|
+
{formatValue(node[col])}
|
|
140
|
+
</td>
|
|
141
|
+
))}
|
|
142
|
+
</tr>
|
|
143
|
+
))}
|
|
144
|
+
</tbody>
|
|
145
|
+
</table>
|
|
146
|
+
</Box>
|
|
147
|
+
</Stack>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Box className="flex justify-center p-12">
|
|
153
|
+
<Card className="max-w-[720px] w-full">
|
|
154
|
+
<CardHeader className="items-center">
|
|
155
|
+
<Database size={40} color="#1a56db" strokeWidth={1.5} />
|
|
156
|
+
<CardTitle>GraphQL API Demo</CardTitle>
|
|
157
|
+
<CardDescription className="text-center">
|
|
158
|
+
This page demonstrates querying the JTL ERP GraphQL API from a Cloud App. Requests are proxied through
|
|
159
|
+
the backend, which authenticates with client credentials and resolves the tenant from the session token.
|
|
160
|
+
</CardDescription>
|
|
161
|
+
</CardHeader>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<Stack spacing="5" direction="column">
|
|
164
|
+
{error && (
|
|
165
|
+
<Box className="w-full p-3 rounded-lg bg-red-50 border border-red-200">
|
|
166
|
+
<Text type="small" color="destructive">
|
|
167
|
+
{error}
|
|
168
|
+
</Text>
|
|
169
|
+
</Box>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
<Separator />
|
|
173
|
+
|
|
174
|
+
{(Object.keys(QUERIES) as QueryKey[]).map(key => {
|
|
175
|
+
const meta = QUERY_META[key];
|
|
176
|
+
const Icon = meta.icon;
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Stack key={key} spacing="3" direction="column">
|
|
180
|
+
<Stack spacing="2" direction="row" itemAlign="center">
|
|
181
|
+
<Icon size={16} color="#1a56db" className="shrink-0" />
|
|
182
|
+
<Text type="small" weight="semibold">
|
|
183
|
+
{meta.label}
|
|
184
|
+
</Text>
|
|
185
|
+
</Stack>
|
|
186
|
+
<Box className="w-full p-3 rounded-lg" style={{ background: 'var(--base-muted)', fontFamily: 'monospace', fontSize: '0.75rem', whiteSpace: 'pre', overflowX: 'auto' }}>
|
|
187
|
+
{QUERIES[key].trim()}
|
|
188
|
+
</Box>
|
|
189
|
+
<Button onClick={() => runQuery(key)} disabled={loading !== null} label={loading === key ? 'Loading...' : 'Run Query'} />
|
|
190
|
+
{renderResult(key)}
|
|
191
|
+
<Separator />
|
|
192
|
+
</Stack>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
195
|
+
</Stack>
|
|
196
|
+
</CardContent>
|
|
197
|
+
</Card>
|
|
198
|
+
</Box>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default GraphqlDemoPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as GraphqlDemoPage } from './GraphqlDemoPage';
|
package/src/pages/index.ts
CHANGED