@visns-studio/visns-components 6.17.2 → 6.20.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 +10 -7
- package/src/components/AssociationManager.jsx +344 -177
- package/src/components/AsyncSelect.jsx +2 -2
- package/src/components/Autocomplete.jsx +57 -12
- package/src/components/Breadcrumb.jsx +23 -4
- package/src/components/ClientAssociationManager.jsx +321 -145
- package/src/components/DataGrid.jsx +625 -20
- package/src/components/DropZone.jsx +4 -1
- package/src/components/Form.jsx +233 -6
- package/src/components/MultiSelect.jsx +2 -2
- package/src/components/Navigation.jsx +39 -23
- package/src/components/TableFilter.jsx +172 -12
- package/src/components/UniversalSearch.jsx +390 -0
- package/src/components/auth/Login.jsx +164 -8
- package/src/components/auth/Profile.jsx +209 -22
- package/src/components/auth/authEndpoints.js +4 -0
- package/src/components/auth/webauthn.js +194 -0
- package/src/components/columns/ColumnRenderers.jsx +6 -1
- package/src/components/controls/DataGridSearch.jsx +11 -5
- package/src/components/generic/GenericAuth.jsx +6 -0
- package/src/components/generic/GenericDashboard.jsx +93 -36
- package/src/components/generic/GenericDetail.jsx +146 -61
- package/src/components/generic/GenericIndex.jsx +134 -38
- package/src/components/generic/GenericSettings.jsx +220 -0
- package/src/components/generic/StandardModal.jsx +165 -8
- package/src/components/generic/dashboardChartDefaults.js +230 -0
- package/src/components/generic/shared/indexLayout.js +38 -0
- package/src/components/settings/IntegrationsManager.jsx +474 -0
- package/src/components/sms/SmsLineSettings.jsx +16 -12
- package/src/components/styles/AssociationManager.module.scss +4 -363
- package/src/components/styles/AuditLog.module.scss +6 -6
- package/src/components/styles/AuditLogs.module.scss +3 -3
- package/src/components/styles/Autocomplete.module.scss +7 -7
- package/src/components/styles/BusinessCardOcr.module.scss +6 -1
- package/src/components/styles/CallQueueSettings.module.scss +4 -1
- package/src/components/styles/CameraPlacement.module.scss +19 -19
- package/src/components/styles/CategorizedDropZone.module.scss +6 -1
- package/src/components/styles/ClientAssociationManager.module.scss +5 -289
- package/src/components/styles/ClientAuth.module.scss +1 -1
- package/src/components/styles/DataGrid.module.scss +455 -8
- package/src/components/styles/Field.module.scss +32 -24
- package/src/components/styles/Form.module.scss +123 -3
- package/src/components/styles/GenericDashboard.module.scss +612 -825
- package/src/components/styles/GenericDetail.module.scss +406 -61
- package/src/components/styles/GenericDynamic.module.scss +25 -25
- package/src/components/styles/GenericEditableTable.module.scss +33 -33
- package/src/components/styles/GenericFormBuilder.module.scss +119 -119
- package/src/components/styles/GenericIndex.module.scss +483 -38
- package/src/components/styles/GenericMain.module.scss +6 -3
- package/src/components/styles/GenericQuote.module.scss +399 -81
- package/src/components/styles/GenericReport.module.scss +8 -8
- package/src/components/styles/GenericSettings.module.scss +209 -0
- package/src/components/styles/GenericSort.module.scss +9 -9
- package/src/components/styles/Integrations.module.scss +321 -0
- package/src/components/styles/List.module.scss +4 -4
- package/src/components/styles/Login.module.scss +57 -0
- package/src/components/styles/MultiSelect.module.scss +4 -4
- package/src/components/styles/Navigation.module.scss +12 -12
- package/src/components/styles/Notification.module.scss +12 -8
- package/src/components/styles/NotificationList.module.scss +5 -5
- package/src/components/styles/Profile.module.scss +5 -5
- package/src/components/styles/QrCode.module.scss +4 -4
- package/src/components/styles/QuickAction.module.scss +20 -20
- package/src/components/styles/SortableQuoteItems.module.scss +4 -4
- package/src/components/styles/TableFilter.module.scss +128 -17
- package/src/components/styles/TicketConversation.module.scss +497 -0
- package/src/components/styles/UniversalSearch.module.scss +229 -0
- package/src/components/styles/Vault.module.scss +413 -0
- package/src/components/styles/_associationManager.scss +467 -0
- package/src/components/styles/_authBrandPanel.scss +15 -9
- package/src/components/styles/_controls.scss +43 -39
- package/src/components/styles/global-datagrid.css +196 -0
- package/src/components/styles/global.css +168 -17
- package/src/components/tickets/TicketConversation.jsx +455 -0
- package/src/components/utils/rowActionSettings.js +117 -0
- package/src/components/vault/VaultClientPicker.jsx +183 -0
- package/src/components/vault/VaultEntryForm.jsx +23 -0
- package/src/components/vault/VaultManager.jsx +382 -34
- package/src/components/vault/VaultShareModal.jsx +454 -0
- package/src/components/vault/vaultEndpoints.js +16 -2
- package/src/components/vault/vaultShares.js +247 -0
- package/src/index.js +35 -0
- package/src/utils/fetchUtil.js +38 -14
|
@@ -3,6 +3,35 @@ import React, { useState, useEffect, useRef } from 'react';
|
|
|
3
3
|
import { Link, useNavigate } from 'react-router-dom';
|
|
4
4
|
import { ResponsiveBar } from '@nivo/bar';
|
|
5
5
|
import { ResponsiveLine } from '@nivo/line';
|
|
6
|
+
|
|
7
|
+
// Shared Nivo defaults so every chart on a dashboard reads the same way:
|
|
8
|
+
// room for the axis legends, a recessive grid, and point sizing that thins
|
|
9
|
+
// out as series are added. `widget.props` is spread after these, so any
|
|
10
|
+
// individual widget can still override a default from its JSON config.
|
|
11
|
+
const defaultsFor = (type, seriesCount = 1) => {
|
|
12
|
+
const base = {
|
|
13
|
+
margin: { top: 20, right: 24, bottom: 56, left: 56 },
|
|
14
|
+
colors: { scheme: 'nivo' },
|
|
15
|
+
animate: true,
|
|
16
|
+
theme: {
|
|
17
|
+
axis: { ticks: { text: { fontSize: 11 } } },
|
|
18
|
+
grid: { line: { strokeOpacity: 0.25 } },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (type === 'line') {
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
curve: 'monotoneX',
|
|
26
|
+
enableGridX: false,
|
|
27
|
+
useMesh: true,
|
|
28
|
+
pointSize: seriesCount > 3 ? 4 : 8,
|
|
29
|
+
pointBorderWidth: 2,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return base;
|
|
34
|
+
};
|
|
6
35
|
import { ResponsivePie } from '@nivo/pie';
|
|
7
36
|
import dayjs from 'dayjs';
|
|
8
37
|
import parse from 'html-react-parser';
|
|
@@ -1281,7 +1310,10 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1281
1310
|
widgetData !== null &&
|
|
1282
1311
|
widgetData !== undefined);
|
|
1283
1312
|
|
|
1284
|
-
|
|
1313
|
+
// `list` widgets render their own empty state (`emptyMessage`), so they
|
|
1314
|
+
// must reach the switch even with zero rows — gating them out here made
|
|
1315
|
+
// that message dead code and painted an empty widget instead.
|
|
1316
|
+
if (hasData || widget.type === 'bar' || widget.type === 'list') {
|
|
1285
1317
|
// Always try to render bar charts for debugging
|
|
1286
1318
|
switch (widget.type) {
|
|
1287
1319
|
case 'counter':
|
|
@@ -1622,44 +1654,70 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1622
1654
|
case 'line':
|
|
1623
1655
|
if (widgetData.length > 0) {
|
|
1624
1656
|
return (
|
|
1625
|
-
<
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1657
|
+
<div
|
|
1658
|
+
style={{ height: widget.height || '320px' }}
|
|
1659
|
+
>
|
|
1660
|
+
<ResponsiveLine
|
|
1661
|
+
key={`${widget.id}-${widget.size}`}
|
|
1662
|
+
data={widgetData}
|
|
1663
|
+
{...defaultsFor('line', widgetData.length)}
|
|
1664
|
+
{...widget.props}
|
|
1665
|
+
/>
|
|
1666
|
+
</div>
|
|
1630
1667
|
);
|
|
1631
1668
|
}
|
|
1632
1669
|
break;
|
|
1633
|
-
case 'list':
|
|
1670
|
+
case 'list': {
|
|
1671
|
+
// A `url` supplied by the API row wins; otherwise build the
|
|
1672
|
+
// href from `link.base`/`link.key`. A widget with neither
|
|
1673
|
+
// still renders, it just isn't clickable — a missing `link`
|
|
1674
|
+
// used to throw and blank the entire dashboard.
|
|
1675
|
+
const listRows = Array.isArray(widgetData)
|
|
1676
|
+
? widgetData
|
|
1677
|
+
: [];
|
|
1678
|
+
const rowHref = (item) => {
|
|
1679
|
+
if (item?.url) return item.url;
|
|
1680
|
+
if (widget.link?.base && widget.link?.key) {
|
|
1681
|
+
return `${widget.link.base}/${
|
|
1682
|
+
item[widget.link.key]
|
|
1683
|
+
}`;
|
|
1684
|
+
}
|
|
1685
|
+
return null;
|
|
1686
|
+
};
|
|
1687
|
+
const rowBody = (item) => (
|
|
1688
|
+
<button>
|
|
1689
|
+
<strong>
|
|
1690
|
+
{(widget?.item?.label &&
|
|
1691
|
+
item[widget.item.label]) ||
|
|
1692
|
+
''}
|
|
1693
|
+
</strong>
|
|
1694
|
+
{widget.showDate && <span>{item.date}</span>}
|
|
1695
|
+
</button>
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1634
1698
|
return (
|
|
1635
1699
|
<ul className={styles.dashList}>
|
|
1636
|
-
{
|
|
1637
|
-
|
|
1700
|
+
{listRows.length > 0 ? (
|
|
1701
|
+
listRows
|
|
1638
1702
|
.slice(0, widget.limit || 4)
|
|
1639
|
-
.map((item) =>
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1703
|
+
.map((item, i) => {
|
|
1704
|
+
const href = rowHref(item);
|
|
1705
|
+
return (
|
|
1706
|
+
<li
|
|
1707
|
+
key={`${widget.id}-${
|
|
1708
|
+
item.id ?? i
|
|
1644
1709
|
}`}
|
|
1645
1710
|
>
|
|
1646
|
-
|
|
1647
|
-
<
|
|
1648
|
-
{
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
{widget.showDate && (
|
|
1657
|
-
<span>{item.date}</span>
|
|
1658
|
-
)}
|
|
1659
|
-
</button>
|
|
1660
|
-
</Link>
|
|
1661
|
-
</li>
|
|
1662
|
-
))
|
|
1711
|
+
{href ? (
|
|
1712
|
+
<Link to={href}>
|
|
1713
|
+
{rowBody(item)}
|
|
1714
|
+
</Link>
|
|
1715
|
+
) : (
|
|
1716
|
+
rowBody(item)
|
|
1717
|
+
)}
|
|
1718
|
+
</li>
|
|
1719
|
+
);
|
|
1720
|
+
})
|
|
1663
1721
|
) : (
|
|
1664
1722
|
<li>
|
|
1665
1723
|
<button>
|
|
@@ -1669,6 +1727,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1669
1727
|
)}
|
|
1670
1728
|
</ul>
|
|
1671
1729
|
);
|
|
1730
|
+
}
|
|
1672
1731
|
case 'table': {
|
|
1673
1732
|
const tableData = widgetData.data || widgetData;
|
|
1674
1733
|
const tableKeys = widgetData.keys || [];
|
|
@@ -2050,11 +2109,9 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
2050
2109
|
<div
|
|
2051
2110
|
key={widget.id}
|
|
2052
2111
|
className={`${getGridClass(widget.size)} ${
|
|
2053
|
-
widget.
|
|
2054
|
-
?
|
|
2055
|
-
|
|
2056
|
-
: styles.dashwidget
|
|
2057
|
-
: ''
|
|
2112
|
+
widget.highlight
|
|
2113
|
+
? styles['dashwidget--highlight']
|
|
2114
|
+
: styles.dashwidget
|
|
2058
2115
|
} ${
|
|
2059
2116
|
editMode
|
|
2060
2117
|
? widget.highlight
|
|
@@ -84,7 +84,7 @@ import QrCode from '../QrCode';
|
|
|
84
84
|
import StagePopupModal from './StagePopupModal';
|
|
85
85
|
import StandardModal from './StandardModal';
|
|
86
86
|
import Table from '../DataGrid';
|
|
87
|
-
import TableFilter from '../TableFilter';
|
|
87
|
+
import TableFilter, { isFlatFilterList } from '../TableFilter';
|
|
88
88
|
import CategorizedDropZone from '../CategorizedDropZone';
|
|
89
89
|
import VisnsDropZone from '../DropZone';
|
|
90
90
|
|
|
@@ -238,8 +238,26 @@ function GenericDetail({
|
|
|
238
238
|
urlParam = 'dataId',
|
|
239
239
|
userProfile,
|
|
240
240
|
actions = null,
|
|
241
|
+
/**
|
|
242
|
+
* Host-supplied components for `type: "custom"` tabs, keyed by the tab's
|
|
243
|
+
* `renderer`.
|
|
244
|
+
*
|
|
245
|
+
* Same shape as Navigation's `renderers` slot, deliberately — that is
|
|
246
|
+
* already how VaultQuickSearch mounts into the app chrome, so there is one
|
|
247
|
+
* pattern for "the library lays this out, the app decides what goes in it"
|
|
248
|
+
* rather than two.
|
|
249
|
+
*
|
|
250
|
+
* Every tab type before this had to be a type the library knew about, so a
|
|
251
|
+
* page that wanted to show something the library had no case for (a vault,
|
|
252
|
+
* a map, a live feed) had no way in short of a new `case` here. Each
|
|
253
|
+
* renderer is called with { data, tab, routeParams, reload }.
|
|
254
|
+
*/
|
|
255
|
+
renderers = {},
|
|
241
256
|
}) {
|
|
242
257
|
const gridRef = useRef(null);
|
|
258
|
+
|
|
259
|
+
/** The open form's guarded close — see the note in DataGrid.jsx. */
|
|
260
|
+
const formCloseRef = useRef(null);
|
|
243
261
|
const currentConfigRef = useRef(null);
|
|
244
262
|
const routeParams = useParams();
|
|
245
263
|
|
|
@@ -1720,6 +1738,62 @@ function GenericDetail({
|
|
|
1720
1738
|
}
|
|
1721
1739
|
};
|
|
1722
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* The built-in `type: "custom"` components, addressed by `tab.component`.
|
|
1743
|
+
*
|
|
1744
|
+
* Extracted so the single `case 'custom'` above can serve both this and
|
|
1745
|
+
* the host-supplied `renderers` slot. Previously these lived in a second
|
|
1746
|
+
* `case 'custom'` in the same switch, which JavaScript never reaches.
|
|
1747
|
+
*/
|
|
1748
|
+
const renderCustomComponentTab = (activeTabConfig) => {
|
|
1749
|
+
if (
|
|
1750
|
+
activeTabConfig.component === 'AssociationManager' ||
|
|
1751
|
+
activeTabConfig.component === 'ClientAssociationManager'
|
|
1752
|
+
) {
|
|
1753
|
+
return (
|
|
1754
|
+
<AssociationManager
|
|
1755
|
+
endpoints={activeTabConfig.endpoints}
|
|
1756
|
+
config={activeTabConfig.config}
|
|
1757
|
+
dataId={routeParams[urlParam]}
|
|
1758
|
+
onUpdate={handleReload}
|
|
1759
|
+
/>
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (activeTabConfig.component === 'MergeEntity') {
|
|
1764
|
+
return (
|
|
1765
|
+
<MergeEntity
|
|
1766
|
+
endpoints={activeTabConfig.endpoints}
|
|
1767
|
+
config={activeTabConfig.config}
|
|
1768
|
+
dataId={routeParams[urlParam]}
|
|
1769
|
+
onUpdate={handleReload}
|
|
1770
|
+
/>
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (activeTabConfig.component === 'ProposalTemplateSectionManager') {
|
|
1775
|
+
return (
|
|
1776
|
+
<ProposalTemplateSectionManager
|
|
1777
|
+
{...activeTabConfig.props}
|
|
1778
|
+
templateId={routeParams[urlParam]}
|
|
1779
|
+
onUpdate={handleReload}
|
|
1780
|
+
/>
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (activeTabConfig.component === 'ProposalTemplatePreview') {
|
|
1785
|
+
return (
|
|
1786
|
+
<ProposalTemplatePreview
|
|
1787
|
+
{...activeTabConfig.props}
|
|
1788
|
+
templateId={routeParams[urlParam]}
|
|
1789
|
+
onUpdate={handleReload}
|
|
1790
|
+
/>
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
return null;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1723
1797
|
const renderContent = () => {
|
|
1724
1798
|
if (subnav && subnav.length > 0) {
|
|
1725
1799
|
const activeTab = subnav.find((s) => s.show === true);
|
|
@@ -1730,6 +1804,52 @@ function GenericDetail({
|
|
|
1730
1804
|
|
|
1731
1805
|
if (activeTabConfig && activeTabConfig.type) {
|
|
1732
1806
|
switch (activeTabConfig.type) {
|
|
1807
|
+
case 'custom': {
|
|
1808
|
+
// A tab naming a `component` is the ORIGINAL
|
|
1809
|
+
// built-in form of this case — AssociationManager,
|
|
1810
|
+
// MergeEntity, the proposal panels. Both cases
|
|
1811
|
+
// live in this one switch, so adding a second
|
|
1812
|
+
// `case 'custom'` above made the built-in one
|
|
1813
|
+
// unreachable and silently emptied the Customer
|
|
1814
|
+
// Assignments tab. Hand those straight over.
|
|
1815
|
+
if (activeTabConfig.component) {
|
|
1816
|
+
return renderCustomComponentTab(activeTabConfig);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
const render =
|
|
1820
|
+
renderers[activeTabConfig.renderer];
|
|
1821
|
+
|
|
1822
|
+
if (typeof render !== 'function') {
|
|
1823
|
+
// A named renderer the host did not supply is
|
|
1824
|
+
// a config error, and a blank panel is the one
|
|
1825
|
+
// outcome that makes it invisible. Say which
|
|
1826
|
+
// name is missing.
|
|
1827
|
+
if (
|
|
1828
|
+
process.env.NODE_ENV !== 'production' &&
|
|
1829
|
+
activeTabConfig.renderer
|
|
1830
|
+
) {
|
|
1831
|
+
console.warn(
|
|
1832
|
+
`[GenericDetail] Tab "${activeTabConfig.id}" asks for renderer "${activeTabConfig.renderer}", which was not passed in the \`renderers\` prop.`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// `data` can be null on the first paint, before the
|
|
1840
|
+
// record has loaded. Renderers that key off a
|
|
1841
|
+
// record id must not be mounted with undefined.
|
|
1842
|
+
if (!data) {
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
return render({
|
|
1847
|
+
data,
|
|
1848
|
+
tab: activeTabConfig,
|
|
1849
|
+
routeParams,
|
|
1850
|
+
reload: handleReload,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1733
1853
|
case 'calendar': {
|
|
1734
1854
|
// Prepare events from your data, converting moment objects to Date objects
|
|
1735
1855
|
const events =
|
|
@@ -3577,58 +3697,6 @@ function GenericDetail({
|
|
|
3577
3697
|
userProfile={userProfile}
|
|
3578
3698
|
/>
|
|
3579
3699
|
);
|
|
3580
|
-
case 'custom':
|
|
3581
|
-
// Handle custom components
|
|
3582
|
-
if (
|
|
3583
|
-
activeTabConfig.component ===
|
|
3584
|
-
'AssociationManager' ||
|
|
3585
|
-
activeTabConfig.component ===
|
|
3586
|
-
'ClientAssociationManager'
|
|
3587
|
-
) {
|
|
3588
|
-
return (
|
|
3589
|
-
<AssociationManager
|
|
3590
|
-
endpoints={activeTabConfig.endpoints}
|
|
3591
|
-
config={activeTabConfig.config}
|
|
3592
|
-
dataId={routeParams[urlParam]}
|
|
3593
|
-
onUpdate={handleReload}
|
|
3594
|
-
/>
|
|
3595
|
-
);
|
|
3596
|
-
}
|
|
3597
|
-
if (activeTabConfig.component === 'MergeEntity') {
|
|
3598
|
-
return (
|
|
3599
|
-
<MergeEntity
|
|
3600
|
-
endpoints={activeTabConfig.endpoints}
|
|
3601
|
-
config={activeTabConfig.config}
|
|
3602
|
-
dataId={routeParams[urlParam]}
|
|
3603
|
-
onUpdate={handleReload}
|
|
3604
|
-
/>
|
|
3605
|
-
);
|
|
3606
|
-
}
|
|
3607
|
-
if (
|
|
3608
|
-
activeTabConfig.component ===
|
|
3609
|
-
'ProposalTemplateSectionManager'
|
|
3610
|
-
) {
|
|
3611
|
-
return (
|
|
3612
|
-
<ProposalTemplateSectionManager
|
|
3613
|
-
{...activeTabConfig.props}
|
|
3614
|
-
templateId={routeParams[urlParam]}
|
|
3615
|
-
onUpdate={handleReload}
|
|
3616
|
-
/>
|
|
3617
|
-
);
|
|
3618
|
-
}
|
|
3619
|
-
if (
|
|
3620
|
-
activeTabConfig.component ===
|
|
3621
|
-
'ProposalTemplatePreview'
|
|
3622
|
-
) {
|
|
3623
|
-
return (
|
|
3624
|
-
<ProposalTemplatePreview
|
|
3625
|
-
{...activeTabConfig.props}
|
|
3626
|
-
templateId={routeParams[urlParam]}
|
|
3627
|
-
onUpdate={handleReload}
|
|
3628
|
-
/>
|
|
3629
|
-
);
|
|
3630
|
-
}
|
|
3631
|
-
return null;
|
|
3632
3700
|
case 'proposalSectionManager':
|
|
3633
3701
|
return (
|
|
3634
3702
|
<ProposalTemplateSectionManager
|
|
@@ -3955,15 +4023,27 @@ function GenericDetail({
|
|
|
3955
4023
|
};
|
|
3956
4024
|
|
|
3957
4025
|
/**
|
|
3958
|
-
*
|
|
3959
|
-
*
|
|
4026
|
+
* Detail tabs are a horizontal strip above the content panel.
|
|
4027
|
+
*
|
|
4028
|
+
* They were a vertical rail down the left edge, and this was an opt-in
|
|
4029
|
+
* (`page.tabLayout: "underline"`) that almost nothing opted into. The rail
|
|
4030
|
+
* costs a fifth of the page width on every detail page in every project,
|
|
4031
|
+
* permanently, to show six words — and it is the CONTENT panel beside it,
|
|
4032
|
+
* the thing people opened the page to read, that pays for it. A strip
|
|
4033
|
+
* under the breadcrumb costs one row of height and gives the panel the
|
|
4034
|
+
* whole width back.
|
|
4035
|
+
*
|
|
4036
|
+
* The rail survives for the one shape a single row cannot express: tabs
|
|
4037
|
+
* with expandable children, which are a two-level menu rather than a tab
|
|
4038
|
+
* bar. `isFlatFilterList` is TableFilter's own test, imported rather than
|
|
4039
|
+
* repeated, so the container laid out here always agrees with the markup
|
|
4040
|
+
* TableFilter actually renders into it.
|
|
3960
4041
|
*
|
|
3961
|
-
*
|
|
3962
|
-
*
|
|
3963
|
-
*
|
|
3964
|
-
* every existing page keeps the rail it was designed against.
|
|
4042
|
+
* No opt-out: this is the detail-page layout now, not a mode. `page` keeps
|
|
4043
|
+
* its shape either way, so no config has to change and `tabLayout` in an
|
|
4044
|
+
* existing config is simply redundant.
|
|
3965
4045
|
*/
|
|
3966
|
-
const tabsUnderlined =
|
|
4046
|
+
const tabsUnderlined = isFlatFilterList(filters);
|
|
3967
4047
|
|
|
3968
4048
|
return (
|
|
3969
4049
|
<>
|
|
@@ -4183,12 +4263,17 @@ function GenericDetail({
|
|
|
4183
4263
|
|
|
4184
4264
|
<StandardModal
|
|
4185
4265
|
isOpen={modalShow}
|
|
4186
|
-
onClose={modalClose}
|
|
4266
|
+
onClose={() => (formCloseRef.current ?? modalClose)()}
|
|
4187
4267
|
title={formData.title || 'Edit Details'}
|
|
4188
4268
|
closeOnDocumentClick={false}
|
|
4189
4269
|
verticalAlign={formData.verticalAlign || 'center'}
|
|
4270
|
+
variant={formData.layout === 'modal' ? 'modal' : 'sheet'}
|
|
4271
|
+
size={formData.modalSize || 'medium'}
|
|
4190
4272
|
>
|
|
4191
4273
|
<Form
|
|
4274
|
+
registerClose={(fn) => {
|
|
4275
|
+
formCloseRef.current = fn;
|
|
4276
|
+
}}
|
|
4192
4277
|
closeModal={modalClose}
|
|
4193
4278
|
columnId={formId}
|
|
4194
4279
|
fetchTable={handleReload}
|
|
@@ -13,6 +13,7 @@ import Download from '../Download';
|
|
|
13
13
|
import Form from '../Form';
|
|
14
14
|
import Table from '../DataGrid';
|
|
15
15
|
import TableFilter from '../TableFilter';
|
|
16
|
+
import { shouldUnderlineTabs } from './shared/indexLayout';
|
|
16
17
|
import GenericGrid from './GenericGrid';
|
|
17
18
|
import GenericReportForm from './GenericReportForm';
|
|
18
19
|
import StandardModal from './StandardModal';
|
|
@@ -267,12 +268,40 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
267
268
|
return { config, setConfig, subnav, setSubnav };
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
/**
|
|
272
|
+
* An index page: a title bar, an optional filter rail, and the grid.
|
|
273
|
+
*
|
|
274
|
+
* EMBEDDING. An index is normally the whole route, so it prints its own page
|
|
275
|
+
* title and puts its filters in a rail down the left. Both of those are wrong
|
|
276
|
+
* when the index is one panel inside another page that has already drawn a
|
|
277
|
+
* header and a rail of its own — the settings hub, for one, which stacked a
|
|
278
|
+
* second `<h1>` and a second vertical rail inside a panel that already had
|
|
279
|
+
* both. `showTitle` and `tabLayout` are the two opt-outs, and they are opt-ins:
|
|
280
|
+
* with neither passed this renders exactly what it rendered before, which is
|
|
281
|
+
* what every existing index page relies on.
|
|
282
|
+
*
|
|
283
|
+
* @param {boolean} [showTitle=true] Print the page-title bar (`page.title`
|
|
284
|
+
* plus the record count). `false` suppresses the bar only — the count moves
|
|
285
|
+
* to the tab strip, or to its own quiet row above the grid when there is no
|
|
286
|
+
* strip, because "how many rows am I looking at" is still a question an
|
|
287
|
+
* embedded index has to answer.
|
|
288
|
+
* @param {'rail'|'underline'} [tabLayout='rail'] Where the filter menu goes.
|
|
289
|
+
* `'underline'` asks for TableFilter's horizontal tab strip above the grid
|
|
290
|
+
* instead of the rail beside it, which gives the grid the full width back.
|
|
291
|
+
* Honoured only for a FLAT filter list, and only outside the `simple`
|
|
292
|
+
* layout — see `shouldUnderlineTabs`, which reuses TableFilter's own gate so
|
|
293
|
+
* the container built here can never disagree with the markup TableFilter
|
|
294
|
+
* puts inside it. A rail with expandable parents is a two-level menu with no
|
|
295
|
+
* single row to lay out, and falls back to the rail.
|
|
296
|
+
*/
|
|
270
297
|
function GenericIndex({
|
|
271
298
|
layout = 'full',
|
|
272
299
|
setting,
|
|
273
300
|
setActiveNav = null,
|
|
274
301
|
themeType,
|
|
275
302
|
userProfile,
|
|
303
|
+
showTitle = true,
|
|
304
|
+
tabLayout = 'rail',
|
|
276
305
|
}) {
|
|
277
306
|
const gridRef = useRef(null);
|
|
278
307
|
useParams(); // Using router params if needed in the future
|
|
@@ -739,8 +768,91 @@ function GenericIndex({
|
|
|
739
768
|
[setSubnav, setConfig]
|
|
740
769
|
);
|
|
741
770
|
|
|
771
|
+
/**
|
|
772
|
+
* The record count and any `tableInfo` headings, as one node.
|
|
773
|
+
*
|
|
774
|
+
* It normally lives in the page-title bar. With `showTitle={false}` there
|
|
775
|
+
* is no bar to live in, so the same node is rendered against the grid
|
|
776
|
+
* instead — at the right-hand end of the tab strip when there is one, in
|
|
777
|
+
* its own row above the grid when there is not. Dropping it was not an
|
|
778
|
+
* option: it is the only place the total row count is printed above the
|
|
779
|
+
* fold.
|
|
780
|
+
*/
|
|
781
|
+
const headings = setting.page?.tableInfo?.heading;
|
|
782
|
+
const hasHeadings = headings?.length > 0;
|
|
783
|
+
|
|
784
|
+
// Memoised because in embedded mode this node is a dependency of
|
|
785
|
+
// `renderContent`, and a fresh element every render would rebuild the grid
|
|
786
|
+
// on every render with it.
|
|
787
|
+
const titleInfo = useMemo(
|
|
788
|
+
() => (
|
|
789
|
+
<div className={styles.titleInfo}>
|
|
790
|
+
{hasHeadings &&
|
|
791
|
+
headings.map((h) => (
|
|
792
|
+
<span key={h.id}>
|
|
793
|
+
<strong>{tableInfo[h.id]}</strong> {h.label}
|
|
794
|
+
</span>
|
|
795
|
+
))}
|
|
796
|
+
{/* Square brackets round a count is a convention from a
|
|
797
|
+
spreadsheet, not an interface — and it repeated the page
|
|
798
|
+
title back at you. The count alone is what anyone actually
|
|
799
|
+
reads. */}
|
|
800
|
+
{total > 0 && (
|
|
801
|
+
<span>
|
|
802
|
+
{/* Grouped, matching the pager's own "of 32,134" — the
|
|
803
|
+
same figure was printing two different ways on one
|
|
804
|
+
screen. */}
|
|
805
|
+
<strong>{Number(total).toLocaleString()}</strong> total
|
|
806
|
+
</span>
|
|
807
|
+
)}
|
|
808
|
+
</div>
|
|
809
|
+
),
|
|
810
|
+
[hasHeadings, headings, tableInfo, total]
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// An empty box is worth printing inside the title bar (it holds the bar's
|
|
814
|
+
// right-hand column open) and not worth printing anywhere else.
|
|
815
|
+
const embeddedInfo =
|
|
816
|
+
!showTitle && (total > 0 || hasHeadings) ? titleInfo : null;
|
|
817
|
+
|
|
818
|
+
// Lay the filter menu out as a horizontal strip rather than a rail. The
|
|
819
|
+
// conditions are written out in `shouldUnderlineTabs`, which is where they
|
|
820
|
+
// are also tested.
|
|
821
|
+
const tabsUnderlined = shouldUnderlineTabs({
|
|
822
|
+
tabLayout,
|
|
823
|
+
layout,
|
|
824
|
+
filters: subnav,
|
|
825
|
+
});
|
|
826
|
+
|
|
742
827
|
const renderContent = useCallback(() => {
|
|
743
828
|
if (subnav?.length > 0) {
|
|
829
|
+
if (tabsUnderlined) {
|
|
830
|
+
return (
|
|
831
|
+
<div
|
|
832
|
+
className={`${styles.grid__subrow} ${styles['grid__subrow--stacked']}`}
|
|
833
|
+
>
|
|
834
|
+
<div
|
|
835
|
+
className={`${styles.grid__subnav} ${styles['grid__subnav--underline']}`}
|
|
836
|
+
>
|
|
837
|
+
<TableFilter
|
|
838
|
+
filters={subnav}
|
|
839
|
+
setFilters={setSubnav}
|
|
840
|
+
setSettings={setConfig}
|
|
841
|
+
settings={config}
|
|
842
|
+
type={layout}
|
|
843
|
+
onFilterChange={handleFilterChange}
|
|
844
|
+
tabs={setting.tabs}
|
|
845
|
+
variant="underline"
|
|
846
|
+
/>
|
|
847
|
+
{embeddedInfo}
|
|
848
|
+
</div>
|
|
849
|
+
<div className={styles.grid__subcontent}>
|
|
850
|
+
{renderContentBasedOnType()}
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
744
856
|
return (
|
|
745
857
|
<div className={styles.grid__subrow}>
|
|
746
858
|
{layout === 'simple' ? (
|
|
@@ -801,6 +913,8 @@ function GenericIndex({
|
|
|
801
913
|
renderContentBasedOnType,
|
|
802
914
|
handleFilterChange,
|
|
803
915
|
setting.tabs,
|
|
916
|
+
tabsUnderlined,
|
|
917
|
+
embeddedInfo,
|
|
804
918
|
]);
|
|
805
919
|
|
|
806
920
|
useEffect(() => {
|
|
@@ -912,47 +1026,29 @@ function GenericIndex({
|
|
|
912
1026
|
|
|
913
1027
|
return (
|
|
914
1028
|
<>
|
|
915
|
-
|
|
916
|
-
<div className={styles.
|
|
917
|
-
<div
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
{
|
|
927
|
-
'heading'
|
|
928
|
-
) &&
|
|
929
|
-
setting.page.tableInfo.heading.length > 0 &&
|
|
930
|
-
setting.page.tableInfo.heading.map((h) => (
|
|
931
|
-
<span key={h.id}>
|
|
932
|
-
<strong>{tableInfo[h.id]}</strong>{' '}
|
|
933
|
-
{h.label}
|
|
934
|
-
</span>
|
|
935
|
-
))}
|
|
936
|
-
{/* Square brackets round a count is a convention
|
|
937
|
-
from a spreadsheet, not an interface — and it
|
|
938
|
-
repeated the page title back at you. The count
|
|
939
|
-
alone is what anyone actually reads. */}
|
|
940
|
-
{total > 0 && (
|
|
941
|
-
<span>
|
|
942
|
-
{/* Grouped, matching the pager's own
|
|
943
|
-
"of 32,134" — the same figure was
|
|
944
|
-
printing two different ways on one
|
|
945
|
-
screen. */}
|
|
946
|
-
<strong>
|
|
947
|
-
{Number(total).toLocaleString()}
|
|
948
|
-
</strong>{' '}
|
|
949
|
-
total
|
|
950
|
-
</span>
|
|
951
|
-
)}
|
|
1029
|
+
{showTitle && (
|
|
1030
|
+
<div className={styles.grid}>
|
|
1031
|
+
<div className={styles.grid__row}>
|
|
1032
|
+
<div
|
|
1033
|
+
className={`${styles.grid__full} ${
|
|
1034
|
+
layout === 'full'
|
|
1035
|
+
? styles.crmtitle
|
|
1036
|
+
: styles.crmtitleSimple
|
|
1037
|
+
}`}
|
|
1038
|
+
>
|
|
1039
|
+
<h1>{parse(setting.page?.title || '')}</h1>
|
|
1040
|
+
{titleInfo}
|
|
952
1041
|
</div>
|
|
953
1042
|
</div>
|
|
954
1043
|
</div>
|
|
955
|
-
|
|
1044
|
+
)}
|
|
1045
|
+
|
|
1046
|
+
{/* The count without a title bar and without a strip to sit in.
|
|
1047
|
+
Its own quiet, right-aligned row above the grid — the host page
|
|
1048
|
+
has already drawn the heading this belongs under. */}
|
|
1049
|
+
{!tabsUnderlined && embeddedInfo && (
|
|
1050
|
+
<div className={styles.embeddedInfoRow}>{embeddedInfo}</div>
|
|
1051
|
+
)}
|
|
956
1052
|
|
|
957
1053
|
<div className={styles.grid}>{renderContent()}</div>
|
|
958
1054
|
<Outlet />
|