ui-soxo-bootstrap-core 2.6.40-dev.0 → 2.6.40-dev.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.
@@ -1,33 +1,40 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
-
4
- name: Node.js Package
5
-
6
- on:
7
- release:
8
- types: [created]
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v3
15
- - uses: actions/setup-node@v3
16
- with:
17
- node-version: 16
18
- - run: npm i
19
- # - run: npm test
20
-
21
- publish-npm:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v3
26
- - uses: actions/setup-node@v3
27
- with:
28
- node-version: 16
29
- registry-url: https://registry.npmjs.org/
30
- - run: npm i
31
- - run: npm publish
32
- env:
33
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
1
+ name: Node.js Package
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ publish-npm:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: 20
15
+ registry-url: https://registry.npmjs.org/
16
+ - run: npm install
17
+
18
+ - name: Determine npm dist-tag
19
+ id: dist_tag
20
+ shell: bash
21
+ run: |
22
+ VERSION=$(node -p "require('./package.json').version")
23
+ echo "package.json version: $VERSION"
24
+ echo "release tag: ${GITHUB_REF_NAME}"
25
+ if [[ "v${VERSION}" != "${GITHUB_REF_NAME}" ]]; then
26
+ echo "::error::Release tag '${GITHUB_REF_NAME}' does not match package.json version 'v${VERSION}'."
27
+ echo "::error::Bump the version with 'npm version' and re-create the release."
28
+ exit 1
29
+ fi
30
+ if [[ "$VERSION" == *-dev* ]]; then
31
+ echo "tag=dev" >> "$GITHUB_OUTPUT"
32
+ echo "Will publish with dist-tag: dev"
33
+ else
34
+ echo "tag=latest" >> "$GITHUB_OUTPUT"
35
+ echo "Will publish with dist-tag: latest"
36
+ fi
37
+
38
+ - run: npm publish --access public --tag ${{ steps.dist_tag.outputs.tag }}
39
+ env:
40
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -17,6 +17,7 @@ Incorrect versioning or incorrect tags will break the publish pipeline — follo
17
17
  - Publishing via GitHub Release UI
18
18
  - How GitHub Action Detects Release Type
19
19
  - Summary Table
20
+ - CI/CD Authentication (Trusted Publishing)
20
21
  - Common Mistakes & Fixes
21
22
 
22
23
  ---
@@ -255,17 +256,14 @@ npm publish --tag dev
255
256
 
256
257
  # ⚙️ How GitHub Action Detects Release Type
257
258
 
258
- If version contains `dev`:
259
+ The workflow reads the `version` field from `package.json` at publish time:
259
260
 
260
- ```
261
- npm publish --tag dev
262
- ```
261
+ | Condition | Command | Result |
262
+ | ---------------------------- | ------------------------------------------------------- | ---------------------------------- |
263
+ | Version contains `-dev` | `npm publish --provenance --access public --tag dev` | Publishes to the `dev` dist-tag |
264
+ | Version has no `-dev` suffix | `npm publish --provenance --access public --tag latest` | Publishes to the `latest` dist-tag |
263
265
 
264
- Otherwise:
265
-
266
- ```
267
- npm publish
268
- ```
266
+ The workflow also enforces that the GitHub release tag matches `v<version>` from `package.json` and fails the run immediately if they diverge — this prevents the most common publish failure described below.
269
267
 
270
268
  ---
271
269
 
@@ -283,6 +281,37 @@ npm publish
283
281
 
284
282
  ---
285
283
 
284
+ # 🔐 CI/CD Authentication (Trusted Publishing)
285
+
286
+ As of npm's 2025 policy changes, classic automation tokens (`NPM_TOKEN`) are deprecated. This repo now authenticates to npm via **OIDC Trusted Publishing** — GitHub Actions exchanges a short-lived OIDC token for a publish token at run time, so **no secret is stored in the repository**.
287
+
288
+ ## What this means for developers
289
+
290
+ Nothing. You still follow the same flow: `npm version` → push tag → create GitHub Release. The auth happens transparently in CI.
291
+
292
+ ## What this means for maintainers
293
+
294
+ The first-time setup on npmjs.com must be done once per package:
295
+
296
+ 1. Log in to [npmjs.com](https://www.npmjs.com) → open the package (`ui-soxo-bootstrap-core`) → **Settings**.
297
+ 2. Under **Trusted Publisher**, click **Add trusted publisher** and fill in:
298
+ - Publisher: **GitHub Actions**
299
+ - Organization or user: `soxo-tech`
300
+ - Repository: `bootstrap-core`
301
+ - Workflow filename: `npm-publish.yml`
302
+ - Environment name: *(leave blank)*
303
+ 3. Save. Any old `NPM_TOKEN` repository secret can be removed.
304
+
305
+ ## Runtime requirements
306
+
307
+ The workflow runs on Node 20 and upgrades npm to the latest CLI (`npm install -g npm@latest`) because OIDC trusted publishing requires **npm ≥ 11.5.1**. The `--provenance` flag attaches a verifiable build attestation to every published version, visible on the npmjs.com package page.
308
+
309
+ ## If publish fails with `403 Forbidden` or `ENEEDAUTH`
310
+
311
+ The trusted publisher config on npmjs.com no longer matches the workflow. Check that org, repo, and workflow filename match exactly — including case.
312
+
313
+ ---
314
+
286
315
  # ⚠️ Common Mistakes & Fixes
287
316
 
288
317
  | Mistake | Issue | Fix |
@@ -1,7 +1,3 @@
1
-
2
-
3
-
4
-
5
1
  import LandingAPI from './landing-api/landing-api';
6
2
 
7
3
  import ExtraInfoDetail from './extra-info/extra-info-details';
@@ -11,11 +7,6 @@ import RootApplicationAPI from './root-application-api/root-application-api';
11
7
  import { HomePageAPI } from '../modules';
12
8
 
13
9
  import { ExternalWindow } from './external-window/external-window';
10
+ import LicenseAlert from './license-management/license-alert';
14
11
 
15
- export {
16
- LandingAPI,
17
- RootApplicationAPI,
18
- ExtraInfoDetail,
19
- HomePageAPI,
20
- ExternalWindow
21
- }
12
+ export { LandingAPI, RootApplicationAPI, ExtraInfoDetail, HomePageAPI, ExternalWindow, LicenseAlert };
@@ -2,9 +2,20 @@ import { useContext, useEffect, useRef, useState } from 'react';
2
2
 
3
3
  import { Route, Switch } from 'react-router-dom';
4
4
 
5
- import { Skeleton } from 'antd';
6
-
7
- import { Card, ChangePassword, GlobalContext, GlobalHeader, ModuleRoutes, Profile, SettingsUtil, SpotlightSearch, useTranslation } from '../../lib';
5
+ import { Skeleton, message, Modal } from 'antd';
6
+
7
+ import {
8
+ GlobalHeader,
9
+ ChangePassword,
10
+ useTranslation,
11
+ GlobalContext,
12
+ ModuleRoutes,
13
+ SpotlightSearch,
14
+ SettingsUtil,
15
+ Profile,
16
+ Card,
17
+ safeJSON,
18
+ } from '../../lib';
8
19
 
9
20
  import './landing-api.scss';
10
21
 
@@ -46,7 +57,6 @@ function getRandomMessage(previousMessage = '') {
46
57
  */
47
58
  export default function LandingApi({ history, CustomComponents, CustomModels, appSettings, transitionPending = false, onHomeReady, ...props }) {
48
59
  const [loader, setLoader] = useState(false);
49
-
50
60
  // const [modules, setModules] = useState([]);
51
61
 
52
62
  const [connected] = useState();
@@ -59,11 +69,31 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
59
69
 
60
70
  const [meta, setMeta] = useState({});
61
71
  const [loadingMessage, setLoadingMessage] = useState('');
72
+ const [licenseData, setLicenseData] = useState(null);
73
+
74
+ const [licAlert, setLicAlert] = useState(false);
75
+ // License data state
62
76
 
63
77
  // const [reports, setReports] = useState([]);
64
78
 
65
79
  var config = {};
66
80
 
81
+ //fetch license summary
82
+ // const fetchSummary = async () => {
83
+ // try {
84
+ // const res = await MenusAPI.getSummary();
85
+ // if (res?.data) {
86
+ // setLicenseData(res?.data);
87
+ // setLicAlert(true);
88
+ // } else {
89
+ // setLicenseData(null);
90
+ // setLicAlert(false);
91
+ // }
92
+ // } catch (err) {
93
+ // console.error(err);
94
+ // }
95
+ // };
96
+
67
97
  // Variable decides the control of homepage
68
98
  // #TODO This is a temporary fix - Homemage
69
99
 
@@ -73,6 +103,70 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
73
103
  disableHomepage = JSON.parse(process.env.REACT_APP_DISABLEHOMEPAGE);
74
104
  }
75
105
 
106
+ /**
107
+ * Normalizes the user's branch access list from `organization_details`.
108
+ *
109
+ * The API can return `organization_details` as a JSON string, so we always
110
+ * parse it through `safeJSON` before reading the branch collection.
111
+ *
112
+ * @returns {Array} List of branches the current user can access.
113
+ */
114
+ const getAccessibleBranches = () => {
115
+ const orgDetails = safeJSON(user?.organization_details);
116
+ return Array.isArray(orgDetails?.branch) ? orgDetails.branch : [];
117
+ };
118
+
119
+ /**
120
+ * Resolves the currently selected branch record using the persisted db pointer.
121
+ *
122
+ * @param {Array} branches
123
+ * @returns {Object|null}
124
+ */
125
+ const getCurrentBranchRecord = (branches) => {
126
+ const currentDbPtr = localStorage.getItem('db_ptr');
127
+ return branches.find((branch) => String(branch.dbPtr) === String(currentDbPtr)) || null;
128
+ };
129
+
130
+ /**
131
+ * Resolves the target branch from the URL `index` query parameter.
132
+ *
133
+ * @param {Array} branches
134
+ * @param {string|null} branchId
135
+ * @returns {Object|null}
136
+ */
137
+ const getBranchRecordById = (branches, branchId) => {
138
+ return branches.find((branch) => String(branch.branch_id) === String(branchId)) || null;
139
+ };
140
+
141
+ /**
142
+ * Persists branch-specific auth data after a successful switch.
143
+ *
144
+ * @param {Object} tokenBundle
145
+ * @param {string} dbPtr
146
+ */
147
+ const persistBranchSession = (tokenBundle, dbPtr) => {
148
+ const accessToken = tokenBundle?.access_token;
149
+ const refreshToken = tokenBundle?.refresh_token;
150
+
151
+ if (accessToken) localStorage.setItem('access_token', accessToken);
152
+ if (refreshToken) localStorage.setItem('refresh_token', refreshToken);
153
+ if (dbPtr) localStorage.setItem('db_ptr', dbPtr);
154
+ };
155
+
156
+ const fetchSummary = async () => {
157
+ try {
158
+ const res = await MenusAPI.getSummary();
159
+ if (res?.data) {
160
+ setLicenseData(res?.data);
161
+ setLicAlert(true);
162
+ } else {
163
+ setLicenseData(null);
164
+ setLicAlert(false);
165
+ }
166
+ } catch (err) {
167
+ console.error(err);
168
+ }
169
+ };
76
170
  // useEffect(() => {
77
171
 
78
172
  // // Initialize the menus for the logged in user
@@ -88,6 +182,67 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
88
182
  // }
89
183
  // }, [loader]);
90
184
 
185
+ /**
186
+ * Synchronizes the active branch with the `index` query parameter.
187
+ *
188
+ * Flow:
189
+ * 1. Read the target branch id from the URL.
190
+ * 2. Compare it against the branch represented by the current `db_ptr`.
191
+ * 3. Switch branch only when the user has access and the branch actually differs.
192
+ * 4. Refresh auth/profile state and reload menus for the new branch context.
193
+ */
194
+ useEffect(() => {
195
+ const handleUrlBranchSwitch = async () => {
196
+ const searchParams = new URLSearchParams(history.location.search);
197
+ const urlDbPtr = searchParams.get('index');
198
+ if (!urlDbPtr) return;
199
+
200
+ const accessibleBranches = getAccessibleBranches();
201
+ const currentBranch = getCurrentBranchRecord(accessibleBranches);
202
+ const targetBranch = getBranchRecordById(accessibleBranches, urlDbPtr);
203
+
204
+ if (!targetBranch || String(currentBranch?.branch_id) === String(urlDbPtr)) return;
205
+
206
+ setLoader(true);
207
+
208
+ try {
209
+ const switchResult = await MenusAPI.switchBranch(
210
+ {
211
+ firm_id: targetBranch.firm_ptr,
212
+ branch_id: targetBranch.branch_id,
213
+ },
214
+ targetBranch.dbPtr
215
+ );
216
+
217
+ if (!switchResult?.success) {
218
+ Modal.error({
219
+ title: 'Branch Switch Failed',
220
+ content: switchResult?.message || 'An error occurred while attempting to switch branches.',
221
+ });
222
+ return;
223
+ }
224
+
225
+ persistBranchSession(switchResult?.token, targetBranch.dbPtr);
226
+ window.dispatchEvent(new CustomEvent('branchChanged', { detail: targetBranch.dbPtr }));
227
+
228
+ const accessToken = switchResult?.token?.access_token;
229
+ const profileResult = await MenusAPI.getProfile(accessToken);
230
+ const updatedUser = { ...profileResult, loggedCheckDone: true };
231
+
232
+ dispatch({ type: 'user', payload: updatedUser });
233
+ localStorage.setItem('userInfo', JSON.stringify(updatedUser));
234
+
235
+ await initializeUserMenus();
236
+ } catch (error) {
237
+ console.error('Auto branch switch failed:', error);
238
+ } finally {
239
+ setLoader(false);
240
+ }
241
+ };
242
+
243
+ if (user?.id && history?.location) handleUrlBranchSwitch();
244
+ }, [history?.location?.search, user?.id]);
245
+
91
246
  useEffect(() => {
92
247
  // Initialize the menus for the logged in user
93
248
  initializeUserMenus();
@@ -138,9 +293,12 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
138
293
  */
139
294
  async function initializeUserMenus() {
140
295
  // need to find what implement, with a login who has the respective value ("wug_custreportids")
296
+
141
297
  const report = await loadScripts(user);
142
298
 
143
299
  await loadMenus(report);
300
+ // fetch license summary
301
+ fetchSummary();
144
302
  }
145
303
 
146
304
  // const keyMap = {
@@ -162,7 +320,7 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
162
320
  setLoader(true);
163
321
 
164
322
  // setReports(report)
165
-
323
+ fetchSummary();
166
324
  const result = await MenusAPI.getMenus(user);
167
325
 
168
326
  // console.log(result);
@@ -292,6 +450,8 @@ export default function LandingApi({ history, CustomComponents, CustomModels, ap
292
450
  modules={allModules}
293
451
  user={user}
294
452
  history={history}
453
+ licenseData={licenseData}
454
+ licAlert={licAlert}
295
455
  >
296
456
  {loader ? (
297
457
  <Card className="skeleton-card">
@@ -0,0 +1,97 @@
1
+ import { Alert } from 'antd';
2
+ import React, { useState, useEffect } from 'react';
3
+
4
+ export default function LicenseAlert({ data }) {
5
+ // setting visibility of alert based on license status
6
+ const [visible, setVisible] = useState(true);
7
+ // resolve alert configuration based on license data
8
+ const alertConfig = resolveLicenseAlert(data);
9
+ // auto-hide alert after 10 seconds or when data changes
10
+ useEffect(() => {
11
+ if (alertConfig) {
12
+ setVisible(true);
13
+
14
+ const timer = setTimeout(() => {
15
+ setVisible(false);
16
+ }, 10000); // 10 seconds
17
+
18
+ return () => {
19
+ clearTimeout(timer);
20
+ };
21
+ }
22
+ }, [data]);
23
+ // if no alert configuration or not visible, render nothing
24
+ if (!alertConfig || !visible) return null;
25
+
26
+ return (
27
+ // render the alert with appropriate type, message, and description
28
+ <Alert
29
+ type={alertConfig.type}
30
+ message={alertConfig.message}
31
+ description={alertConfig.description}
32
+ showIcon
33
+ closable
34
+ onClose={() => setVisible(false)}
35
+ />
36
+ );
37
+ }
38
+ // function to determine alert configuration based on license data
39
+ function resolveLicenseAlert(data) {
40
+ if (!data) return null;
41
+ // destructure relevant fields from license data
42
+ const { status, expiresInDays, isExpiringSoon, gracePeriod } = data;
43
+
44
+ // ===== NOT INSTALLED =====
45
+ if (status === 'NOT_INSTALLED') {
46
+ return {
47
+ type: 'error',
48
+ message: 'License not found',
49
+ description: 'Please install a valid license to continue.',
50
+ };
51
+ }
52
+
53
+ // ===== GRACE PERIOD =====
54
+ if (gracePeriod) {
55
+ return {
56
+ type: 'warning',
57
+ message: 'Grace period mode',
58
+ description: 'License expired. Running in read-only mode.',
59
+ };
60
+ }
61
+
62
+ // ===== EXPIRING SOON =====
63
+ if (status === 'ACTIVE' && isExpiringSoon) {
64
+ let descriptionText = '';
65
+ // customize message based on how soon the license is expiring
66
+ if (expiresInDays === 0) {
67
+ descriptionText = 'Your license will expire today. Please renew immediately.';
68
+ } else {
69
+ descriptionText = `Your license will expire in ${expiresInDays} days. Please plan for renewal.`;
70
+ }
71
+
72
+ return {
73
+ type: 'warning',
74
+ message: 'License expiring soon',
75
+ description: descriptionText,
76
+ };
77
+ }
78
+
79
+ // ===== NOT INSTALLED =====
80
+ if (status === 'NOT_INSTALLED') {
81
+ return {
82
+ type: 'error',
83
+ message: 'License not found',
84
+ description: 'Please install a valid license to continue.',
85
+ };
86
+ }
87
+ // =====EXPIRED=====
88
+ if (status === 'EXPIRED') {
89
+ return {
90
+ type: 'error',
91
+ message: 'License expired',
92
+ description: 'Your license has expired. Please renew or install a new license.',
93
+ };
94
+ }
95
+
96
+ return null;
97
+ }