@visns-studio/visns-components 6.4.3 → 6.5.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/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.4.3",
94
+ "version": "6.5.0",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -39,6 +39,7 @@ import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
39
39
  import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
40
40
  import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
41
41
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
42
+ import NativeDateFilterEditor from './columns/NativeDateFilterEditor';
42
43
  import { toast } from 'react-toastify';
43
44
  import fetchUtil from '../utils/fetchUtil';
44
45
  import { confirmDialog } from './utils/ConfirmDialog';
@@ -3725,7 +3726,21 @@ const DataGrid = forwardRef(
3725
3726
  if (column.filter && column.filter.type) {
3726
3727
  switch (column.filter.type) {
3727
3728
  case 'date':
3728
- filterEditor = DateFilter;
3729
+ // Native `<input type="date">` by default
3730
+ // — the popup calendar is clipped inside a
3731
+ // scrolling grid and its day cells are far
3732
+ // below a comfortable tap target. A column
3733
+ // can opt back into it with
3734
+ // `filter: { datePicker: 'popup' }`, the
3735
+ // same escape hatch Field uses. Both
3736
+ // editors emit the identical value
3737
+ // (`dateFormat`-formatted string, `''`
3738
+ // when cleared), so the posted `where[]`
3739
+ // entry is unchanged either way.
3740
+ filterEditor =
3741
+ column.filter.datePicker === 'popup'
3742
+ ? DateFilter
3743
+ : NativeDateFilterEditor;
3729
3744
  filterEditorProps = (props, { index }) => {
3730
3745
  return {
3731
3746
  dateFormat: 'DD-MM-YYYY',
@@ -45,7 +45,6 @@ import DatePickerPortal from './utils/DatePickerPortal';
45
45
  import {
46
46
  fromNativeInputValue,
47
47
  toNativeInputValue,
48
- useNativeDateInput,
49
48
  } from './utils/nativeDateInput';
50
49
 
51
50
  import 'react-toggle/style.css';
@@ -171,15 +170,6 @@ function Field({
171
170
  const [emailSuggestions, setEmailSuggestions] = useState([]);
172
171
  const [showEmailSuggestions, setShowEmailSuggestions] = useState(false);
173
172
 
174
- /**
175
- * On touch / narrow devices `date` and `datetime` fields swap
176
- * react-datepicker's popup calendar for the platform's own picker — the
177
- * popup is clipped by modals on a phone and its day cells are far below a
178
- * comfortable tap target. Desktop rendering is untouched.
179
- * See utils/nativeDateInput.js for the (identical) value contract.
180
- */
181
- const nativeDateInput = useNativeDateInput();
182
-
183
173
  /**
184
174
  * `colour` field popup picker: dismiss the popover when the user clicks
185
175
  * anywhere outside it or presses Escape. Picking a colour inside the
@@ -3147,16 +3137,22 @@ function Field({
3147
3137
  }
3148
3138
  autoComplete="off"
3149
3139
  />
3150
- {value && (
3151
- <button
3152
- type="button"
3153
- aria-label="Clear date"
3154
- className={styles.nativeDateClear}
3155
- onClick={() => onChangeDate(null, settings.id)}
3156
- >
3157
- <X size={16} />
3158
- </button>
3159
- )}
3140
+ {/* Always mounted: inserting this node while the input is
3141
+ focused makes mobile browsers dismiss the just-opened
3142
+ native picker (the DOM mutated around the focused
3143
+ control). Visibility is CSS-only. */}
3144
+ <button
3145
+ type="button"
3146
+ aria-label="Clear date"
3147
+ aria-hidden={value ? undefined : 'true'}
3148
+ tabIndex={value ? 0 : -1}
3149
+ className={`${styles.nativeDateClear} ${
3150
+ value ? '' : styles.nativeDateClearHidden
3151
+ }`}
3152
+ onClick={() => onChangeDate(null, settings.id)}
3153
+ >
3154
+ <X size={16} />
3155
+ </button>
3160
3156
  </div>
3161
3157
  );
3162
3158
  };
@@ -3165,7 +3161,11 @@ function Field({
3165
3161
  switch (settings.type) {
3166
3162
  case 'date':
3167
3163
  case 'datetime':
3168
- if (nativeDateInput) {
3164
+ // Native on every viewport — the popup calendar clips inside
3165
+ // modals and duplicates what browsers now do well themselves.
3166
+ // A field can opt back into the react-datepicker popup with
3167
+ // `datePicker: 'popup'` in its config.
3168
+ if (settings.datePicker !== 'popup') {
3169
3169
  return renderNativeDateInput(settings.type === 'datetime');
3170
3170
  }
3171
3171
 
@@ -50,6 +50,32 @@ const NAV_ICONS = {
50
50
  users: Users,
51
51
  };
52
52
 
53
+ // The icons an account action can wear, named by a settings entry's `icon`
54
+ // key. Module scope because two surfaces read it now: the header's action
55
+ // chips and — when a project opts in — the sidebar's pinned action rows.
56
+ const SETTING_ICONS = {
57
+ bug: Bug,
58
+ calendar: Calendar,
59
+ draft: FileText,
60
+ image: Image,
61
+ peopleGroup: Users,
62
+ person: User,
63
+ phone: Phone,
64
+ shield: Shield,
65
+ signOut: LogOut,
66
+ settings: Settings,
67
+ search: Search,
68
+ userSearch: UserSearch,
69
+ clipboardCheck: ClipboardCheck,
70
+ };
71
+
72
+ // A rail row carries a visible label. In the header these actions were icon
73
+ // chips with a tooltip, so a settings entry is allowed to have no label of
74
+ // its own — the bell in particular usually doesn't.
75
+ const RAIL_ACTION_LABELS = {
76
+ notifications: 'Notifications',
77
+ };
78
+
53
79
  const SearchComponent = ({
54
80
  navigate,
55
81
  search,
@@ -270,21 +296,7 @@ function Navigation({
270
296
  };
271
297
 
272
298
  const renderSetting = (n) => {
273
- const iconComponents = {
274
- bug: Bug,
275
- calendar: Calendar,
276
- draft: FileText,
277
- image: Image,
278
- peopleGroup: Users,
279
- person: User,
280
- phone: Phone,
281
- shield: Shield,
282
- signOut: LogOut,
283
- settings: Settings,
284
- search: Search,
285
- userSearch: UserSearch,
286
- clipboardCheck: ClipboardCheck,
287
- };
299
+ const iconComponents = SETTING_ICONS;
288
300
 
289
301
  // Check if the setting's URL matches the current page
290
302
  const isActive = n.url && n.url === `/${currentPage}`;
@@ -356,6 +368,93 @@ function Navigation({
356
368
  }
357
369
  };
358
370
 
371
+ /**
372
+ * The same account actions, drawn as rows for the sidebar rail.
373
+ *
374
+ * Opt-in only — see `actionsInSidebar` below. Every action keeps the
375
+ * behaviour it had in the header: the same logout handler, the same
376
+ * links, the same mounted components (notifications, account switcher),
377
+ * so nothing here re-implements a chip. What changes is the shape: a
378
+ * full-width row wearing the rail's own language, icon then label, since
379
+ * a 36px chip in a 216px column reads as a stray button.
380
+ *
381
+ * Components mounted here draw their own trigger and can't be handed a
382
+ * label, so the row publishes one as `--nav-action-label` for their
383
+ * stylesheet to place. See Notification.module.scss / SwitchAccount.
384
+ */
385
+ const renderRailSetting = (n) => {
386
+ const isActive = n.url && n.url === `/${currentPage}`;
387
+ const activeClass = isActive ? styles.active : '';
388
+ const label = n.label || RAIL_ACTION_LABELS[n.id] || '';
389
+ // A CSS string, quoted and escaped, for `content`.
390
+ const labelVar = { '--nav-action-label': JSON.stringify(label) };
391
+ const IconComponent = SETTING_ICONS[n.icon];
392
+
393
+ if (n.id === 'notifications') {
394
+ return (
395
+ <li style={labelVar}>
396
+ <Notification setSystemAuth={setSystemAuth} />
397
+ </li>
398
+ );
399
+ } else if (n.id === 'businessCardOcr') {
400
+ return (
401
+ <li>
402
+ <button
403
+ onClick={() => setShowBusinessCardOcr(true)}
404
+ className={activeClass}
405
+ type="button"
406
+ >
407
+ <CreditCard size={19} strokeWidth={2} />
408
+ <span>{label}</span>
409
+ </button>
410
+ </li>
411
+ );
412
+ } else if (n.id === 'switchAccount') {
413
+ return (
414
+ <li style={labelVar}>
415
+ <SwitchAccount
416
+ setSystemAuth={setSystemAuth}
417
+ setting={n}
418
+ userProfile={userProfile}
419
+ />
420
+ </li>
421
+ );
422
+ } else if (n.id === 'logout') {
423
+ return (
424
+ <li>
425
+ <button
426
+ onClick={handleLogout}
427
+ className={activeClass}
428
+ type="button"
429
+ >
430
+ {IconComponent && (
431
+ <IconComponent size={19} strokeWidth={2} />
432
+ )}
433
+ <span>{label}</span>
434
+ </button>
435
+ </li>
436
+ );
437
+ }
438
+
439
+ return (
440
+ <li>
441
+ <Link
442
+ to={n.url}
443
+ className={`${activeClass}`}
444
+ target={n.target ? n.target : '_self'}
445
+ // The rail is the mobile drawer below 1024px; navigating
446
+ // from it has to close the drawer, as the nav list does.
447
+ onClick={() => setOpen(false)}
448
+ >
449
+ {IconComponent && (
450
+ <IconComponent size={19} strokeWidth={2} />
451
+ )}
452
+ <span>{label}</span>
453
+ </Link>
454
+ </li>
455
+ );
456
+ };
457
+
359
458
  const [, setActiveDropdown] = useState(null); // We only need the setter
360
459
  const navWrapRef = useRef(null);
361
460
 
@@ -704,14 +803,60 @@ function Navigation({
704
803
  };
705
804
  }, [debugIcons]);
706
805
 
806
+ /**
807
+ * `navConfig.actionsPlacement === 'sidebar'` moves the account actions
808
+ * out of the 50px content header and pins them to the bottom of the
809
+ * rail, which is otherwise empty space, so the page gets the whole
810
+ * height of the glass. Every project that publishes nothing — or
811
+ * 'header' — renders exactly what it rendered before.
812
+ */
813
+ const actionsInSidebar =
814
+ (layout === 'simple' || layout === 'cms') &&
815
+ navConfig.actionsPlacement === 'sidebar';
816
+ const searchEnabled = navConfig.search?.enable === true;
817
+ /**
818
+ * With the actions gone the header holds a search field the project may
819
+ * not have enabled and a hamburger that only exists below 1024px. In
820
+ * that case it stays in the markup — the drawer is anchored under it and
821
+ * offsets itself by its 50px — but desktop drops it, and the main
822
+ * container starts at the top of the viewport.
823
+ */
824
+ const headerIsMobileOnly = actionsInSidebar && !searchEnabled;
825
+
826
+ // Built only when a project asks for it, so the default path does no
827
+ // work it then throws away.
828
+ const railActions = !actionsInSidebar ? null : (
829
+ <div
830
+ className={styles['rail-actions']}
831
+ /* Read by the stylesheets of the components mounted in here —
832
+ the notification panel and the account switcher list both
833
+ anchor to a header chip by default, which from a rail at the
834
+ left edge would open off-screen. */
835
+ data-nav-actions="sidebar"
836
+ ref={actionItemsRef}
837
+ >
838
+ <ul>
839
+ {navData.settings.map((setting, settingKey) =>
840
+ setting.hidden !== true && setting.permission === true ? (
841
+ <React.Fragment key={`rail-setting-${settingKey}`}>
842
+ {renderRailSetting(setting)}
843
+ </React.Fragment>
844
+ ) : null
845
+ )}
846
+ </ul>
847
+ </div>
848
+ );
849
+
707
850
  return layout === 'simple' || layout === 'cms' ? (
708
851
  <div className={styles.cwrap}>
709
852
  <aside
710
- className={
711
- isOpen
712
- ? `${styles['aside--cms']} ${styles['aside--cms--open']}`
713
- : `${styles['aside--cms']}`
714
- }
853
+ className={[
854
+ styles['aside--cms'],
855
+ isOpen ? styles['aside--cms--open'] : '',
856
+ actionsInSidebar ? styles['aside--cms--rail'] : '',
857
+ ]
858
+ .filter(Boolean)
859
+ .join(' ')}
715
860
  >
716
861
  <div className={styles.logo}>
717
862
  <Link to="/">
@@ -733,6 +878,7 @@ function Navigation({
733
878
  </ul>
734
879
  </nav>
735
880
  </div>
881
+ {railActions}
736
882
  </aside>
737
883
  {isOpen ? (
738
884
  <button
@@ -742,8 +888,20 @@ function Navigation({
742
888
  onClick={() => setOpen(false)}
743
889
  />
744
890
  ) : null}
745
- <main className={styles.content}>
746
- <div className={styles['content-header']}>
891
+ <main
892
+ className={
893
+ headerIsMobileOnly
894
+ ? `${styles.content} ${styles['content--full']}`
895
+ : styles.content
896
+ }
897
+ >
898
+ <div
899
+ className={
900
+ headerIsMobileOnly
901
+ ? `${styles['content-header']} ${styles['content-header--mobile']}`
902
+ : styles['content-header']
903
+ }
904
+ >
747
905
  {navConfig.search?.enable === true && (
748
906
  <SearchComponent
749
907
  navigate={navigate}
@@ -774,25 +932,27 @@ function Navigation({
774
932
  <span></span>
775
933
  </button>
776
934
  </div>
777
- <div
778
- className={`${styles['hactions-alternate']} ${
779
- debugIcons ? styles['debug-icons'] : ''
780
- }`}
781
- ref={actionItemsRef}
782
- >
783
- <ul>
784
- {navData.settings.map((setting, settingKey) =>
785
- setting.hidden !== true &&
786
- setting.permission === true ? (
787
- <React.Fragment
788
- key={`setting-${settingKey}`}
789
- >
790
- {renderSetting(setting)}
791
- </React.Fragment>
792
- ) : null
793
- )}
794
- </ul>
795
- </div>
935
+ {actionsInSidebar ? null : (
936
+ <div
937
+ className={`${styles['hactions-alternate']} ${
938
+ debugIcons ? styles['debug-icons'] : ''
939
+ }`}
940
+ ref={actionItemsRef}
941
+ >
942
+ <ul>
943
+ {navData.settings.map((setting, settingKey) =>
944
+ setting.hidden !== true &&
945
+ setting.permission === true ? (
946
+ <React.Fragment
947
+ key={`setting-${settingKey}`}
948
+ >
949
+ {renderSetting(setting)}
950
+ </React.Fragment>
951
+ ) : null
952
+ )}
953
+ </ul>
954
+ </div>
955
+ )}
796
956
  </div>
797
957
  {children}
798
958
  </main>