@adobedjangir/commerce-admin-management 0.0.2

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.
Files changed (48) hide show
  1. package/README.md +637 -0
  2. package/actions/commerce-creds.js +262 -0
  3. package/actions/configurations/commerce/index.js +41 -0
  4. package/actions/configurations/commerce-connection-save/index.js +53 -0
  5. package/actions/configurations/commerce-connection-status/index.js +27 -0
  6. package/actions/configurations/commerce-connection-test/index.js +47 -0
  7. package/actions/configurations/export-config/index.js +256 -0
  8. package/actions/configurations/ext.config.yaml +192 -0
  9. package/actions/configurations/import-config/index.js +541 -0
  10. package/actions/configurations/registration/index.js +37 -0
  11. package/actions/configurations/sync-store-mappings-from-commerce/index.js +190 -0
  12. package/actions/configurations/system-config-list/index.js +127 -0
  13. package/actions/configurations/system-config-save/index.js +160 -0
  14. package/actions/configurations/system-config-schema/index.js +327 -0
  15. package/actions/utils.js +73 -0
  16. package/package.json +80 -0
  17. package/scripts/build-web.js +58 -0
  18. package/scripts/setup.js +445 -0
  19. package/src/abdb-config.js +8 -0
  20. package/src/abdb-helper.js +8 -0
  21. package/src/index.js +22 -0
  22. package/src/oauth1a.js +8 -0
  23. package/src/system-config-crypto.js +8 -0
  24. package/src/system-config-shared.js +8 -0
  25. package/web/dist/index.css +305 -0
  26. package/web/dist/index.js +2986 -0
  27. package/web/index.js +7 -0
  28. package/web/src/components/App.js +54 -0
  29. package/web/src/components/AppSectionNav.js +49 -0
  30. package/web/src/components/CommerceSetupWizard.js +160 -0
  31. package/web/src/components/ExtensionRegistration.js +33 -0
  32. package/web/src/components/MainPage.js +147 -0
  33. package/web/src/components/SystemConfig.js +1464 -0
  34. package/web/src/components/SystemConfigSchemaEditor.js +459 -0
  35. package/web/src/hooks/useConfirm.js +355 -0
  36. package/web/src/hooks/useSystemConfig.js +238 -0
  37. package/web/src/hooks/useSystemConfigSchema.js +102 -0
  38. package/web/src/index.js +46 -0
  39. package/web/src/nav-icons.js +30 -0
  40. package/web/src/nav.json +10 -0
  41. package/web/src/pages/index.js +17 -0
  42. package/web/src/schema/systemConfigSchema.js +82 -0
  43. package/web/src/settings.js +101 -0
  44. package/web/src/styles/index.css +337 -0
  45. package/web/src/theme.js +104 -0
  46. package/web/src/utils/storeMappingsFromCommerceRest.js +73 -0
  47. package/web/src/utils.js +52 -0
  48. package/web/styles.css +337 -0
package/web/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ Licensed under the Apache License, Version 2.0
4
+ */
5
+
6
+ import './dist/index.css'
7
+ export * from './dist/index.js'
@@ -0,0 +1,54 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ */
7
+
8
+ import React from 'react'
9
+ import { Provider, lightTheme } from '@adobe/react-spectrum'
10
+ import { ErrorBoundary } from 'react-error-boundary'
11
+ import { Route, Routes, HashRouter } from 'react-router-dom'
12
+ import ExtensionRegistration from './ExtensionRegistration'
13
+
14
+ function App (props) {
15
+ props.runtime.on('configuration', ({ imsOrg, imsToken }) => {
16
+ console.log('configuration change', { imsOrg, imsToken })
17
+ })
18
+
19
+ return (
20
+ <ErrorBoundary onError={onError} FallbackComponent={fallbackComponent}>
21
+ <HashRouter>
22
+ <Provider
23
+ theme={lightTheme}
24
+ colorScheme="light"
25
+ UNSAFE_className="sm-provider"
26
+ >
27
+ <Routes>
28
+ {/* Catch-all: every hash path renders the same shell. MainPage's
29
+ nav registry (getNavItems/getPageComponent) reads
30
+ location.pathname and chooses which page component to mount,
31
+ so React-Router only needs one route here. */}
32
+ <Route
33
+ path="*"
34
+ element={<ExtensionRegistration runtime={props.runtime} ims={props.ims} />}
35
+ />
36
+ </Routes>
37
+ </Provider>
38
+ </HashRouter>
39
+ </ErrorBoundary>
40
+ )
41
+
42
+ function onError (e, componentStack) {}
43
+
44
+ function fallbackComponent ({ componentStack, error }) {
45
+ return (
46
+ <React.Fragment>
47
+ <h1 style={{ textAlign: 'center', marginTop: '20px' }}>Something went wrong :(</h1>
48
+ <pre>{componentStack + '\n' + error.message}</pre>
49
+ </React.Fragment>
50
+ )
51
+ }
52
+ }
53
+
54
+ export default App
@@ -0,0 +1,49 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ Licensed under the Apache License, Version 2.0
4
+ */
5
+
6
+ import { useLocation, useNavigate } from 'react-router-dom'
7
+ import { getNavItems } from '../settings'
8
+ import { getNavIcon } from '../nav-icons'
9
+
10
+ /**
11
+ * Top-level navigation. Driven entirely by nav.json + configureWeb({ extraNav }).
12
+ * To add a tab: register a page in pages/ and add a matching entry to nav.json
13
+ * (or pass it via host configureWeb).
14
+ */
15
+ export default function AppSectionNav ({ rightSlot } = {}) {
16
+ const navigate = useNavigate()
17
+ const location = useLocation()
18
+ const items = getNavItems()
19
+ const activeKey = items.some((it) => it.path === location.pathname)
20
+ ? location.pathname
21
+ : (items[0] && items[0].path) || '/'
22
+
23
+ return (
24
+ <div className="sm-tab-bar">
25
+ <div className="sm-tab-bar__track" role="tablist" aria-label="Application sections">
26
+ {items.map(({ id, path, label, icon }) => {
27
+ const Icon = getNavIcon(icon)
28
+ const active = path === activeKey
29
+ return (
30
+ <button
31
+ key={id}
32
+ type="button"
33
+ role="tab"
34
+ aria-selected={active}
35
+ className={`sm-tab${active ? ' is-active' : ''}`}
36
+ onClick={() => { if (!active) navigate(path) }}
37
+ >
38
+ <span className="sm-tab__icon">
39
+ <Icon size="XS" />
40
+ </span>
41
+ {label}
42
+ </button>
43
+ )
44
+ })}
45
+ </div>
46
+ {rightSlot ? <div className="sm-tab-bar__actions">{rightSlot}</div> : null}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,160 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ Licensed under the Apache License, Version 2.0
4
+ */
5
+
6
+ import React, { useState } from 'react'
7
+ import {
8
+ View,
9
+ Flex,
10
+ Heading,
11
+ Text,
12
+ TextField,
13
+ Button,
14
+ ButtonGroup,
15
+ ProgressCircle,
16
+ StatusLight,
17
+ Divider,
18
+ Form
19
+ } from '@adobe/react-spectrum'
20
+ import { callAction } from '../utils'
21
+ import { getActionKey } from '../settings'
22
+
23
+ const FIELD_DEFS = [
24
+ { key: 'baseUrl', label: 'Commerce base URL', placeholder: 'https://store.example.com/', type: 'text' },
25
+ { key: 'consumerKey', label: 'Consumer key', placeholder: '', type: 'text' },
26
+ { key: 'consumerSecret', label: 'Consumer secret', placeholder: '', type: 'password' },
27
+ { key: 'accessToken', label: 'Access token', placeholder: '', type: 'text' },
28
+ { key: 'accessTokenSecret', label: 'Access token secret', placeholder: '', type: 'password' }
29
+ ]
30
+
31
+ const EMPTY = FIELD_DEFS.reduce((a, f) => { a[f.key] = ''; return a }, {})
32
+
33
+ /**
34
+ * First-run wizard for Adobe Commerce REST connection.
35
+ *
36
+ * Props:
37
+ * runtime, ims — passed through to callAction
38
+ * initial — masked saved values (used to prefill the URL when "Reconfigure")
39
+ * onCompleted() — called after a successful save; parent should refetch status
40
+ * onCancel?() — only rendered when present (Reconfigure mode)
41
+ */
42
+ export default function CommerceSetupWizard ({ runtime, ims, initial, onCompleted, onCancel }) {
43
+ const [values, setValues] = useState(() => ({
44
+ ...EMPTY,
45
+ ...(initial && initial.baseUrl ? { baseUrl: initial.baseUrl } : {})
46
+ }))
47
+ const [testState, setTestState] = useState({ status: 'idle', message: '' })
48
+ const [saveState, setSaveState] = useState({ status: 'idle', message: '' })
49
+
50
+ const set = (k) => (v) => setValues((prev) => ({ ...prev, [k]: v }))
51
+
52
+ const allFilled = FIELD_DEFS.every((f) => String(values[f.key] || '').trim() !== '')
53
+
54
+ async function handleTest () {
55
+ setTestState({ status: 'running', message: 'Testing connection…' })
56
+ try {
57
+ const res = await callAction(
58
+ { runtime, ims },
59
+ getActionKey('commerceConnectionTest'),
60
+ '',
61
+ values
62
+ )
63
+ const body = res && res.body ? res.body : res
64
+ if (body && body.ok) {
65
+ setTestState({ status: 'ok', message: body.message || 'Connection OK' })
66
+ } else {
67
+ setTestState({ status: 'fail', message: (body && body.message) || 'Connection failed' })
68
+ }
69
+ } catch (e) {
70
+ setTestState({ status: 'fail', message: e.message || 'Connection failed' })
71
+ }
72
+ }
73
+
74
+ async function handleSave () {
75
+ setSaveState({ status: 'running', message: 'Saving…' })
76
+ try {
77
+ const res = await callAction(
78
+ { runtime, ims },
79
+ getActionKey('commerceConnectionSave'),
80
+ '',
81
+ values
82
+ )
83
+ const body = res && res.body ? res.body : res
84
+ if (body && body.ok && body.saved) {
85
+ setSaveState({ status: 'ok', message: 'Saved. Loading the rest of the app…' })
86
+ // Hand off to parent — it should refetch status and unmount the wizard.
87
+ if (typeof onCompleted === 'function') onCompleted(body)
88
+ } else {
89
+ setSaveState({ status: 'fail', message: (body && body.message) || 'Save failed' })
90
+ }
91
+ } catch (e) {
92
+ setSaveState({ status: 'fail', message: e.message || 'Save failed' })
93
+ }
94
+ }
95
+
96
+ const testLight = testState.status === 'ok' ? 'positive'
97
+ : testState.status === 'fail' ? 'negative'
98
+ : testState.status === 'running' ? 'notice'
99
+ : 'neutral'
100
+
101
+ return (
102
+ <View padding="size-400" maxWidth="size-6000" margin="0 auto">
103
+ <Heading level={2}>Connect to Adobe Commerce</Heading>
104
+ <Text>
105
+ Enter the REST/OAuth credentials for your Commerce instance. They are encrypted before
106
+ being saved to App Builder Database. The rest of the app stays disabled until the
107
+ connection is verified.
108
+ </Text>
109
+ <Divider size="S" marginY="size-300" />
110
+
111
+ <Form isRequired necessityIndicator="icon" labelPosition="top">
112
+ {FIELD_DEFS.map((f) => (
113
+ <TextField
114
+ key={f.key}
115
+ label={f.label}
116
+ placeholder={f.placeholder}
117
+ type={f.type === 'password' ? 'password' : 'text'}
118
+ value={values[f.key]}
119
+ onChange={set(f.key)}
120
+ autoComplete="off"
121
+ isRequired
122
+ width="100%"
123
+ />
124
+ ))}
125
+ </Form>
126
+
127
+ <View marginTop="size-300">
128
+ <Flex alignItems="center" gap="size-200" wrap>
129
+ <ButtonGroup>
130
+ <Button variant="secondary" onPress={handleTest} isDisabled={!allFilled || testState.status === 'running'}>
131
+ {testState.status === 'running' ? 'Testing…' : 'Test connection'}
132
+ </Button>
133
+ <Button
134
+ variant="cta"
135
+ onPress={handleSave}
136
+ isDisabled={!allFilled || testState.status === 'running' || saveState.status === 'running'}
137
+ >
138
+ {saveState.status === 'running' ? 'Saving…' : 'Save & continue'}
139
+ </Button>
140
+ {onCancel ? (
141
+ <Button variant="secondary" onPress={onCancel}>Cancel</Button>
142
+ ) : null}
143
+ </ButtonGroup>
144
+ {testState.status !== 'idle' && (
145
+ <Flex alignItems="center" gap="size-100">
146
+ {testState.status === 'running' && <ProgressCircle size="S" isIndeterminate aria-label="Testing" />}
147
+ <StatusLight variant={testLight}>{testState.message}</StatusLight>
148
+ </Flex>
149
+ )}
150
+ </Flex>
151
+ {saveState.status === 'fail' && (
152
+ <View marginTop="size-150"><StatusLight variant="negative">{saveState.message}</StatusLight></View>
153
+ )}
154
+ {saveState.status === 'ok' && (
155
+ <View marginTop="size-150"><StatusLight variant="positive">{saveState.message}</StatusLight></View>
156
+ )}
157
+ </View>
158
+ </View>
159
+ )
160
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { register } from '@adobe/uix-guest'
14
+ import { MainPage } from './MainPage'
15
+ import { useEffect } from 'react'
16
+ import { getExtensionId } from '../settings'
17
+
18
+ export default function ExtensionRegistration(props) {
19
+
20
+ useEffect(() => {
21
+ (async () => {
22
+
23
+ await register({
24
+ id: getExtensionId(),
25
+ methods: {
26
+ }
27
+ })
28
+
29
+ })()
30
+ }, [])
31
+
32
+ return <MainPage ims={props.ims} runtime={props.runtime} />
33
+ }
@@ -0,0 +1,147 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ Licensed under the Apache License, Version 2.0
4
+ */
5
+
6
+ import { View, Flex, ProgressCircle, Text, Button, IllustratedMessage, Heading } from '@adobe/react-spectrum'
7
+ import { attach } from '@adobe/uix-guest'
8
+ import React, { useEffect, useState, useCallback } from 'react'
9
+ import { ErrorBoundary } from 'react-error-boundary'
10
+ import { useLocation } from 'react-router-dom'
11
+ import { getExtensionId, getActionKey, getNavItems, getPageComponent } from '../settings'
12
+ import { callAction } from '../utils'
13
+ import AppSectionNav from './AppSectionNav'
14
+ import CommerceSetupWizard from './CommerceSetupWizard'
15
+
16
+ export const MainPage = props => {
17
+ const location = useLocation()
18
+
19
+ // Commerce connection gate. `status === 'unknown'` while we're loading,
20
+ // 'configured' / 'unconfigured' after the first probe. `reconfiguring`
21
+ // re-shows the wizard for an already-configured connection.
22
+ const [status, setStatus] = useState('unknown')
23
+ const [maskedCreds, setMaskedCreds] = useState(null)
24
+ const [error, setError] = useState(null)
25
+ const [reconfiguring, setReconfiguring] = useState(false)
26
+
27
+ const fetchStatus = useCallback(async () => {
28
+ try {
29
+ const res = await callAction(
30
+ props,
31
+ getActionKey('commerceConnectionStatus'),
32
+ '',
33
+ { fresh: true }
34
+ )
35
+ const body = res && res.body ? res.body : res
36
+ setMaskedCreds(body && body.creds ? body.creds : null)
37
+ setStatus(body && body.configured ? 'configured' : 'unconfigured')
38
+ setError(null)
39
+ } catch (e) {
40
+ setError(e.message || 'Failed to load Commerce connection status')
41
+ setStatus('error')
42
+ }
43
+ }, [props])
44
+
45
+ useEffect(() => {
46
+ // Kick off connection-status check immediately — the action is configured
47
+ // with require-adobe-auth: false, so it doesn't need an IMS token. The
48
+ // IMS handshake runs in parallel and is best-effort: in raw localhost
49
+ // (outside the Experience Cloud Shell iframe) `attach()` would hang
50
+ // forever waiting for a parent that isn't there, so we time it out.
51
+ fetchStatus()
52
+
53
+ if (props.ims.token) return
54
+ let cancelled = false
55
+ const handshake = Promise.race([
56
+ attach({ id: getExtensionId() }).then((gc) => ({
57
+ token: gc?.sharedContext?.get('imsToken'),
58
+ org: gc?.sharedContext?.get('imsOrgId')
59
+ })),
60
+ new Promise((resolve) => setTimeout(() => resolve(null), 2000))
61
+ ])
62
+ handshake.then((res) => {
63
+ if (cancelled || !res) return
64
+ if (res.token) props.ims.token = res.token
65
+ if (res.org) props.ims.org = res.org
66
+ }).catch(() => {})
67
+ return () => { cancelled = true }
68
+ }, [fetchStatus])
69
+
70
+ if (status === 'unknown') {
71
+ return (
72
+ <Flex alignItems="center" justifyContent="center" height="size-3000">
73
+ <Flex direction="column" alignItems="center" gap="size-150">
74
+ <ProgressCircle size="L" isIndeterminate aria-label="Loading" />
75
+ <Text>Checking Commerce connection…</Text>
76
+ </Flex>
77
+ </Flex>
78
+ )
79
+ }
80
+
81
+ if (status === 'error') {
82
+ return (
83
+ <View padding="size-400">
84
+ <IllustratedMessage>
85
+ <Heading>Could not load connection status</Heading>
86
+ <Text>{error}</Text>
87
+ </IllustratedMessage>
88
+ <Flex marginTop="size-200" justifyContent="center">
89
+ <Button variant="cta" onPress={fetchStatus}>Retry</Button>
90
+ </Flex>
91
+ </View>
92
+ )
93
+ }
94
+
95
+ if (status === 'unconfigured' || reconfiguring) {
96
+ return (
97
+ <CommerceSetupWizard
98
+ runtime={props.runtime}
99
+ ims={props.ims}
100
+ initial={maskedCreds}
101
+ onCompleted={() => {
102
+ setReconfiguring(false)
103
+ fetchStatus()
104
+ }}
105
+ onCancel={reconfiguring ? () => setReconfiguring(false) : undefined}
106
+ />
107
+ )
108
+ }
109
+
110
+ // Pick the page component for the active path from the nav registry.
111
+ // To add a new page: register it in pages/index.js (or host extraPages)
112
+ // and add an entry to nav.json (or host extraNav).
113
+ const items = getNavItems()
114
+ const match = items.find((it) => it.path === location.pathname) || items[0]
115
+ const Page = match ? getPageComponent(match.id) : null
116
+
117
+ const pageFallback = ({ error }) => (
118
+ <View padding="size-400">
119
+ <Heading level={3}>This page crashed</Heading>
120
+ <Text>
121
+ {match ? `Error in page "${match.id}": ` : ''}{error && error.message ? error.message : String(error)}
122
+ </Text>
123
+ </View>
124
+ )
125
+
126
+ return (
127
+ <View UNSAFE_style={{ overflowX: 'clip' }}>
128
+ <AppSectionNav
129
+ rightSlot={
130
+ <Button variant="secondary" onPress={() => setReconfiguring(true)}>
131
+ Reconfigure Commerce
132
+ </Button>
133
+ }
134
+ />
135
+ <View>
136
+ {Page
137
+ ? (
138
+ <ErrorBoundary FallbackComponent={pageFallback}>
139
+ <Page runtime={props.runtime} ims={props.ims} />
140
+ </ErrorBoundary>
141
+ )
142
+ : <View padding="size-400"><Text>No page registered for this route.</Text></View>
143
+ }
144
+ </View>
145
+ </View>
146
+ )
147
+ }