@visns-studio/visns-components 6.31.1 → 6.32.0

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/README.md CHANGED
@@ -9,6 +9,53 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
9
9
 
10
10
  VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
11
11
 
12
+ ## Recent Updates (v6.32.0)
13
+
14
+ ### The pre-6.15 boxed overview is back, as an opt-in
15
+
16
+ 6.15.0 rebuilt the `GenericDetail` overview tab: one card per section, fields
17
+ as stacked label-above-value pairs on a two-column grid, the heading
18
+ left-aligned on a hairline. That stays the default. But a project whose users
19
+ read their detail pages as a grid of discrete facts — every field in its own
20
+ tinted, bordered box with a run-in `Label: value`, under a centred tinted
21
+ heading bar — can now ask for the old treatment back across the whole app,
22
+ the same way `setDefaultFormLayout` works:
23
+
24
+ ```jsx
25
+ import { setDefaultOverviewStyle } from '@visns-studio/visns-components';
26
+
27
+ setDefaultOverviewStyle('boxed'); // once, at bootstrap, before the first render
28
+ ```
29
+
30
+ A single page can go the other way, or opt in on its own, from its view
31
+ config: `"page": { "overviewStyle": "boxed" }` — or `"plain"` — wins over the
32
+ project default. `'plain'` is the shipped default, so an app that calls
33
+ nothing is completely unchanged. `getDefaultOverviewStyle()` is exported too.
34
+
35
+ ### The title bar's counts no longer run together
36
+
37
+ `GenericIndex` prints each `page.tableInfo.heading` and the "N total" count as
38
+ its own span in the title bar. Until 6.15.0 those spans carried their own
39
+ spacing — `[0 Enquiry Generated Today] ` — and when the brackets were retired
40
+ the only space between them went with them, so a leads page read
41
+ "0 Enquiry Generated Today0 Enquiry with Follow Ups79 total". The tab-strip and
42
+ embedded rows already laid the same node out as a flex row with a 0.75rem gap;
43
+ the `.crmtitle` / `.crmtitleSimple` bar now does the same (wrapping when the
44
+ bar is narrow). CSS only; no markup or config change.
45
+
46
+ ## Recent Updates (v6.31.2)
47
+
48
+ ### `MergeEntity` compares the two records it is actually merging
49
+
50
+ `processTemplate` substituted `{dataId}` with the current (source) entity
51
+ before applying the caller's own values, so `loadTargetEntity(entityId)`
52
+ fetched the source a second time. Step 3 then diffed the source against
53
+ itself, always reported "no conflicts", and sent an empty `field_overrides`
54
+ map with the merge. Explicit template values now win and `{dataId}` only
55
+ falls back to the current entity when nothing was supplied. Every consumer's
56
+ merge tab (contacts, clients, …) gets a real conflict step without a config
57
+ change.
58
+
12
59
  ## Recent Updates (v6.31.0)
13
60
 
14
61
  ### The header badge self-heals too, and the pop takes its timings from the server
package/package.json CHANGED
@@ -93,7 +93,7 @@
93
93
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
94
94
  },
95
95
  "name": "@visns-studio/visns-components",
96
- "version": "6.31.1",
96
+ "version": "6.32.0",
97
97
  "description": "Various packages to assist in the development of our Custom Applications.",
98
98
  "main": "src/index.js",
99
99
  "files": [
@@ -174,13 +174,16 @@ const MergeEntity = ({ endpoints, config, dataId, onUpdate = () => {} }) => {
174
174
  const [showAdvanced, setShowAdvanced] = useState(false);
175
175
 
176
176
  // Replace placeholders in URLs
177
+ // Explicit values win: `{dataId}` only falls back to the current entity
178
+ // when the caller did not supply one (loadTargetEntity passes the target's
179
+ // id, and must not be given the source again).
177
180
  const processTemplate = (template, data = {}) => {
178
181
  let processed = template;
179
- processed = processed.replace(/\{dataId\}/g, dataId);
180
182
  Object.keys(data).forEach((key) => {
181
183
  const placeholder = new RegExp(`\\{${key}\\}`, 'g');
182
184
  processed = processed.replace(placeholder, data[key]);
183
185
  });
186
+ processed = processed.replace(/\{dataId\}/g, dataId);
184
187
  return processed;
185
188
  };
186
189
 
@@ -48,6 +48,7 @@ import { toast } from 'react-toastify';
48
48
  import imageCompression from 'browser-image-compression';
49
49
  import fetchUtil from '../../utils/fetchUtil';
50
50
  import { resolveFormVariant } from '../../utils/formLayout';
51
+ import { resolveOverviewStyle } from '../../utils/overviewStyle';
51
52
  import {
52
53
  CheckCircle as CircleCheck,
53
54
  Copy,
@@ -290,6 +291,15 @@ function GenericDetail({
290
291
  const actionPlacement = resolveEditPlacement(setting, editPlacement);
291
292
  const drawActionsInHeader = isHeaderPlacement(actionPlacement);
292
293
 
294
+ /**
295
+ * `'plain'` (the 6.15 card) unless this page's config asks for the older
296
+ * boxed fields, or the app set `setDefaultOverviewStyle('boxed')` at
297
+ * bootstrap. Resolved once per render so every section of the overview
298
+ * draws the same way.
299
+ */
300
+ const overviewStyle = resolveOverviewStyle(page?.overviewStyle);
301
+ const overviewIsBoxed = overviewStyle === 'boxed';
302
+
293
303
  /** Fileupload states */
294
304
  const [files, setFiles] = useState([]);
295
305
 
@@ -2921,7 +2931,13 @@ function GenericDetail({
2921
2931
  </div>
2922
2932
  ) : (
2923
2933
  // Show the regular overview when not editing
2924
- <div className={styles.gridtxt}>
2934
+ <div
2935
+ className={
2936
+ overviewIsBoxed
2937
+ ? `${styles.gridtxt} ${styles['gridtxt--boxed']}`
2938
+ : styles.gridtxt
2939
+ }
2940
+ >
2925
2941
  {activeTabConfig.sections.map(
2926
2942
  (section, sectionIndex) => (
2927
2943
  <React.Fragment
@@ -3095,16 +3111,33 @@ function GenericDetail({
3095
3111
  liClass
3096
3112
  }
3097
3113
  >
3114
+ {/* `overviewFieldLabel` strips
3115
+ the trailing colon a lot of
3116
+ configs still carry, because
3117
+ stacked above the value it is
3118
+ noise. Boxed fields read as a
3119
+ run-in "Label: value", so the
3120
+ colon is put back here — from
3121
+ one place, whatever the config
3122
+ wrote. */}
3098
3123
  {item.label && (
3099
- <strong
3100
- className={
3101
- styles.overviewLabel
3102
- }
3103
- >
3104
- {overviewFieldLabel(
3105
- item.label
3106
- )}
3107
- </strong>
3124
+ <>
3125
+ <strong
3126
+ className={
3127
+ styles.overviewLabel
3128
+ }
3129
+ >
3130
+ {overviewFieldLabel(
3131
+ item.label
3132
+ )}
3133
+ {overviewIsBoxed
3134
+ ? ':'
3135
+ : ''}
3136
+ </strong>
3137
+ {overviewIsBoxed
3138
+ ? ' '
3139
+ : null}
3140
+ </>
3108
3141
  )}
3109
3142
  <span
3110
3143
  className={
@@ -473,6 +473,129 @@
473
473
  color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.45));
474
474
  }
475
475
 
476
+ /* ==========================================================================
477
+ The boxed overview — opt-in, and only opt-in.
478
+ --------------------------------------------------------------------------
479
+ The 6.15 rework above is the default and stays the default. But one team
480
+ reads their detail pages as a grid of discrete facts, not as a card of
481
+ pairs, and asked for the pre-6.15 treatment back: every field in its own
482
+ tinted, bordered box with a run-in "Label: value", under a centred tinted
483
+ heading bar. Rather than fork the component, an app calls
484
+ `setDefaultOverviewStyle('boxed')` at bootstrap (or a single page sets
485
+ `page.overviewStyle`), GenericDetail adds `.gridtxt--boxed`, and these
486
+ rules put the 5.26 geometry back.
487
+
488
+ Specificity is doing real work here. The rules being undone are
489
+ `.gridtxt > ul.customer__overview li` above AND the bare `.overviewLabel` /
490
+ `.gridtxt__header` in the 2026 pass at the bottom of the file, which win on
491
+ source order — so every selector below doubles up the class
492
+ (`.gridtxt.gridtxt--boxed …`) or qualifies the target with the modifier,
493
+ and nothing here relies on being later in the file.
494
+
495
+ Token fallbacks are literal for the same reason they are everywhere else in
496
+ this file: a bare var() for a token the host has not declared is invalid at
497
+ computed-value time and the declaration is simply dropped. */
498
+
499
+ /* The heading, back to a centred tinted bar. `display: flex` is kept so a
500
+ section's optional `.gridtxt__meta` still renders; it is centred with the
501
+ title rather than pushed to the far edge, since there is no edge to push to
502
+ once the bar is tinted. */
503
+ .gridtxt.gridtxt--boxed > .gridtxt__header {
504
+ width: 100%;
505
+ display: flex;
506
+ align-items: baseline;
507
+ justify-content: center;
508
+ gap: 0;
509
+ flex-wrap: wrap;
510
+ box-sizing: border-box;
511
+ padding: 10px 20px;
512
+ border-bottom: 0;
513
+ border-radius: var(--br, 5px);
514
+ margin-bottom: 0;
515
+ background: rgba(var(--primary-rgb, 27, 57, 51), 0.05);
516
+ font-size: var(--font-size-md, 0.9rem);
517
+ font-weight: 700;
518
+ letter-spacing: 0;
519
+ text-transform: none;
520
+ text-align: center;
521
+ color: var(--paragraph-color, #221e33);
522
+ }
523
+
524
+ /* The meta sits beside the centred title, not opposite it, so it needs its own
525
+ small gap where the header's `space-between` used to provide one. */
526
+ .gridtxt.gridtxt--boxed > .gridtxt__header > .gridtxt__meta {
527
+ margin-left: var(--spacing-sm, 0.5rem);
528
+ }
529
+
530
+ /* A wrapping row of boxes, not a two-column grid: a full-width field can then
531
+ sit between two half-width ones without leaving a hole in a grid track. */
532
+ .gridtxt.gridtxt--boxed > ul.customer__overview {
533
+ width: 100%;
534
+ display: flex;
535
+ justify-content: flex-start;
536
+ flex-direction: row;
537
+ flex-wrap: wrap;
538
+ gap: 0;
539
+ list-style: none;
540
+ box-sizing: border-box;
541
+ padding: 0.85rem;
542
+ margin: 0;
543
+ }
544
+
545
+ /* The box itself. `display: block` is the whole point — the label and the
546
+ value are inline text in one paragraph, not two stacked rows. */
547
+ .gridtxt.gridtxt--boxed > ul.customer__overview li {
548
+ width: auto;
549
+ flex: 0 0 calc(50% - 10px);
550
+ min-width: 0;
551
+ display: block;
552
+ box-sizing: border-box;
553
+ padding: 0.65rem;
554
+ margin: 3px;
555
+ border: 1px solid rgba(var(--primary-rgb, 27, 57, 51), 0.095);
556
+ border-radius: var(--br, 5px);
557
+ background: rgba(var(--primary-rgb, 27, 57, 51), 0.015);
558
+ color: var(--paragraph-color, #221e33);
559
+ font-size: 0.85rem;
560
+ line-height: 1.45;
561
+ }
562
+
563
+ .gridtxt.gridtxt--boxed > ul.customer__overview .fw-grid-item {
564
+ flex: 0 0 calc(100% - 10px);
565
+ grid-column: auto;
566
+ }
567
+
568
+ /* The accent on a note-shaped field, as it was before 6.15. */
569
+ .gridtxt.gridtxt--boxed > ul.customer__overview .notecolor {
570
+ border: 1px solid var(--secondary-color, #b23c3c);
571
+ }
572
+
573
+ /* Run-in label: ordinary bold text with a colon after it, not the eyebrow the
574
+ stacked layout wears. */
575
+ .gridtxt--boxed .overviewLabel {
576
+ display: inline;
577
+ font-size: 0.85rem;
578
+ font-weight: 700;
579
+ letter-spacing: 0;
580
+ text-transform: none;
581
+ line-height: inherit;
582
+ color: var(--paragraph-color, #221e33);
583
+ }
584
+
585
+ .gridtxt--boxed .overviewValue {
586
+ display: inline;
587
+ font-size: 0.85rem;
588
+ line-height: inherit;
589
+ }
590
+
591
+ /* One column on a phone — half of a narrow screen is not a readable box. */
592
+ @media (max-width: 759px) {
593
+ .gridtxt.gridtxt--boxed > ul.customer__overview li,
594
+ .gridtxt.gridtxt--boxed > ul.customer__overview .fw-grid-item {
595
+ flex: 0 0 calc(100% - 10px);
596
+ }
597
+ }
598
+
476
599
  .progress {
477
600
  width: 100%;
478
601
  display: block;
@@ -881,6 +881,16 @@
881
881
  position: static;
882
882
  width: max-content;
883
883
  flex: 0 0 auto;
884
+ /* Each `tableInfo` heading and the "N total" count is its own span.
885
+ Until 6.15.0 they were spaced by their own text — "[0 Today] " —
886
+ and when the brackets went, so did the only space between them,
887
+ so "0 Today0 Follow Ups79 total" ran together. The tab-strip and
888
+ embedded rows below already lay the same node out as a flex row;
889
+ the title bar now does too. */
890
+ display: flex;
891
+ align-items: baseline;
892
+ flex-wrap: wrap;
893
+ gap: 0.75rem;
884
894
  }
885
895
 
886
896
  .titleInfo span {
package/src/index.js CHANGED
@@ -22,6 +22,14 @@ import {
22
22
  getDefaultFormLayout,
23
23
  resolveFormVariant,
24
24
  } from './utils/formLayout';
25
+ // Project-wide default for how a detail page's overview fields are drawn — the
26
+ // 6.15 card unless an app calls setDefaultOverviewStyle('boxed') at bootstrap.
27
+ // A page config's own `page.overviewStyle` still wins.
28
+ import {
29
+ setDefaultOverviewStyle,
30
+ getDefaultOverviewStyle,
31
+ resolveOverviewStyle,
32
+ } from './utils/overviewStyle';
25
33
 
26
34
  /** CRM Components */
27
35
  import AsyncSelect from './components/AsyncSelect';
@@ -550,6 +558,7 @@ export {
550
558
  GenericReport,
551
559
  GenericSort,
552
560
  getDefaultFormLayout,
561
+ getDefaultOverviewStyle,
553
562
  GroupedReportRenderer,
554
563
  hasChallengeState,
555
564
  ImpersonateGate,
@@ -598,6 +607,7 @@ export {
598
607
  resolveEditPlacement,
599
608
  isHeaderPlacement,
600
609
  resolveFormVariant,
610
+ resolveOverviewStyle,
601
611
  // Passkeys (WebAuthn). Exported so a consuming app can build its own
602
612
  // enrolment screen against the same marshalling the login screen uses.
603
613
  base64UrlToBytes,
@@ -612,6 +622,7 @@ export {
612
622
  SelectList,
613
623
  Select,
614
624
  setDefaultFormLayout,
625
+ setDefaultOverviewStyle,
615
626
  showConfirmDialog,
616
627
  SortableList,
617
628
  StagePopupModal,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Project-wide default for how a detail page's overview fields are drawn.
3
+ *
4
+ * GenericDetail's overview tab can draw a section either as the 6.15 card
5
+ * (`'plain'` — one panel, fields as stacked label-above-value pairs on a
6
+ * two-column grid, a left-aligned heading on a hairline) or as the pre-6.15
7
+ * boxes (`'boxed'` — every field in its own tinted, bordered box with the
8
+ * label inline as "Label: value", under a centred tinted heading bar). The
9
+ * library ships with `'plain'` as its default so existing projects keep the
10
+ * presentation they already have. An app that wants the older, boxier reading
11
+ * calls `setDefaultOverviewStyle('boxed')` once at bootstrap (before the first
12
+ * render) and every GenericDetail overview follows.
13
+ *
14
+ * An individual page config still wins in both directions: set
15
+ * `"page": { "overviewStyle": "boxed" }` — or `'plain'` — on the view and that
16
+ * page ignores the project default.
17
+ */
18
+
19
+ /** The two presentations the overview understands. */
20
+ const VALID_STYLES = ['plain', 'boxed'];
21
+
22
+ /** Library default — the 6.15 card, unless a project opts out. */
23
+ let defaultOverviewStyle = 'plain';
24
+
25
+ /** Warn at most once so a bad bootstrap call is not a console flood. */
26
+ let warnedInvalidStyle = false;
27
+
28
+ /**
29
+ * Set the project-wide default presentation for overview fields.
30
+ * @param {'plain'|'boxed'} style - Presentation to use when a page config does
31
+ * not specify its own `page.overviewStyle`.
32
+ * @returns {void} Anything other than 'plain' or 'boxed' is ignored.
33
+ */
34
+ export const setDefaultOverviewStyle = (style) => {
35
+ if (!VALID_STYLES.includes(style)) {
36
+ if (!warnedInvalidStyle) {
37
+ warnedInvalidStyle = true;
38
+ // eslint-disable-next-line no-console
39
+ console.warn(
40
+ `setDefaultOverviewStyle: expected 'plain' or 'boxed', received ${JSON.stringify(style)}. Keeping '${defaultOverviewStyle}'.`
41
+ );
42
+ }
43
+ return;
44
+ }
45
+
46
+ defaultOverviewStyle = style;
47
+ };
48
+
49
+ /**
50
+ * Read the current project-wide default.
51
+ * @returns {'plain'|'boxed'} The default presentation for overview fields.
52
+ */
53
+ export const getDefaultOverviewStyle = () => defaultOverviewStyle;
54
+
55
+ /**
56
+ * Resolve the overview presentation for one detail page.
57
+ * @param {string} [style] - The page config's own `page.overviewStyle`, if it
58
+ * has one.
59
+ * @returns {'plain'|'boxed'} The page's own style when it is valid, otherwise
60
+ * the project-wide default.
61
+ */
62
+ export const resolveOverviewStyle = (style) =>
63
+ (VALID_STYLES.includes(style) ? style : defaultOverviewStyle);