diff --git a/.gitignore b/.gitignore index 3faf97ab..0b15c178 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,9 @@ test-results.xml dist/* +# Generated FarmControl desktop bundle +/public/desktop/bundle.json +/public/desktop/models/ + # Native macOS vibrancy (built by scripts/build-macos-effects.mjs) src/bun/libMacWindowEffects.dylib \ No newline at end of file diff --git a/assets/icons/chevronlefticon.svg b/assets/icons/chevronlefticon.svg new file mode 100644 index 00000000..d81c9ddd --- /dev/null +++ b/assets/icons/chevronlefticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/chevronrighticon.svg b/assets/icons/chevronrighticon.svg new file mode 100644 index 00000000..c3182117 --- /dev/null +++ b/assets/icons/chevronrighticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/duplicateicon.svg b/assets/icons/duplicateicon.svg new file mode 100644 index 00000000..6eac6aa3 --- /dev/null +++ b/assets/icons/duplicateicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/emailaccounticon.svg b/assets/icons/emailaccounticon.svg new file mode 100644 index 00000000..8019073d --- /dev/null +++ b/assets/icons/emailaccounticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/equalicon.svg b/assets/icons/equalicon.svg new file mode 100644 index 00000000..5f4db9ff --- /dev/null +++ b/assets/icons/equalicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/fulfillmentpolicyicon.svg b/assets/icons/fulfillmentpolicyicon.svg new file mode 100644 index 00000000..70f5993c --- /dev/null +++ b/assets/icons/fulfillmentpolicyicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/greaterthanicon.svg b/assets/icons/greaterthanicon.svg new file mode 100644 index 00000000..5fc932db --- /dev/null +++ b/assets/icons/greaterthanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/greaterthanorequaltoicon.svg b/assets/icons/greaterthanorequaltoicon.svg new file mode 100644 index 00000000..9c494929 --- /dev/null +++ b/assets/icons/greaterthanorequaltoicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/kanbanicon.svg b/assets/icons/kanbanicon.svg new file mode 100644 index 00000000..1900d1e4 --- /dev/null +++ b/assets/icons/kanbanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/lessthanicon.svg b/assets/icons/lessthanicon.svg new file mode 100644 index 00000000..d0ecfd86 --- /dev/null +++ b/assets/icons/lessthanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/lessthanorequaltoicon.svg b/assets/icons/lessthanorequaltoicon.svg new file mode 100644 index 00000000..5094130f --- /dev/null +++ b/assets/icons/lessthanorequaltoicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/loadingicon.svg b/assets/icons/loadingicon.svg new file mode 100644 index 00000000..5836c96e --- /dev/null +++ b/assets/icons/loadingicon.svg @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/assets/icons/magicwandicon.svg b/assets/icons/magicwandicon.svg new file mode 100644 index 00000000..e50833c2 --- /dev/null +++ b/assets/icons/magicwandicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/mailtemplateicon.svg b/assets/icons/mailtemplateicon.svg new file mode 100644 index 00000000..41290340 --- /dev/null +++ b/assets/icons/mailtemplateicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/notequalicon.svg b/assets/icons/notequalicon.svg new file mode 100644 index 00000000..a0ac9dd4 --- /dev/null +++ b/assets/icons/notequalicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/panfilledicon.svg b/assets/icons/panfilledicon.svg new file mode 100644 index 00000000..38617ecd --- /dev/null +++ b/assets/icons/panfilledicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/panicon.svg b/assets/icons/panicon.svg new file mode 100644 index 00000000..ba86dc9e --- /dev/null +++ b/assets/icons/panicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/paymentpolicyicon.svg b/assets/icons/paymentpolicyicon.svg new file mode 100644 index 00000000..f1837f1e --- /dev/null +++ b/assets/icons/paymentpolicyicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/permissionsettingsicon.svg b/assets/icons/permissionsettingsicon.svg new file mode 100644 index 00000000..78bb70fa --- /dev/null +++ b/assets/icons/permissionsettingsicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/persongroupicon.svg b/assets/icons/persongroupicon.svg new file mode 100644 index 00000000..f9f288c1 --- /dev/null +++ b/assets/icons/persongroupicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/returnpolicyicon.svg b/assets/icons/returnpolicyicon.svg new file mode 100644 index 00000000..8fad746a --- /dev/null +++ b/assets/icons/returnpolicyicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/sortcirclefilledicon.svg b/assets/icons/sortcirclefilledicon.svg new file mode 100644 index 00000000..dfe1d1a8 --- /dev/null +++ b/assets/icons/sortcirclefilledicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/sortcircleicon.svg b/assets/icons/sortcircleicon.svg new file mode 100644 index 00000000..6c36d5cc --- /dev/null +++ b/assets/icons/sortcircleicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/stockauditlevelicon.svg b/assets/icons/stockauditlevelicon.svg index 9aa57148..dca83d34 100644 --- a/assets/icons/stockauditlevelicon.svg +++ b/assets/icons/stockauditlevelicon.svg @@ -1,17 +1,16 @@ - - - - - + + + + - + - + diff --git a/assets/icons/timelineicon.svg b/assets/icons/timelineicon.svg new file mode 100644 index 00000000..8c3a5505 --- /dev/null +++ b/assets/icons/timelineicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 4e9ed8a7..833e47f3 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -22,7 +22,7 @@ .ant-form-item-control-input, .ant-input, .ant-input-number .ant-input-number-input, -.ant-segmented-item-label, +.segmented-nav-item-label, .ant-badge-status-text, .ant-tree-title, .ant-select, @@ -37,6 +37,7 @@ .ant-badge, .ant-select-dropdown, .ant-splitter, +.adaptive-splitter, .ant-steps-item-title, .ant-steps-icon, .ant-color-picker-presets-label, @@ -46,8 +47,19 @@ font-family: 'DM Sans'; } +:root { + --color-list-view-tabs-border: #8282822e; + --color-button-border: #8282822e; + --color-button-background: #8282822e; +} + +.ant-dropdown { + z-index: 999; +} + .ant-typography code, -.ant-typography pre { +.ant-typography pre, +.ͼ1 .cm-scroller { font-family: 'DM Mono'; } @@ -74,15 +86,6 @@ vertical-align: top !important; } -.object-display-tag .ant-typography-ellipsis-single-line > code { - background-color: transparent !important; - border: 1px solid transparent !important; - max-width: calc(100%); - padding: 0; - margin: 0; - font-size: 95%; -} - .ant-select-selection-item .country-display { margin-left: 1px !important; } @@ -234,7 +237,7 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card { } .electron-sidebar.ant-menu-inline .ant-menu-item, -.electron-sidebar.ant-menu-inline .ant-menu-submenu-title { +.electron-sidebar.ant-menu-inline.ant-menu-root .ant-menu-submenu-title { padding-left: 20px !important; } @@ -253,8 +256,8 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card { } .electron-sider.ant-layout-sider-collapsed { - max-width: 55px !important; - min-width: 55px !important; + min-width: var(--dashboard-sidebar-collapsed-width, 55px) !important; + max-width: var(--dashboard-sidebar-collapsed-width, 55px) !important; } .loading-modal .ant-modal-footer { @@ -331,12 +334,131 @@ code { padding: 0 !important; } +.iddisplay > *:first-child { + min-width: 0; + flex: 0 1 auto; + max-width: 100%; + overflow: hidden; +} + +.iddisplay .ant-btn { + flex-shrink: 0; +} + +.miscid > *:first-child { + min-width: 0; + flex: 0 1 auto; + max-width: 100%; + overflow: hidden; +} + +.miscid .ant-btn { + flex-shrink: 0; +} + .ant-popover-inner:has(.spotlight-tooltip) { padding: 0 !important; } -.ant-popover-inner:has(.keyboard-shortcut-tooltip) { - padding: 8px !important; +.ant-popover:has(.spotlight-tooltip) { + z-index: 1051; +} + +:root, +.light-mode, +html:has(.light-mode) { + --kbd-color: #363636; + --kbd-bg: #efefef; + --kbd-inset: #e8e8e8; + --kbd-edge: #c3c3c3; + --kbd-ridge: #c9c9c9; + --kbd-drop-shadow: rgb(51 51 51 / 40%); + --kbd-text-shadow: #f6f6f6; +} + +.dark-mode, +html:has(.dark-mode) { + --kbd-color: #ececec; + --kbd-bg: #3a3a3a; + --kbd-inset: #323232; + --kbd-edge: #1c1c1c; + --kbd-ridge: #141414; + --kbd-drop-shadow: rgb(0 0 0 / 45%); + --kbd-text-shadow: #111111; +} + +.keyboard-key { + display: inline-block; + color: var(--kbd-color); + text-decoration: none; + text-align: center; + background: var(--kbd-bg); + padding: 3px 7px; + margin: 5px 3px; + border-radius: 4px; + border: 1px solid color-mix(in srgb, var(--color-text) 10%, transparent); + box-shadow: + inset 0 0 25px var(--kbd-inset), + 0 1px 0 var(--kbd-edge), + 0 3px 0 var(--kbd-ridge), + 0 4px 8px var(--kbd-drop-shadow); + text-shadow: 0 1px 0 var(--kbd-text-shadow); +} + +.keyboard-key-md { + padding: 4.5px 10.5px; + margin: 7.5px 3.5px; + font-size: 110%; + border-radius: 10px; + box-shadow: + inset 0 0 37.5px var(--kbd-inset), + 0 1.5px 0 var(--kbd-edge), + 0 4.5px 0 var(--kbd-ridge), + 0 6px 12px var(--kbd-drop-shadow); + text-shadow: 0 1.5px 0 var(--kbd-text-shadow); +} + +.keyboard-key-lg { + padding: 6px 14px; + margin: 10px 6px; + font-size: 200%; + border-radius: 8px; + box-shadow: + inset 0 0 50px var(--kbd-inset), + 0 2px 0 var(--kbd-edge), + 0 6px 0 var(--kbd-ridge), + 0 8px 16px var(--kbd-drop-shadow); + text-shadow: 0 2px 0 var(--kbd-text-shadow); +} + +.keyboard-key-xl { + padding: 9px 21px; + margin: 15px 9px; + font-size: 300%; + border-radius: 12px; + box-shadow: + inset 0 0 75px var(--kbd-inset), + 0 3px 0 var(--kbd-edge), + 0 9px 0 var(--kbd-ridge), + 0 12px 24px var(--kbd-drop-shadow); + text-shadow: 0 3px 0 var(--kbd-text-shadow); +} + +.cursor-tooltip { + opacity: 0; + transition: opacity 0.25s ease; +} + +.cursor-tooltip.is-visible { + opacity: 1; + transition: opacity 0.15s ease; +} + +.cursor-tooltip .ant-card { + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); } /* --- Start of src/index.css --- */ @@ -353,6 +475,67 @@ body { .ant-spin-blur { filter: blur(3px); } + +.spin { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--color-primary); + font-size: 20px; + line-height: 1; +} + +.spin-nested { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + flex: 1; +} + +.spin-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + flex: 1; + transition: opacity 0.3s; +} + +.spin-content.is-spinning { + filter: blur(3px); + opacity: 0.5; + user-select: none; + pointer-events: none; +} + +.spin-overlay { + position: absolute; + inset: 0; + z-index: 4; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-primary); + font-size: 20px; + pointer-events: none; +} + +.spin.spin-sm, +.spin-nested.spin-sm .spin-overlay { + font-size: 14px; +} + +.spin.spin-lg, +.spin-nested.spin-lg .spin-overlay { + font-size: 32px; +} /* --- End of src/index.css --- */ /* --- Start of src/components/Dashboard/Layout.css --- */ @@ -385,9 +568,24 @@ body { /* --- Start of src/components/Dashboard/common/DashboardSidebar.css --- */ .ant-layout-sider { - width: 250px; /* Adjust the width as per your preference */ - min-width: 250px; /* Ensure a minimum width to avoid collapsing */ - max-width: 400px; /* Optionally set a maximum width */ + height: 100%; +} + +.dashboard-sider.ant-layout-sider { + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; + flex: 0 0 100% !important; + transition: + all 0.3s cubic-bezier(0.2, 0, 0, 1) 0s, + background 0s; +} + +.dashboard-sider.ant-layout-sider-collapsed { + width: var(--dashboard-sidebar-collapsed-width, 80px) !important; + min-width: var(--dashboard-sidebar-collapsed-width, 80px) !important; + max-width: var(--dashboard-sidebar-collapsed-width, 80px) !important; + flex: 0 0 var(--dashboard-sidebar-collapsed-width, 80px) !important; } /* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */ @@ -402,11 +600,13 @@ body { width: 35%; } -.farmcontrol-splitter > .ant-splitter-bar { +.farmcontrol-splitter > .ant-splitter-bar, +.farmcontrol-splitter > .adaptive-splitter-bar { margin: 8px; } -.farmcontrol-splitter.large > .ant-splitter-bar { +.farmcontrol-splitter.large > .ant-splitter-bar, +.farmcontrol-splitter.large > .adaptive-splitter-bar { margin: 11px; opacity: 0.6; } @@ -514,8 +714,32 @@ body { height: 8px !important; } +.scrollbox-inner .simplebar-track.simplebar-vertical { + right: var(--scrollbox-vertical-right-padding); +} + +.scrollbox-inner .simplebar-track.simplebar-horizontal { + bottom: var(--scrollbox-horizontal-bottom-padding); +} + +.scrollbox-inner-small-padding .simplebar-track.simplebar-vertical { + right: 8px; + bottom: 8px; + top: 8px; +} + +.scrollbox-inner-small-padding .simplebar-track.simplebar-horizontal { + bottom: 8px; +} + +.scrollbox-horizontal > [data-simplebar], +.scrollbox-horizontal .simplebar-wrapper { + width: 100%; +} + .simplebar-scrollbar:before { background: #78787854 !important; + transition: opacity 0.3s ease; } .printer-alerts-display-popover .ant-popover-inner { @@ -536,9 +760,11 @@ body { border-radius: 0px !important; } -.ant-select-selection-item .object-display-tag, -.ant-select-tree-title .object-display-tag { +.ant-select-selection-item .ant-tag.object-display-tag, +.ant-select-tree-title .ant-tag.object-display-tag, +.ant-select-dropdown .ant-tag.object-display-tag { background: transparent !important; + border: none; padding: 0; margin-right: 1px !important; } @@ -547,6 +773,11 @@ body { top: 1.5px; } +.object-select .ant-select-selection-item .object-display-tag .iddisplay, +.ant-select-dropdown .object-display-tag .iddisplay { + margin-bottom: -1px; +} + .ant-select-tree-title .object-display-tag { top: 0.5px; } @@ -561,7 +792,11 @@ body { .ant-select-selection-item .object-display-tag .ant-flex { - margin-top: -0.75px; + margin-top: 0; +} + +.ant-select-tree-node-content-wrapper .object-display-tag > .ant-flex { + margin-top: 1px; } .object-info-descriptions table { @@ -592,6 +827,45 @@ body { position: relative; } +.input-number-cal input { + padding: 4px 12px; +} + +.input-number-cal-highlight { + position: absolute; + display: flex; + align-items: center; + overflow: hidden; + white-space: pre; + pointer-events: none; + z-index: 2; + color: color-mix(in srgb, var(--color-text) 85%, transparent); +} + +.input-number-cal-operator { + color: var(--color-purple); +} + +.input-number-cal-bracket { + color: var(--color-primary); +} + +.input-number-cal-expr-input, +.input-number-cal-expr-input.ant-input-affix-wrapper { + position: relative; + z-index: 1; +} + +.input-number-cal-expr-input.ant-input-affix-wrapper .ant-input, +.input-number-cal-expr-input.ant-input-outlined:not(.ant-input-affix-wrapper) { + color: transparent !important; + caret-color: var(--color-text); +} + +.input-number-cal-expr-input.ant-input-affix-wrapper .ant-input { + background: transparent !important; +} + .input-number-cal .ant-input-suffix { margin-right: 28px; } @@ -602,7 +876,7 @@ body { } .input-number-cal .ant-input { - font-family: 'DM Mono'; + font-family: 'DM Sans'; font-weight: 400; } @@ -783,41 +1057,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { } .sidebar-scroll .simplebar-track.simplebar-vertical { - right: -1px; - width: 0px !important; - transition: - width 0.15s ease-in-out, - right 0.15s ease-in-out; -} - -.sidebar-scroll .simplebar-track.simplebar-vertical { - border-radius: 0 0 6.5px 0; -} - -.sidebar-scroll.dark .simplebar-track.simplebar-vertical { - background-color: #141414; - border-inline-end: 1px solid rgba(253, 253, 253, 0.12); - border-bottom: 1px solid rgba(253, 253, 253, 0.12); -} - -.sidebar-scroll.light .simplebar-track.simplebar-vertical { - background-color: #ffffff; - border-inline-end: 1px solid rgba(5, 5, 5, 0.06); - border-bottom: 1px solid rgba(5, 5, 5, 0.06); -} - -.sidebar-scroll.simplebar-scrolling .simplebar-track.simplebar-vertical { - width: 12px !important; - right: -13px; + top: 8px; + right: calc(-1 * var(--scrollbox-vertical-right-padding) - 0.5px); + width: 8px !important; } .sidebar-scroll .simplebar-track.simplebar-vertical .simplebar-scrollbar:before { - top: 4px; - bottom: 4px; - left: 4px; - right: 4px; + top: 2px; + bottom: 2px; + left: 2px; + right: 2px; } .ant-color-picker @@ -926,19 +1177,246 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { margin: 0; } +.code-block-editor { + display: flex; + flex-direction: column; + min-height: 0; +} + +.code-block-editor > div { + flex: 1 1 auto; + min-height: 0; +} + .ant-splitter-panel:not(:first-child) .info-collapse .info-collapse-label { margin-left: 4px; } -.farmcontrol-splitter .ant-splitter-panel { +.farmcontrol-splitter .ant-splitter-panel, +.farmcontrol-splitter .adaptive-splitter-panel { overflow: visible; } -.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel { +.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel, +.farmcontrol-splitter.adaptive-splitter-horizontal .adaptive-splitter-panel { width: 100%; min-width: 0; } +.adaptive-splitter { + display: flex; + width: 100%; + height: 100%; + min-height: 0; + align-items: stretch; + position: relative; +} + +.adaptive-splitter-vertical { + flex-direction: column; +} + +.adaptive-splitter-panel { + min-width: 0; + min-height: 0; +} + +.adaptive-splitter-bar { + flex: none; + position: relative; + z-index: 2; + user-select: none; + touch-action: none; +} + +.adaptive-splitter-horizontal > .adaptive-splitter-bar { + width: 0; +} + +.adaptive-splitter-vertical > .adaptive-splitter-bar { + height: 0; +} + +.adaptive-splitter-bar-dragger { + position: absolute; + z-index: 1; + background: transparent; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger { + top: 0; + bottom: 0; + left: 50%; + width: 6px; + transform: translateX(-50%); + cursor: col-resize; +} + +.adaptive-splitter-vertical + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger { + left: 0; + right: 0; + top: 50%; + height: 6px; + transform: translateY(-50%); + cursor: row-resize; +} + +.adaptive-splitter-bar-dragger::before { + content: ''; + position: absolute; + inset: 0; + margin: auto; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::before { + width: 2px; + height: 100%; + background: var(--ant-color-split, rgba(5, 5, 5, 0.06)); +} + +.adaptive-splitter-vertical + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::before { + height: 2px; + width: 100%; + background: var(--ant-color-split, rgba(5, 5, 5, 0.06)); +} + +.adaptive-splitter-bar-dragger::after { + content: ''; + position: absolute; + inset: 0; + margin: auto; + background: var(--ant-color-text-quaternary, rgba(0, 0, 0, 0.25)); + opacity: 0.35; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::after { + width: 2px; + height: 20px; +} + +.adaptive-splitter-vertical + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::after { + height: 2px; + width: 20px; +} + +.adaptive-splitter-bar.is-active .adaptive-splitter-bar-dragger::before, +.adaptive-splitter-bar-dragger:hover::before { + background: var(--ant-color-primary-border-hover, #4096ff); +} + +.adaptive-splitter-bar-collapse { + position: absolute; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--ant-color-text-secondary, rgba(0, 0, 0, 0.45)); + cursor: pointer; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-collapse { + top: 50%; + transform: translateY(-50%); +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-collapse-start { + left: -18px; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-collapse-end { + right: -18px; +} + +.adaptive-splitter-bar-preview { + position: absolute; + z-index: 3; + pointer-events: none; + background: var(--ant-color-primary, #1677ff); + opacity: 0.45; +} + +.adaptive-splitter-horizontal + > .adaptive-splitter-bar + .adaptive-splitter-bar-preview { + top: 0; + bottom: 0; + width: 2px; +} + +.adaptive-splitter-vertical + > .adaptive-splitter-bar + .adaptive-splitter-bar-preview { + left: 0; + right: 0; + height: 2px; +} + +.adaptive-splitter.is-dragging { + user-select: none; +} + +.adaptive-splitter.is-dragging.adaptive-splitter-horizontal, +.adaptive-splitter.is-dragging.adaptive-splitter-horizontal * { + cursor: col-resize !important; +} + +.adaptive-splitter.is-dragging.adaptive-splitter-vertical, +.adaptive-splitter.is-dragging.adaptive-splitter-vertical * { + cursor: row-resize !important; +} + +.adaptive-splitter-mask { + position: absolute; + inset: 0; + z-index: 10; + pointer-events: none; +} + +.adaptive-splitter.is-ready:not(.is-dragging) > .adaptive-splitter-panel, +.adaptive-splitter.is-size-transitioning > .adaptive-splitter-panel, +.farmcontrol-splitter.is-size-transitioning > .ant-splitter-panel, +.farmcontrol-splitter.is-size-transitioning > .adaptive-splitter-panel { + transition: + flex-basis var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + width var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + min-width var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + max-width var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + height var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + min-height var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1), + max-height var(--adaptive-splitter-size-transition, 0.3s) + cubic-bezier(0.2, 0, 0, 1); +} + .farmcontrol-splitter .ant-splitter-panel:has(~ .ant-splitter-panel) .info-collapse.ant-collapse.ant-collapse-icon-position-end @@ -956,16 +1434,458 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { width: 100%; height: 100%; min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; } -.objectTableCardsContainer .ant-spin-nested-loading { +.object-table-select-cell { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; +} + +.object-table-select-cell .object-table-row-checkbox, +.object-table-select-cell .object-table-row-icon { + position: absolute; + inset: 0; +} + +.object-table-select-cell .object-table-row-checkbox { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} + +.dashboard-table + .ant-table-row:hover + .object-table-select-cell + .object-table-row-checkbox, +.object-table-select-cell.object-table-select-cell-selected + .object-table-row-checkbox { + opacity: 1; + pointer-events: auto; +} + +.dashboard-table + .ant-table-row:hover + .object-table-select-cell + .object-table-row-icon, +.object-table-select-cell.object-table-select-cell-selected + .object-table-row-icon { + transition: opacity 0.2s; + opacity: 0; +} + +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected + > .ant-table-cell, +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected + > .ant-table-cell.ant-table-cell-fix-left, +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected + > .ant-table-cell.ant-table-cell-fix-right { + background-color: color-mix( + in srgb, + var(--color-primary) 5%, + var(--color-button-background) + ); +} + +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected:hover + > .ant-table-cell, +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected + > .ant-table-cell.ant-table-cell-row-hover, +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected:hover + > .ant-table-cell.ant-table-cell-fix-left, +.ant-table-wrapper.dashboard-table + .ant-table-tbody + > tr.object-table-row-selected:hover + > .ant-table-cell.ant-table-cell-fix-right { + background-color: color-mix( + in srgb, + var(--color-primary) 15%, + var(--color-button-background) + ); +} + +.objectKanbanContainerWrapper { + position: relative; width: 100%; height: 100%; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; +} + +.objectKanbanContainerSkeletons { + display: flex; + flex-direction: column; + position: relative; + flex: 1; + min-width: 0; + min-height: 0; +} + +.objectKanbanOverlayColumns { + position: absolute; + inset: 0; + z-index: 4; + overflow: hidden; + pointer-events: none; +} + +.objectKanbanKanbanBody { + position: relative; + flex: 1; + min-width: 0; + min-height: 0; +} + +.objectKanbanContainer { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + z-index: 1; + pointer-events: auto; +} + +.objectKanbanWrap { + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; +} + +.objectKanbanScroll { + flex: 1; + width: 100%; + min-width: 0; + min-height: 0; +} + +.objectKanbanScroll .simplebar-content-wrapper { + height: 100%; } -.objectTableCardsContainer .ant-spin-container { +.objectKanbanScroll .simplebar-content { + min-height: 100%; +} + +.objectKanbanHeader { + flex-shrink: 0; + width: 100%; + min-width: 0; + overflow: hidden; +} + +.objectKanbanHeaderTrack { + width: max-content; + min-width: 100%; + will-change: transform; +} + +.objectKanbanHeaderRow { + width: max-content; + min-width: 100%; + align-items: flex-start; +} + +.objectKanbanHeaderCell { + flex: 0 0 auto; + min-width: 200px; + flex-shrink: 0; + padding: 8px 8px; + border: 1px solid var(--ant-color-border-secondary); + border-bottom: 1px solid var(--color-descriptions-border); + border-radius: 12px 12px 0 0; + background-color: var(--layout-header-bg); + transition: + outline-color 0.15s ease, + opacity 0.15s ease; +} + +.objectKanbanHeaderCell--dragging { + opacity: 0.45; +} + +.objectKanbanHeaderCell--dropTarget { + outline: 2px dashed color-mix(in srgb, var(--color-primary) 55%, transparent); + outline-offset: -2px; +} + +.objectKanbanHeaderCellActions { + flex-shrink: 0; + margin-left: auto; +} + +.objectKanbanHeaderDragHandle { + flex-shrink: 0; +} + +.objectKanban { + width: max-content; + min-width: 100%; + height: 100%; + min-height: 100%; + align-items: stretch; + padding-bottom: 4px; +} + +.objectKanbanSkeletonColumns { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; +} + +.objectKanbanSkeletonTrack, +.objectKanbanOverlayTrack { + width: max-content; + min-width: 100%; + height: 100%; + will-change: transform; +} + +.objectKanbanSkeletonRow, +.objectKanbanOverlayRow { + width: max-content; + min-width: 100%; + height: 100%; + align-items: stretch; +} + +.objectKanbanColumn, +.objectKanbanColumnSkeleton { + position: relative; + flex: 0 0 auto; + min-width: 200px; + min-height: 0; + border-top: none; + border-radius: 0 0 12px 12px; +} + +.objectKanbanColumnResizeOverlay { + position: relative; +} + +.objectKanbanColumnResizeHandle { + position: absolute; + top: 0; + right: -11px; + bottom: 0; + width: 6px; + z-index: 3; + cursor: col-resize; + touch-action: none; + user-select: none; + pointer-events: auto; + opacity: 0.6; +} + +.objectKanbanColumnResizeHandle::before { + content: ''; + position: absolute; + top: 11.15px; + bottom: 10.85px; + right: 1px; + width: 2px; + transform: translateX(-50%); + transition: opacity 0.15s ease; +} + +.objectKanbanColumnResizeHandle::after { + content: ''; + position: absolute; + top: 50%; + right: 1px; + width: 2px; + height: 20px; + transform: translate(-50%, -50%); + transition: opacity 0.15s ease; +} + +.dark-mode .objectKanbanColumnResizeHandle::after { + background: rgba(255, 255, 255, 0.18); +} + +.dark-mode .objectKanbanColumnResizeHandle::before { + background: rgba(255, 255, 255, 0.08); +} + +.light-mode .objectKanbanColumnResizeHandle::after { + background: rgba(0, 0, 0, 0.15); +} + +.light-mode .objectKanbanColumnResizeHandle::before { + background: rgba(0, 0, 0, 0.04); +} + +.dark-mode .objectKanbanColumnResizeHandle:hover::before { + background: #0e3a5b; +} + +.light-mode .objectKanbanColumnResizeHandle:hover::before { + background: #e6f8ff; +} + +.dark-mode .objectKanbanColumnResizeHandle.active::before { + background: #0b4c7e; +} + +.light-mode .objectKanbanColumnResizeHandle.active::before { + background: #a3e3ff; +} + +body.objectKanbanColumnResizing, +body.objectKanbanColumnResizing * { + cursor: col-resize !important; + user-select: none !important; +} + +.objectKanbanColumnSkeleton { + border-radius: 0 0 12px 12px; + overflow: visible; +} + +.objectKanbanColumnCards { + flex: 1; + border-radius: 0 0 12px 12px; + height: 100%; +} + +.objectKanbanColumnSkeleton .objectKanbanColumnCards { + background-color: var(--layout-header-bg); +} + +.objectKanbanColumnCardsInner { + padding: 16px; +} + +.objectTimelineContainer { width: 100%; height: 100%; + min-width: 0; + min-height: 0; +} + +.objectTimelineContainer .simplebar-mask { + border-radius: 12px; +} + +.objectTimelineItemLabelContainer { + border-radius: 3px; + height: 12px; +} + +.objectTimelineRow { + transition: background-color 0.2s ease; +} + +.objectTimelineRowHeaderLabelContainer { + position: sticky; + left: 0; + z-index: 2; + overflow: hidden; +} + +.objectTimelineRowHeaderLabel { + height: 100%; + flex: 0 0 auto; + padding-left: 44px; + background: transparent; + transition: + background 0.2s ease, + box-shadow 0.3s; +} + +.objectTimelineRowHeaderLabel:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + transition: opacity 0.2s ease; + opacity: 1; + background: linear-gradient( + to right, + var(--color-table-header-bg) 0%, + var(--color-table-header-bg) 70%, + transparent calc(100% - 40px) + ); +} + +.objectTimelineShadow .objectTimelineRowHeaderLabel { + background: var(--color-table-header-bg); +} + +.objectTimelineRowLabelContainer { + position: sticky; + left: 0; + z-index: 2; + overflow: hidden; +} + +.objectTimelineRowLabel { + height: 100%; + transition: + background-color 0.2s ease, + box-shadow 0.3s; + background: var(--layout-header-bg, #fff); +} + +.dark-mode .objectTimelineShadow .objectTimelineRowLabel, +.dark-mode .objectTimelineShadow .objectTimelineRowHeaderLabel { + box-shadow: -2px -1px 8px 4px rgba(253, 253, 253, 0.12); +} + +.light-mode .objectTimelineShadow .objectTimelineRowLabel, +.light-mode .objectTimelineShadow .objectTimelineRowHeaderLabel { + box-shadow: -2px -1px 8px 4px rgba(5, 5, 5, 0.06); +} + +.objectTimelineRow:hover, +.objectTimelineRow:hover .objectTimelineRowLabel { + background-color: var(--color-table-header-bg); +} + +.objectTimelineItemLabel { + display: block; + margin-top: -1.5px; + font-size: 10px; + font-weight: 600; +} + +.objectTimelineItemLabel-reference { + font-family: 'DM Mono', monospace; +} + +.objectTimelineItemWithEndDate { + margin-right: 2px; } .electron-body .ant-modal-wrap, @@ -1089,3 +2009,1443 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { margin-inline-start: 0; margin-top: 4px; } + +.simple-date-time-property-filter { + margin: -6px 0; + background-color: transparent; +} + +.simple-date-time-property-filter.ant-tree .ant-tree-treenode { + margin-bottom: 0; + height: 40px; + align-items: center; +} + +.simple-date-time-property-filter.ant-tree .ant-tree-switcher { + align-self: center; +} + +.simple-date-time-property-filter.ant-tree .ant-tree-switcher-leaf-line:after { + height: 0; + top: -5px; +} + +.simple-date-time-property-filter.ant-tree .ant-tree-switcher-leaf-line:before { + top: -16px !important; + bottom: -24px !important; +} + +/* Size FilterSidebar field selects to the longest label (via labelRender sizer). */ +.filter-sidebar-field-select.ant-select { + width: max-content; +} +.filter-sidebar-field-select .ant-select-selector { + width: max-content !important; +} + +.elipsis-text-wrapper.ant-typography { + display: block; + min-width: 0; + max-width: 100%; + width: auto; + overflow-y: hidden; + overflow-x: visible; + flex-grow: 0; + flex-shrink: 1; + flex-basis: auto; +} + +.elipsis-text-wrapper.ant-typography .elipsis-text { + display: flex; + flex-direction: row; + align-items: baseline; + width: 100%; + max-width: 100%; + min-width: 0; + position: relative; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-wrapper.ant-typography code.elipsis-text-measure, +.elipsis-text-wrapper.ant-typography code.elipsis-text-full, +.elipsis-text-wrapper.ant-typography code.elipsis-text-truncated { + margin: 0; + box-sizing: border-box; + padding-inline: 0.5em; + padding-block: 0.1em 0.1em; +} + +.elipsis-text-measure { + position: absolute; + visibility: hidden; + pointer-events: none; + white-space: pre; +} + +.elipsis-text-full { + display: none; + position: absolute; + left: 0; + top: 0; + width: max-content; + max-width: 100%; + min-width: 0; + overflow: hidden; + white-space: pre; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-full { + display: inline-block; +} + +.elipsis-text.is-truncated .elipsis-text-full { + display: none; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-truncated .elipsis-text-dots { + display: none; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-truncated { + visibility: hidden; +} + +.elipsis-text-truncated { + display: flex; + flex: 1 1 0; + align-items: baseline; + min-width: 0; + width: 100%; + max-width: 100%; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-wrapper.ant-typography code.elipsis-text-truncated { + display: flex; +} + +.elipsis-text-start, +.elipsis-text-end { + flex: 1 1 0; + min-width: 0; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-start { + mask-image: linear-gradient( + to right, + #000 0%, + #000 calc(100% - 0.5em), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + #000 0%, + #000 calc(100% - 0.5em), + transparent 100% + ); +} + +.elipsis-text-end { + display: flex; + justify-content: flex-end; + mask-image: linear-gradient( + to left, + #000 0%, + #000 calc(100% - 0.5em), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to left, + #000 0%, + #000 calc(100% - 0.5em), + transparent 100% + ); +} + +.elipsis-text-end-inner { + flex: 0 0 auto; + white-space: pre; +} + +.elipsis-text-dots { + flex: 0 0 auto; + user-select: none; +} + +.code-diff-viewer { + height: 70vh; + border: 1px solid #85858541; +} + +.code-diff-viewer .cm-mergeView { + height: 100%; + overflow: auto; +} + +.cm-editor { + width: fit-content; +} + +.cm-errorLine { + background-color: color-mix(in srgb, var(--color-error) 15%, transparent); +} + +.cm-gutters .cm-errorLine { + color: var(--color-error); +} + +.diff-label-square { + width: 7px; + height: 7px; + background-color: #85858541; + border-radius: 2px; +} + +.permissions-matrix-table .ant-table-tbody .ant-table-cell { + vertical-align: middle; +} + +.permissions-matrix:hover .ant-table-tbody > tr > .ant-table-cell { + box-shadow: inset 0 0 0 100vmax + color-mix( + in srgb, + var(--color-text) + calc( + 4% * + ( + 1 - + min( + 1, + abs( + calc( + var(--permissions-col, 0) - + var(--permissions-hovered-col, -1) + ) + ) + ) + ) + ), + transparent + ); +} + +.permissions-matrix-table .permissions-matrix-group-row > .ant-table-cell { + font-weight: 600; + background-color: color-mix( + in srgb, + var(--color-text) 6%, + var(--layout-header-bg) + ); +} + +.permissions-matrix-table + .permissions-matrix-group-row + > .ant-table-cell.ant-table-cell-row-hover { + background: color-mix( + in srgb, + var(--color-text) 10%, + var(--layout-header-bg) + ); +} + +.permissions-matrix-table .ant-table-row-expand-icon, +.permissions-matrix-table .ant-table-row-indent { + display: none; +} + +.permissions-matrix-expand-button.ant-btn { + width: 22px; + min-width: 22px; + height: 22px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: inherit; +} + +.permissions-matrix-expand-button.ant-btn:hover, +.permissions-matrix-expand-button.ant-btn:focus, +.permissions-matrix-expand-button.ant-btn:focus-visible { + color: inherit; +} + +.permissions-matrix-expand-spacer { + display: inline-block; + width: 18px; + height: 22px; + flex-shrink: 0; +} + +.permissions-matrix-expand-icon { + font-size: 12px !important; + color: inherit; + transition: transform 0.2s ease; +} + +.permissions-matrix-expand-icon-open { + transform: rotate(90deg); +} + +.permissions-matrix-table .ant-table-thead > tr > th { + vertical-align: bottom; + padding-top: 12px; + padding-bottom: 12px; +} + +.permissions-matrix-table .ant-table-thead .ant-table-column-title { + display: flex; + align-items: flex-end; + justify-content: center; + height: 100%; +} + +.permissions-matrix-header { + writing-mode: vertical-rl; + transform: rotate(180deg); + white-space: nowrap; + line-height: 1.2; + display: inline-block; +} + +.permission-checkbox { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + margin: 0; + border: none; + background: transparent; + cursor: pointer; + vertical-align: middle; + flex-shrink: 0; +} + +.permission-checkbox:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.permission-checkbox:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + border-radius: 50%; +} + +.permission-checkbox-face { + position: absolute; + inset: 0; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.permission-checkbox-inherit { + border: 1.5px solid; + background: transparent; + box-sizing: border-box; +} + +.permission-checkbox[data-state='inherit'] .permission-checkbox-inherit, +.permission-checkbox[data-state='allow'] .permission-checkbox-allow, +.permission-checkbox[data-state='deny'] .permission-checkbox-deny, +.permission-checkbox[data-state='mixed'] .permission-checkbox-mixed { + opacity: 1; +} + +.permission-checkbox-square { + width: 7px; + height: 7px; + background-color: #ffffff; + border-radius: 1px; +} + +.permission-checkbox-face .anticon { + display: flex; + line-height: 0; +} + +.permission-checkbox-face .anticon svg path { + stroke: currentColor; + stroke-width: 8px; +} + +.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar, +.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-bar { + margin: 0; +} + +.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel, +.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-panel { + padding: 0; +} + +.dashboard-splitter.farmcontrol-splitter + > .ant-splitter-bar + .ant-splitter-bar-dragger::before, +.dashboard-splitter.farmcontrol-splitter + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::before { + background: transparent; +} + +.dashboard-splitter.farmcontrol-splitter + > .ant-splitter-bar + .ant-splitter-bar-dragger::after, +.dashboard-splitter.farmcontrol-splitter + > .adaptive-splitter-bar + .adaptive-splitter-bar-dragger::after { + content: none; +} + +.farmcontrol-splitter .ant-splitter-bar-dragger { + transition: opacity 0.3s ease; + opacity: 1; +} + +.farmcontrol-splitter.vertical-scroll-visible .ant-splitter-bar-dragger { + opacity: 0; +} + +.object-display-tag + .elipsis-text-wrapper.ant-typography + code.elipsis-text-measure, +.object-display-tag .elipsis-text-wrapper.ant-typography code.elipsis-text-full, +.object-display-tag + .elipsis-text-wrapper.ant-typography + code.elipsis-text-truncated { + background-color: transparent !important; + border: 1px solid transparent !important; + max-width: calc(100%); + padding: 0; + margin: 0; + font-size: 95%; +} + +.ant-tag.object-display-tag, +.object-select-multiple.ant-select-outlined.ant-select-multiple + .ant-select-selection-item { + border: 1px solid color-mix(in srgb, var(--color-text) 5%, transparent); +} + +.code-block-editor .simplebar-content-wrapper { + background-color: var(--layout-modal-bg); +} + +.previewProgressOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 2; + backdrop-filter: blur(8px); + background-color: color-mix( + in srgb, + var(--layout-header-bg) 20%, + transparent + ); + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.2s ease; +} +.previewProgressOverlay.visible { + opacity: 1; + pointer-events: auto; +} +.previewProgressOverlay.hidden { + opacity: 0; + pointer-events: none; +} + +.previewZoomOverlay { + position: absolute; + right: 12px; + bottom: 12px; + z-index: 2; +} + +.previewZoomCard { + background-color: color-mix( + in srgb, + var(--layout-header-bg) 80%, + transparent + ); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.ant-slider .ant-slider-handle.previewZoomSliderHandle::after { + box-shadow: 0 0 0 2px var(--color-primary); + background: var(--color-primary); +} + +.react-pdf__Page { + border: 1px solid #000; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} + +.no-navigation-labels.ant-menu-horizontal .ant-menu-item { + padding: 0 5px; +} + +.overview-drag-handle { + cursor: grab; + display: inline-flex; + align-items: center; + color: var(--color-text); + padding: 0 4px; +} + +.overview-drag-handle svg path { + stroke: currentColor; + stroke-width: 3px; +} + +.overview-drag-handle:active { + cursor: grabbing; +} + +.overview-section-wrapper.overview-dragging, +.overview-flex-columns-item.overview-dragging, +.overview-stat-wrapper.overview-dragging { + opacity: 0.4; +} + +.overview-stat-wrapper.overview-drag-over, +.overview-flex-columns-item.overview-drag-over { + outline: 2px dashed color-mix(in srgb, var(--color-primary) 50%, transparent); + outline-offset: 4px; + border-radius: 12px; +} + +.overview-stats-display { + width: 100%; +} + +.overview-stat-item { + flex: 1 1 var(--stat-min-width, 175px); + min-width: var(--stat-min-width, 175px); +} + +.overview-stat-item .ant-card, +.overview-stat-item .ant-tag { + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.overview-stat-item .ant-tag { + display: inline-flex; +} + +.overview-section-wrapper { + position: relative; +} + +.overview-flex-columns { + position: relative; + box-sizing: border-box; + width: 100%; +} + +.overview-flex-columns.is-editing { + border: 2px dashed color-mix(in srgb, var(--color-text) 25%, transparent); + border-radius: 24px; + padding: 12px; +} + +.overview-flex-columns.is-empty { + min-height: 72px; +} + +.overview-flex-columns-actions.ant-card { + position: absolute; + top: 0; + left: 0px; + transform: translateY(-60%); + z-index: 4; + width: max-content; +} + +.overview-flex-columns-actions.ant-card .ant-card-body { + line-height: 1; +} + +.overview-flex-columns-actions .ant-btn { + height: auto; + width: auto; + padding: 0 2px; +} + +.overview-flex-columns-row { + width: 100%; +} + +.overview-flex-columns.is-vertical .overview-flex-columns-item { + flex: none; + width: 100%; + transition: none; +} + +.overview-flex-columns-empty { + flex: 1; + min-height: 48px; + display: flex; + align-items: center; + justify-content: center; + color: color-mix(in srgb, var(--color-text) 45%, transparent); +} + +.overview-flex-columns-item { + position: relative; + flex: 1 1 0; + min-width: 0; + overflow: visible; + transition: flex-grow 0.1s ease-in-out; +} + +.overview-flex-columns.is-resizing, +.overview-flex-columns.is-resizing * { + cursor: col-resize !important; + user-select: none; +} + +.overview-flex-columns-separator { + position: absolute; + top: 0; + bottom: 0; + left: 100%; + margin-left: calc(var(--ant-margin-lg, 24px) / 2); + transform: translateX(-50%); + width: 16px; + z-index: 3; + display: flex; + justify-content: center; + cursor: col-resize; + touch-action: none; + user-select: none; +} + +.overview-flex-columns-separator-line { + width: 2px; + height: 100%; + box-sizing: border-box; + border-left: 2px solid color-mix(in srgb, var(--color-text) 8%, transparent); + pointer-events: none; +} + +.overview-flex-columns-separator:hover .overview-flex-columns-separator-line, +.overview-flex-columns-separator.is-active + .overview-flex-columns-separator-line { + border-left: none; + background-color: #a3e3ff; +} + +.dark-mode + .overview-flex-columns-separator:hover + .overview-flex-columns-separator-line, +.dark-mode + .overview-flex-columns-separator.is-active + .overview-flex-columns-separator-line { + background-color: #0e3a5b; +} + +.overview-flex-columns-separator-handle { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%); + width: 2px; + height: 20px; + background-color: color-mix(in srgb, var(--color-text) 18%, transparent); + pointer-events: none; + z-index: 4; +} + +.overview-flex-columns-item > .ant-collapse { + width: 100%; + min-width: 0; +} + +.overview-flex-columns.is-drag-over { + outline: 2px dashed color-mix(in srgb, var(--color-primary) 50%, transparent); + outline-offset: 2px; +} + +.overview-section-wrapper.overview-insert-before::before, +.overview-section-wrapper.overview-insert-after::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 0; + border-top: 2px dashed + color-mix(in srgb, var(--color-primary) 50%, transparent); + pointer-events: none; + z-index: 2; +} + +.overview-section-wrapper.overview-insert-before::before { + top: calc(var(--ant-margin-lg, 24px) / -2); + transform: translateY(-50%); +} + +.overview-section-wrapper.overview-insert-after::after { + bottom: calc(var(--ant-margin-lg, 24px) / -2); + transform: translateY(50%); +} + +.overview-stat-wrapper { + cursor: grab; +} + +.overview-stat-wrapper:active { + cursor: grabbing; +} + +.info-collapse-resize-handle { + display: flex; + position: absolute; + bottom: 0; + left: 0; + right: 0; + justify-content: center; + align-items: center; + height: 12px; + margin-top: -2px; + cursor: ns-resize; + touch-action: none; + user-select: none; +} + +.info-collapse-resize-handle::after { + content: ''; + width: 20px; + height: 2px; + border-radius: 1px; + background: color-mix(in srgb, var(--color-text) 50%, transparent); +} + +.file-gallery-list-preview { + position: relative; + width: 100%; + max-height: 600px; + overflow: hidden; +} + +.file-gallery-list-scroll { + width: 100%; +} + +.file-gallery-list-preview .simplebar-track.simplebar-vertical { + display: none; +} + +.file-gallery-list-preview .simplebar-track.simplebar-horizontal { + display: none; +} + +.file-gallery-list-preview .simplebar-content-wrapper { + scroll-snap-type: x mandatory; + scroll-padding-inline: 0; +} + +.file-gallery-list-preview:not(.is-mobile) .simplebar-content-wrapper { + mask-image: linear-gradient( + to right, + transparent 0, + #000 var(--file-gallery-side-padding, 66px), + #000 calc(100% - var(--file-gallery-side-padding, 66px)), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + transparent 0, + #000 var(--file-gallery-side-padding, 66px), + #000 calc(100% - var(--file-gallery-side-padding, 66px)), + transparent 100% + ); +} + +.file-gallery-list-track { + display: flex; + align-items: center; +} + +.file-gallery-list-slide { + box-sizing: border-box; + scroll-snap-align: center; + height: 100%; + min-width: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.file-gallery-list-preview-frame { + flex: 0 0 auto; + max-width: 100%; + max-height: 600px; + overflow: hidden; + border-radius: 8px; +} + +.file-gallery-list-nav { + position: absolute; + top: 50%; + z-index: 2; + transform: translateY(-50%); +} + +.file-gallery-list-nav-left { + left: 16px; +} + +.file-gallery-list-nav-right { + right: 16px; +} + +.file-gallery-footer { + padding: 0 24px; +} + +.file-gallery-list-thumb-container { + height: 64px; + width: 64px; +} +.file-gallery-list-thumb { + position: relative; + flex: 0 0 auto; + cursor: pointer; + border-radius: 5px; + line-height: 0; +} + +.file-gallery-list-thumb.is-active { + outline: 2px solid var(--color-primary); + outline-offset: 0; +} + +.file-gallery-list-thumb-delete.ant-card { + position: absolute; + top: 0; + right: 0; + transform: translate(30%, -60%); + z-index: 4; + width: max-content; +} + +.file-gallery-list-thumb-delete.ant-card .ant-card-body { + line-height: 1; +} + +.file-gallery-list-thumb-delete .ant-btn { + height: auto; + width: auto; + padding: 0 2px; +} + +.file-gallery-list-upload { + flex: 0 0 auto; +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag { + width: 64px; + height: 64px; + padding: 0; + border-radius: 5px; + border: 1px dashed color-mix(in srgb, var(--color-primary) 50%, transparent); + background: repeating-linear-gradient( + -45deg, + color-mix(in srgb, var(--color-primary) 12%, transparent), + color-mix(in srgb, var(--color-primary) 12%, transparent) 6px, + transparent 6px, + transparent 12px + ); +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag + .ant-upload-drag-container { + padding: 0; + display: flex; + align-items: center; + justify-content: center; + height: 64px; +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag:hover, +.file-gallery-list-dropzone.ant-upload.ant-upload-drag.ant-upload-drag-hover { + border-color: var(--color-primary); +} + +.file-gallery-list-dropzone .anticon { + font-size: 18px; + color: var(--color-primary); +} + +.ant-table-wrapper .ant-table-cell, +.ant-table-wrapper .ant-table-thead > tr > th, +.ant-table-wrapper .ant-table-tbody > tr > th, +.ant-table-wrapper .ant-table-tbody > tr > td, +.ant-table-wrapper tfoot > tr > th, +.ant-table-wrapper tfoot > tr > td { + padding: 12px 8px; +} + +/* FilterInput */ +.filter-input-operand-icon { + font-size: 8px; +} + +.filter-input-operand-icon--chevron { + font-size: 11px; +} + +.filter-input-operand-icon--gt { + margin-left: 1px; +} + +.filter-input-operand-icon--lt { + margin-right: 1px; +} + +.filter-input-wildcard, +[data-wildcard] { + color: var(--color-primary); +} + +.filter-input-date-unit, +[data-date-unit] { + opacity: 0.5; +} + +.filter-input-quote, +[data-quote] { + color: color-mix(in srgb, var(--color-text) 40%, transparent); +} + +.filter-input-operand-tag { + margin-inline-end: 0; + margin-right: 0; + height: 18px; + cursor: default; + padding: 0 4px; + margin-top: -2px; + line-height: 1.37; + user-select: none; + pointer-events: none; + flex-shrink: 0; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.filter-input-operand-chip { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin: 0 2px; + user-select: none; + flex-shrink: 0; +} + +.filter-input-property-filter { + display: flex; + flex-direction: column; + max-width: 280px; + margin: 0; + vertical-align: top; + overflow: hidden; + --property-filter-panel-max-height: 220px; +} + +.filter-input-property-filter-quick { + flex-shrink: 0; +} + +.filter-input-property-filter-divider { + margin: 4px 0; + flex-shrink: 0; +} + +.filter-input-property-filter + > .filter-input-property-filter-scroll.scrollbox-inner { + height: auto !important; + max-height: var(--property-filter-panel-max-height); + max-width: 280px; + flex-shrink: 1; + min-height: 0; +} + +.filter-input-property-filter + > .filter-input-property-filter-scroll.scrollbox-inner + .simplebar-wrapper { + max-height: var(--property-filter-panel-max-height); +} + +.filter-input-property-filter .simplebar-content-wrapper { + max-width: none; +} + +.filter-input-property-filter .simplebar-content { + display: inline-block; +} + +.filter-input-property-filter-body { + padding: 12px 26px 12px 16px; + max-width: 280px; + box-sizing: border-box; +} + +.filter-input-property-filter-content { + width: max-content; + max-width: 100%; +} + +.filter-input-affix { + width: 100%; + display: inline-flex; + position: relative; +} + +.filter-input-affix:not(.filter-input-affix--disabled) { + cursor: text; +} + +.filter-input-affix--disabled { + cursor: not-allowed; +} + +.filter-input-affix--elevated { + z-index: 3 !important; +} + +.filter-input-editor::-webkit-scrollbar { + display: none; + height: 0; +} + +.filter-input-editor { + display: block; + padding: 0; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + background: transparent; + flex: auto; + min-width: 0; + width: 100%; + font-size: inherit; + line-height: inherit; + color: inherit; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + word-break: keep-all; + overflow-wrap: normal; + caret-color: var(--ant-color-text, inherit); + cursor: text; +} + +.filter-input-affix--disabled .filter-input-editor { + cursor: not-allowed; +} + +.filter-input-placeholder { + position: absolute; + inset-inline-start: var(--ant-input-padding-inline, 11px); + inset-inline-end: var(--ant-input-padding-inline, 11px); + top: 0; + bottom: 0; + display: flex; + align-items: center; + color: var(--color-text-placeholder, rgba(0, 0, 0, 0.25)); + pointer-events: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + z-index: 1; +} + +.filter-input-affix--has-clear .filter-input-placeholder { + inset-inline-end: calc(var(--ant-input-padding-inline, 11px) + 22px); +} + +.filter-input-popover .ant-popover-inner { + padding: 4px; +} + +.filter-input-popover-anchor { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 0; + pointer-events: none; +} + +.list-view-tabs { + flex: 1; + min-width: 0; +} + +.list-view-tabs .ant-input-sm { + border-radius: 0; +} + +.list-view-tabs-spin { + flex: 1; + min-width: 0; +} + +.list-view-tabs-spin > .ant-spin-container { + display: flex; + flex: 1; + min-width: 0; +} + +.list-view-tabs-segmented-wrap { + flex: 1; + min-width: 0; +} + +.list-view-tabs-segmented-wrap .simplebar-track.simplebar-vertical { + display: none; +} + +.list-view-tabs-segmented-inner { + width: max-content; +} + +.segmented-nav { + display: inline-flex; + width: max-content; + flex-shrink: 0; + padding: 0; +} + +.segmented-nav-group { + position: relative; + display: flex; + align-items: center; + gap: 8px; +} + +.segmented-nav-thumb { + position: absolute; + top: 0; + left: 0; + z-index: 0; + border-radius: 12px; + background: var(--color-button-background); + pointer-events: none; + will-change: transform, width; + transition: + transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + height 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); +} + +.segmented-nav-item { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + border: 1px solid var(--color-list-view-tabs-border); + border-radius: 12px; + background: transparent; + cursor: pointer; + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + box-shadow: none; + outline: none; + transition: + background 0.2s ease, + border 0.2s ease; +} + +.segmented-nav-item-label { + position: relative; + z-index: 1; + display: flex; + align-items: center; + min-height: 30px; + line-height: 30px; + padding-inline: 12px; +} + +.segmented-nav-item-icon { + padding-inline: 8px; + min-width: 32px; +} + +.segmented-nav-item-icon-contents { + position: relative; + z-index: 1; + display: flex; + align-items: center; + min-height: 30px; + line-height: 30px; +} + +.segmented-nav-item:focus-visible { + outline: 2px solid + color-mix(in srgb, var(--color-primary, #1677ff) 45%, transparent); + outline-offset: 2px; +} + +.segmented-nav-item-selected, +.segmented-nav-item:not(.segmented-nav-item-disabled):hover { + background: var(--color-button-background); + border: 1px solid + color-mix( + in srgb, + var(--color-button-border) 40%, + var(--color-button-background) + ); +} + +.segmented-nav-item-disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.segmented-nav-disabled .segmented-nav-item { + cursor: not-allowed; +} + +.list-view-tabs-label { + height: 30px; +} + +.list-view-tabs-label-editing .ant-input { + font-size: inherit; +} + +.list-view-tabs-label-editing { + margin: 0px -8px; + max-width: none; + min-width: 0; +} + +.list-view-tabs-label-editing .list-view-tabs-name-input-wrap { + flex: 0 0 auto; + width: auto; +} + +.list-view-tabs-name-input-wrap { + display: inline-block; + position: relative; + vertical-align: top; + min-width: 0; +} + +.list-view-tabs-name-input-sizer { + visibility: hidden; + white-space: pre; + font-size: inherit; + line-height: inherit; + font-family: inherit; + min-height: 1em; +} + +.list-view-tabs-label-editing .list-view-tabs-name-input { + position: absolute; + inset: 0; + width: 100%; + min-width: 0; + padding-inline: 0; +} + +.list-view-tabs-color-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.list-view-tabs-editing + .segmented-nav-item + .ant-color-picker-trigger.ant-color-picker-sm { + border-radius: 8px; +} + +.list-view-tabs-privacy-button, +.list-view-tabs-options-button { + padding-inline: 4px; + min-width: 20px; +} + +.list-view-tabs-options-dropdown { + display: flex; + flex-direction: column; +} + +.list-view-tabs-options-dropdown-privacy { + margin: 3px 12px 14px 16px; +} + +.segmented-nav-drag-handle { + flex-shrink: 0; +} + +.segmented-nav-item-dragging { + opacity: 0.4; +} + +.segmented-nav-item-drop-before::before, +.segmented-nav-item-drop-after::after { + content: ''; + position: absolute; + top: 4px; + bottom: 4px; + width: 2px; + border-radius: 1px; + background: var(--color-primary, #1677ff); + pointer-events: none; +} + +.segmented-nav-item-drop-before::before { + left: -5px; +} + +.segmented-nav-item-drop-after::after { + right: -5px; +} + +.segmented-nav-reordering + .segmented-nav-item:not(.segmented-nav-item-dragging) { + cursor: default; +} + +.list-view-tabs-add-button { + padding-inline: 4px; + min-width: 20px; +} + +.state-tag-loading svg path { + stroke: currentColor; + stroke-width: 12px; +} + +.printerQueue.ant-steps.ant-steps-vertical + > .ant-steps-item:last-child + .ant-steps-item-content { + min-height: 0; +} + +.mobile-footer-navigation.ant-layout-footer { + flex: 0 0 auto; + width: 100%; + line-height: normal; + padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px)); + background: var(--color-mobile-footer-bg); + color: var(--color-text); + border-top: 1px solid var(--color-mobile-footer-item-border); +} + +.mobile-footer-navigation-tabs.segmented-nav { + display: flex; + width: 100%; +} + +.mobile-footer-navigation .segmented-nav-group { + width: 100%; +} + +.mobile-footer-navigation .segmented-nav-item { + flex: 1 1 0; + min-width: 0; + flex-direction: column; + gap: 10px; + padding: 12px; + border: none; + color: var(--color-text); + transition: + background 0.2s ease, + color 0.2s ease; +} + +.mobile-footer-navigation .segmented-nav-item-icon { + padding-inline: 0; + min-width: 0; +} + +.mobile-footer-navigation .segmented-nav-item-icon-contents { + min-height: 24px; + line-height: 24px; +} + +.mobile-footer-navigation .segmented-nav-item-icon-contents .anticon { + font-size: 24px; +} + +.mobile-footer-navigation .segmented-nav-thumb { + background: var(--color-mobile-footer-item-bg); + border: none; +} + +.mobile-footer-navigation .segmented-nav-item-selected, +.mobile-footer-navigation + .segmented-nav-item:not(.segmented-nav-item-disabled):hover { + background: var(--color-mobile-footer-item-bg); + border: none; +} + +.mobile-footer-navigation .segmented-nav-item-selected { + color: var(--color-primary); +} + +.mobile-footer-navigation .segmented-nav-item-selected .anticon, +.mobile-footer-navigation .segmented-nav-item-selected svg { + color: inherit; +} + +.mobile-footer-navigation .segmented-nav-item-label { + min-height: 0; + line-height: 1.2; + padding-inline: 0; + font-size: 11px; + font-weight: 500; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.main-layout.is-mobile { + overflow: hidden; +} + +.mobile-menu.ant-menu-horizontal { + max-width: 32px; + margin-left: 4px; + margin-right: 12px; +} + +.mobile-menu.ant-menu-horizontal .ant-menu-submenu { + padding: 0; +} diff --git a/bun.lock b/bun.lock index 2797dcef..4a87bd3d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,9 +23,13 @@ "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "6.12.1", + "@codemirror/merge": "^6.12.2", "@codemirror/state": "6.7.1", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "6.39.12", + "@lezer/common": "1.5.0", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.8", "@simplewebauthn/browser": "^13.1.2", "@tanstack/react-query": "^5.90.10", "@tiptap/extension-link": "^3.20.1", @@ -43,27 +47,31 @@ "antd-style": "^3.7.1", "axios": "^1.11.0", "blurhash": "^2.0.5", + "classnames": "^2.5.1", "codemirror": "^6.0.2", "country-list": "^2.4.1", "cross-env": "^10.0.0", "dayjs": "^1.11.18", "dotenv": "^17.2.1", + "echarts": "^6.1.0", + "echarts-for-react": "^3.0.6", "electron-store": "^11.0.2", - "gcode-preview": "^2.18.0", + "gcode-preview": "3.0.0-alpha.6", "json-schema-traverse": "^1.0.0", "keycloak-js": "^26.2.0", "lodash": "^4.17.23", "loglevel": "^1.9.2", "nanoid": "^5.1.14", "online-3d-viewer": "^0.16.0", + "pdfjs-dist": "5.4.296", "prop-types": "^15.8.1", "react": "^19.1.1", "react-country-flag": "^3.1.0", "react-dom": "^19.1.1", "react-markdown": "^10.1.0", + "react-pdf": "^10.5.0", "react-responsive": "^10.0.1", "react-router-dom": "^7.8.2", - "recharts": "^3.8.1", "remark-gfm": "^4.0.1", "simplebar-react": "^3.3.2", "socket.io-client": "*", @@ -234,6 +242,8 @@ "@codemirror/lint": ["@codemirror/lint@6.9.3", "", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.39.12", "crelt": "1.0.6" } }, "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw=="], + "@codemirror/merge": ["@codemirror/merge@6.12.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/highlight": "^1.0.0", "style-mod": "^4.1.0" } }, "sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w=="], + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.39.12", "crelt": "1.0.7" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], "@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "1.0.3" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="], @@ -494,7 +504,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + "@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "1.5.0", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.8" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="], @@ -532,6 +542,30 @@ "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.3", "", {}, "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.100", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.100", "@napi-rs/canvas-darwin-arm64": "0.1.100", "@napi-rs/canvas-darwin-x64": "0.1.100", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", "@napi-rs/canvas-linux-arm64-musl": "0.1.100", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", "@napi-rs/canvas-linux-x64-gnu": "0.1.100", "@napi-rs/canvas-linux-x64-musl": "0.1.100", "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.100", "", { "os": "android", "cpu": "arm64" }, "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.100", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.100", "", { "os": "darwin", "cpu": "x64" }, "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.100", "", { "os": "linux", "cpu": "arm" }, "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.100", "", { "os": "linux", "cpu": "arm64" }, "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.100", "", { "os": "linux", "cpu": "none" }, "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.100", "", { "os": "linux", "cpu": "x64" }, "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.100", "", { "os": "win32", "cpu": "arm64" }, "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -570,8 +604,6 @@ "@rc-component/util": ["@rc-component/util@1.8.1", "", { "dependencies": { "is-mobile": "5.0.0", "react-is": "18.3.1" }, "peerDependencies": { "react": "19.2.4", "react-dom": "19.2.4" } }, "sha512-Ku6BzF0Ov5L9U3ewFJZDQ//iWCR2nIkLBBiYSrhxIVl3PqeUqiYP2W8gNI8qoKAFQKZdYcS0+B6+SQTDtv/erw=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "@standard-schema/utils": "0.3.0", "immer": "11.1.8", "redux": "5.0.1", "redux-thunk": "3.1.0", "reselect": "5.1.1" }, "optionalDependencies": { "react": "19.2.4", "react-redux": "9.3.0" } }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], - "@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], @@ -640,10 +672,6 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "7.29.0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "7.29.0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], @@ -850,24 +878,6 @@ "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "3.1.3" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "3.0.4" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "3.1.1" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "2.1.0" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "1.0.9", "@types/json-schema": "7.0.15" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], @@ -1202,28 +1212,6 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "2.0.3" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "3.1.0" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "3.2.4", "d3-format": "3.1.2", "d3-interpolate": "3.0.1", "d3-time": "3.1.0", "d3-time-format": "4.1.0" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "3.2.4" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "3.1.0" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-data-view": "1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -1238,8 +1226,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], - "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "2.0.2" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], @@ -1304,6 +1290,10 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "echarts": ["echarts@6.1.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.1.0" } }, "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA=="], + + "echarts-for-react": ["echarts-for-react@3.0.6", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "size-sensor": "^1.0.1" }, "peerDependencies": { "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "react": "^15.0.0 || >=16.0.0" } }, "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -1360,8 +1350,6 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "1.2.7", "is-date-object": "1.1.0", "is-symbol": "1.1.1" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="], - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], @@ -1426,8 +1414,6 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "7.0.6", "get-stream": "6.0.1", "human-signals": "2.1.0", "is-stream": "2.0.1", "merge-stream": "2.0.0", "npm-run-path": "4.0.1", "onetime": "5.1.2", "signal-exit": "3.0.7", "strip-final-newline": "2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], @@ -1514,7 +1500,7 @@ "galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="], - "gcode-preview": ["gcode-preview@2.18.0", "", { "dependencies": { "lil-gui": "0.19.2", "three": "0.159.0" } }, "sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A=="], + "gcode-preview": ["gcode-preview@3.0.0-alpha.6", "", { "dependencies": { "lil-gui": "^0.20.0", "three": ">=0.166.0 <0.186.0" } }, "sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ=="], "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], @@ -1610,8 +1596,6 @@ "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], - "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "1.0.1", "resolve-from": "4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1628,8 +1612,6 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "hasown": "2.0.2", "side-channel": "1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1766,7 +1748,7 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lil-gui": ["lil-gui@0.19.2", "", {}, "sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ=="], + "lil-gui": ["lil-gui@0.20.0", "", {}, "sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -1802,6 +1784,10 @@ "magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + "make-cancellable-promise": ["make-cancellable-promise@2.0.0", "", {}, "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw=="], + + "make-event-props": ["make-event-props@2.0.0", "", {}, "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw=="], + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "2.0.1", "entities": "4.5.0", "linkify-it": "5.0.0", "mdurl": "2.0.0", "punycode.js": "2.3.1", "uc.micro": "2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -1852,6 +1838,8 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + "merge-refs": ["merge-refs@2.0.0", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -2058,6 +2046,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pdfjs-dist": ["pdfjs-dist@5.4.296", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.80" } }, "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q=="], + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -2258,7 +2248,7 @@ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "3.0.4", "@types/mdast": "4.0.4", "devlop": "1.1.0", "hast-util-to-jsx-runtime": "2.3.6", "html-url-attributes": "3.0.1", "mdast-util-to-hast": "13.2.1", "remark-parse": "11.0.0", "remark-rehype": "11.1.2", "unified": "11.0.5", "unist-util-visit": "5.1.0", "vfile": "6.0.3" }, "peerDependencies": { "@types/react": "19.2.10", "react": "19.2.4" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "0.0.6", "use-sync-external-store": "1.6.0" }, "optionalDependencies": { "@types/react": "19.2.10", "redux": "5.0.1" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], + "react-pdf": ["react-pdf@10.5.0", "", { "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", "make-cancellable-promise": "^2.0.0", "make-event-props": "^2.0.0", "merge-refs": "^2.0.0", "pdfjs-dist": "5.4.296", "tiny-invariant": "^1.0.0", "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-zGKb6Ph5AYn2OfO4TS8Gg0Go3eW5K/TKy0+0FTm8EwFNbpgbuaWBZoJdYkJt+cRBCkizE9dYvqtMtHRj522Q4w=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], @@ -2276,12 +2266,6 @@ "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "2.12.0", "clsx": "2.1.1", "decimal.js-light": "2.5.1", "es-toolkit": "1.47.1", "eventemitter3": "5.0.4", "immer": "10.2.0", "react-redux": "9.3.0", "reselect": "5.1.1", "tiny-invariant": "1.3.3", "use-sync-external-store": "1.6.0", "victory-vendor": "37.3.6" }, "peerDependencies": { "react": "19.2.4", "react-dom": "19.2.4", "react-is": "18.3.1" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], - - "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "5.0.1" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-abstract": "1.24.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.1", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "which-builtin-type": "1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "1.0.8", "define-properties": "1.2.1", "es-errors": "1.3.0", "get-proto": "1.0.1", "gopd": "1.2.0", "set-function-name": "2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], @@ -2308,8 +2292,6 @@ "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "2.16.1", "path-parse": "1.0.7", "supports-preserve-symlinks-flag": "1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -2420,6 +2402,8 @@ "simplebar-react": ["simplebar-react@3.3.2", "", { "dependencies": { "simplebar-core": "1.3.2" }, "peerDependencies": { "react": "19.2.4" } }, "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A=="], + "size-sensor": ["size-sensor@1.0.3", "", {}, "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -2654,8 +2638,6 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "3.0.3", "unist-util-stringify-position": "4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "3.2.2", "@types/d3-ease": "3.0.2", "@types/d3-interpolate": "3.0.4", "@types/d3-scale": "4.0.9", "@types/d3-shape": "3.1.8", "@types/d3-time": "3.0.4", "@types/d3-timer": "3.0.2", "d3-array": "3.2.4", "d3-ease": "3.0.1", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", "d3-shape": "3.2.0", "d3-time": "3.1.0", "d3-timer": "3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "0.27.2", "fdir": "6.5.0", "picomatch": "4.0.3", "postcss": "8.5.6", "rollup": "4.57.1", "tinyglobby": "0.2.15" }, "optionalDependencies": { "@types/node": "25.2.0", "fsevents": "2.3.3", "jiti": "2.6.1", "terser": "5.46.0" }, "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-eslint": ["vite-plugin-eslint@1.8.1", "", { "dependencies": { "@rollup/pluginutils": "4.2.1", "@types/eslint": "8.56.12", "rollup": "2.79.2" }, "peerDependencies": { "eslint": "9.39.2", "vite": "7.3.1" } }, "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang=="], @@ -2668,6 +2650,8 @@ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], + "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], "webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="], @@ -2726,6 +2710,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zrender": ["zrender@6.1.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@ant-design/colors/@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], @@ -2740,27 +2726,9 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@codemirror/autocomplete/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], + "@codemirror/commands/@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], - "@codemirror/lang-css/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-go/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-html/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-javascript/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-markdown/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-php/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-python/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-sql/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-xml/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@codemirror/lang-yaml/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], + "@codemirror/language/@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], "@codemirror/lint/crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], @@ -2806,40 +2774,12 @@ "@jimp/js-png/pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], - "@lezer/cpp/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/css/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/go/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/highlight/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/html/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/java/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/javascript/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/json/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/lr/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/markdown/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/php/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/python/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/rust/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], - - "@lezer/xml/@lezer/common": ["@lezer/common@1.5.0", "", {}, "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA=="], + "@lezer/yaml/@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@rc-component/util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], - "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@svgr/core/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], @@ -2914,6 +2854,8 @@ "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], + "electrobun/three": ["three@0.165.0", "", {}, "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA=="], "electron-packager/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], @@ -2952,8 +2894,6 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "gcode-preview/three": ["three@0.159.0", "", {}, "sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA=="], - "get-package-info/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "get-source/data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], @@ -3026,8 +2966,6 @@ "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "recharts/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], @@ -3106,6 +3044,8 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "zrender/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], diff --git a/package.json b/package.json index 24442c1b..9b11c850 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ant-design/icons": "^6.1.0", "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@codemirror/commands": "6.10.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", @@ -25,11 +26,14 @@ "@codemirror/lang-sql": "^6.9.1", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.1", + "@codemirror/merge": "^6.12.2", "@codemirror/state": "6.7.1", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "6.39.12", + "@lezer/common": "1.5.0", + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.8", "@simplewebauthn/browser": "^13.1.2", "@tanstack/react-query": "^5.90.10", "@tiptap/extension-link": "^3.20.1", @@ -47,27 +51,31 @@ "antd-style": "^3.7.1", "axios": "^1.11.0", "blurhash": "^2.0.5", + "classnames": "^2.5.1", "codemirror": "^6.0.2", "country-list": "^2.4.1", "cross-env": "^10.0.0", "dayjs": "^1.11.18", "dotenv": "^17.2.1", + "echarts": "^6.1.0", + "echarts-for-react": "^3.0.6", "electron-store": "^11.0.2", - "gcode-preview": "^2.18.0", + "gcode-preview": "3.0.0-alpha.6", "json-schema-traverse": "^1.0.0", "keycloak-js": "^26.2.0", "lodash": "^4.17.23", "loglevel": "^1.9.2", "nanoid": "^5.1.14", "online-3d-viewer": "^0.16.0", + "pdfjs-dist": "5.4.296", "prop-types": "^15.8.1", "react": "^19.1.1", "react-country-flag": "^3.1.0", "react-dom": "^19.1.1", "react-markdown": "^10.1.0", + "react-pdf": "^10.5.0", "react-responsive": "^10.0.1", "react-router-dom": "^7.8.2", - "recharts": "^3.8.1", "remark-gfm": "^4.0.1", "simplebar-react": "^3.3.2", "socket.io-client": "*", @@ -120,8 +128,6 @@ "concurrently": "^9.2.1", "electrobun": "1.18.1", "electron": "^38.7.1", - "jimp": "^1.6.1", - "png-to-ico": "^2.1.8", "electron-builder": "^26.0.12", "electron-packager": "^17.1.2", "eslint": "^9.34.0", @@ -132,6 +138,8 @@ "eslint-plugin-react-refresh": "^0.4.20", "express": "^5.1.0", "globals": "^15.12.0", + "jimp": "^1.6.1", + "png-to-ico": "^2.1.8", "prettier": "^3.6.2", "prettier-eslint": "^16.4.2", "rcedit": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c9e9433..b593e65d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: '@codemirror/language': specifier: 6.12.1 version: 6.12.1 + '@codemirror/merge': + specifier: ^6.12.2 + version: 6.12.2 '@codemirror/state': specifier: 6.7.1 version: 6.7.1 @@ -79,6 +82,15 @@ importers: '@codemirror/view': specifier: 6.39.12 version: 6.39.12 + '@lezer/common': + specifier: 1.5.0 + version: 1.5.0 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 + '@lezer/lr': + specifier: ^1.4.8 + version: 1.4.8 '@simplewebauthn/browser': specifier: ^13.1.2 version: 13.2.2 @@ -130,6 +142,9 @@ importers: blurhash: specifier: ^2.0.5 version: 2.0.5 + classnames: + specifier: ^2.5.1 + version: 2.5.1 codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -145,12 +160,18 @@ importers: dotenv: specifier: ^17.2.1 version: 17.2.3 + echarts: + specifier: ^6.1.0 + version: 6.1.0 + echarts-for-react: + specifier: ^3.0.6 + version: 3.0.6(echarts@6.1.0)(react@19.2.4) electron-store: specifier: ^11.0.2 version: 11.0.2 gcode-preview: - specifier: ^2.18.0 - version: 2.18.0 + specifier: 3.0.0-alpha.6 + version: 3.0.0-alpha.6 json-schema-traverse: specifier: ^1.0.0 version: 1.0.0 @@ -169,6 +190,9 @@ importers: online-3d-viewer: specifier: ^0.16.0 version: 0.16.0 + pdfjs-dist: + specifier: 5.4.296 + version: 5.4.296 prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -184,15 +208,15 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.10)(react@19.2.4) + react-pdf: + specifier: ^10.5.0 + version: 10.5.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-responsive: specifier: ^10.0.1 version: 10.0.1(react@19.2.4) react-router-dom: specifier: ^7.8.2 version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - recharts: - specifier: ^3.8.1 - version: 3.8.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1) remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -599,6 +623,9 @@ packages: '@codemirror/lint@6.9.3': resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==} + '@codemirror/merge@6.12.2': + resolution: {integrity: sha512-V8JvyAPjHbPupqP7BeMcsdsYCbyPij74jxIbaIJDORI+VZzW44zFmon8bF+oxGWvOKhcRmkiUMXd8MxHr3YA2w==} + '@codemirror/search@6.6.0': resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} @@ -1134,79 +1161,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -1382,9 +1397,6 @@ packages: '@lezer/common@1.5.0': resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} - '@lezer/common@1.5.2': - resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@lezer/cpp@1.1.5': resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} @@ -1445,6 +1457,76 @@ packages: '@marijn/find-cluster-break@1.0.3': resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + '@napi-rs/canvas-android-arm64@0.1.100': + resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.100': + resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.100': + resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.100': + resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1534,17 +1616,6 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - '@reduxjs/toolkit@2.12.0': - resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -1598,79 +1669,66 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -1721,12 +1779,6 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -2191,33 +2243,6 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -3017,50 +3042,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} @@ -3112,9 +3093,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -3246,6 +3224,15 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + echarts-for-react@3.0.6: + resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==} + peerDependencies: + echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + react: ^15.0.0 || >=16.0.0 + + echarts@6.1.0: + resolution: {integrity: sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3364,9 +3351,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.47.1: - resolution: {integrity: sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==} - es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -3600,9 +3584,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -3817,8 +3798,8 @@ packages: resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} engines: {node: '>= 12'} - gcode-preview@2.18.0: - resolution: {integrity: sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A==} + gcode-preview@3.0.0-alpha.6: + resolution: {integrity: sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ==} generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} @@ -4027,12 +4008,6 @@ packages: image-q@4.0.0: resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.8: - resolution: {integrity: sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -4062,10 +4037,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -4354,8 +4325,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lil-gui@0.19.2: - resolution: {integrity: sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ==} + lil-gui@0.20.0: + resolution: {integrity: sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4444,6 +4415,12 @@ packages: magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + make-cancellable-promise@2.0.0: + resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} + + make-event-props@2.0.0: + resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==} + make-fetch-happen@14.0.3: resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4536,6 +4513,14 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge-refs@2.0.0: + resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5033,6 +5018,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pdfjs-dist@5.4.296: + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} + engines: {node: '>=20.16.0 || >=22.3.0'} + pe-library@0.4.1: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} @@ -5525,17 +5514,15 @@ packages: '@types/react': '>=18' react: '>=18' - react-redux@9.3.0: - resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + react-pdf@10.5.0: + resolution: {integrity: sha512-zGKb6Ph5AYn2OfO4TS8Gg0Go3eW5K/TKy0+0FTm8EwFNbpgbuaWBZoJdYkJt+cRBCkizE9dYvqtMtHRj522Q4w==} peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - redux: - optional: true react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} @@ -5584,22 +5571,6 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - recharts@3.8.1: - resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -5646,9 +5617,6 @@ packages: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -5892,6 +5860,9 @@ packages: peerDependencies: react: '>=16.8.0' + size-sensor@1.0.3: + resolution: {integrity: sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6163,9 +6134,6 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - three@0.159.0: - resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==} - three@0.165.0: resolution: {integrity: sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==} @@ -6240,6 +6208,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6413,9 +6384,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - vite-plugin-eslint@1.8.1: resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: @@ -6482,6 +6450,9 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -6640,6 +6611,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zrender@6.1.0: + resolution: {integrity: sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6934,7 +6908,7 @@ snapshots: '@codemirror/language': 6.12.1 '@codemirror/state': 6.7.1 '@codemirror/view': 6.39.12 - '@lezer/common': 1.5.2 + '@lezer/common': 1.5.0 '@codemirror/lang-cpp@6.0.3': dependencies: @@ -7052,7 +7026,7 @@ snapshots: dependencies: '@codemirror/state': 6.7.1 '@codemirror/view': 6.39.12 - '@lezer/common': 1.5.2 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.8 style-mod: 4.1.3 @@ -7063,6 +7037,14 @@ snapshots: '@codemirror/view': 6.39.12 crelt: 1.0.6 + '@codemirror/merge@6.12.2': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.39.12 + '@lezer/highlight': 1.2.3 + style-mod: 4.1.3 + '@codemirror/search@6.6.0': dependencies: '@codemirror/state': 6.7.1 @@ -7915,8 +7897,6 @@ snapshots: '@lezer/common@1.5.0': {} - '@lezer/common@1.5.2': {} - '@lezer/cpp@1.1.5': dependencies: '@lezer/common': 1.5.0 @@ -7998,7 +7978,7 @@ snapshots: '@lezer/yaml@1.0.4': dependencies: - '@lezer/common': 1.5.2 + '@lezer/common': 1.5.0 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.8 @@ -8021,6 +8001,54 @@ snapshots: '@marijn/find-cluster-break@1.0.3': {} + '@napi-rs/canvas-android-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas@0.1.100': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.100 + '@napi-rs/canvas-darwin-arm64': 0.1.100 + '@napi-rs/canvas-darwin-x64': 0.1.100 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 + '@napi-rs/canvas-linux-arm64-musl': 0.1.100 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-musl': 0.1.100 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 + '@napi-rs/canvas-win32-x64-msvc': 0.1.100 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8126,18 +8154,6 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.8 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.2.4 - react-redux: 9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1) - '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -8245,10 +8261,6 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@standard-schema/spec@1.1.0': {} - - '@standard-schema/utils@0.3.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -8790,30 +8802,6 @@ snapshots: '@types/node': 22.19.7 '@types/responselike': 1.0.3 - '@types/d3-array@3.2.2': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -9783,44 +9771,6 @@ snapshots: csstype@3.2.3: {} - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-color@3.1.0: {} - - d3-ease@3.0.1: {} - - d3-format@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@3.1.0: {} - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - data-uri-to-buffer@2.0.2: {} data-uri-to-buffer@6.0.2: {} @@ -9861,8 +9811,6 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js-light@2.5.1: {} - decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -10010,6 +9958,18 @@ snapshots: eastasianwidth@0.2.0: {} + echarts-for-react@3.0.6(echarts@6.1.0)(react@19.2.4): + dependencies: + echarts: 6.1.0 + fast-deep-equal: 3.1.3 + react: 19.2.4 + size-sensor: 1.0.3 + + echarts@6.1.0: + dependencies: + tslib: 2.3.0 + zrender: 6.1.0 + ee-first@1.1.1: {} ejs@3.1.10: @@ -10258,8 +10218,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.47.1: {} - es6-error@4.1.1: optional: true @@ -10339,17 +10297,17 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) + eslint-plugin-import: 2.32.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-n: 15.7.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-promise: 6.6.0(eslint@9.39.2(jiti@2.6.1)) eslint-import-resolver-node@0.3.9: dependencies: @@ -10359,22 +10317,22 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-es@4.1.0(eslint@8.57.1): + eslint-plugin-es@4.1.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(eslint@8.57.1): + eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10383,9 +10341,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -10401,12 +10359,12 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-n@15.7.0(eslint@8.57.1): + eslint-plugin-n@15.7.0(eslint@9.39.2(jiti@2.6.1)): dependencies: builtins: 5.1.0 - eslint: 8.57.1 - eslint-plugin-es: 4.1.0(eslint@8.57.1) - eslint-utils: 3.0.0(eslint@8.57.1) + eslint: 9.39.2(jiti@2.6.1) + eslint-plugin-es: 4.1.0(eslint@9.39.2(jiti@2.6.1)) + eslint-utils: 3.0.0(eslint@9.39.2(jiti@2.6.1)) ignore: 5.3.2 is-core-module: 2.16.1 minimatch: 3.1.2 @@ -10423,9 +10381,9 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-promise@6.6.0(eslint@8.57.1): + eslint-plugin-promise@6.6.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): dependencies: @@ -10493,9 +10451,9 @@ snapshots: dependencies: eslint-visitor-keys: 1.3.0 - eslint-utils@3.0.0(eslint@8.57.1): + eslint-utils@3.0.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 8.57.1 + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 2.1.0 eslint-visitor-keys@1.3.0: {} @@ -10624,8 +10582,6 @@ snapshots: etag@1.8.1: {} - eventemitter3@5.0.4: {} - execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -10898,10 +10854,10 @@ snapshots: transitivePeerDependencies: - supports-color - gcode-preview@2.18.0: + gcode-preview@3.0.0-alpha.6: dependencies: - lil-gui: 0.19.2 - three: 0.159.0 + lil-gui: 0.20.0 + three: 0.179.1 generator-function@2.0.1: {} @@ -11165,10 +11121,6 @@ snapshots: dependencies: '@types/node': 16.9.1 - immer@10.2.0: {} - - immer@11.1.8: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -11195,8 +11147,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - internmap@2.0.3: {} - ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -11490,7 +11440,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lil-gui@0.19.2: {} + lil-gui@0.20.0: {} lines-and-columns@1.2.4: {} @@ -11579,6 +11529,10 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 + make-cancellable-promise@2.0.0: {} + + make-event-props@2.0.0: {} + make-fetch-happen@14.0.3: dependencies: '@npmcli/agent': 3.0.0 @@ -11784,6 +11738,10 @@ snapshots: merge-descriptors@2.0.0: {} + merge-refs@2.0.0(@types/react@19.2.10): + optionalDependencies: + '@types/react': 19.2.10 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -12396,6 +12354,10 @@ snapshots: pathe@2.0.3: {} + pdfjs-dist@5.4.296: + optionalDependencies: + '@napi-rs/canvas': 0.1.100 + pe-library@0.4.1: {} pend@1.2.0: {} @@ -13029,14 +12991,20 @@ snapshots: transitivePeerDependencies: - supports-color - react-redux@9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1): + react-pdf@10.5.0(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@types/use-sync-external-store': 0.0.6 + clsx: 2.1.1 + dequal: 2.0.3 + make-cancellable-promise: 2.0.0 + make-event-props: 2.0.0 + merge-refs: 2.0.0(@types/react@19.2.10) + pdfjs-dist: 5.4.296 react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + react-dom: 19.2.4(react@19.2.4) + tiny-invariant: 1.3.3 + warning: 4.0.3 optionalDependencies: '@types/react': 19.2.10 - redux: 5.0.1 react-refresh@0.18.0: {} @@ -13087,32 +13055,6 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - recharts@3.8.1(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.47.1 - eventemitter3: 5.0.4 - immer: 10.2.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-is: 18.3.1 - react-redux: 9.3.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.4) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -13188,8 +13130,6 @@ snapshots: dependencies: pe-library: 0.4.1 - reselect@5.1.1: {} - resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {} @@ -13535,6 +13475,8 @@ snapshots: react: 19.2.4 simplebar-core: 1.3.2 + size-sensor@1.0.3: {} + slash@3.0.0: {} slice-ansi@3.0.0: @@ -13636,10 +13578,10 @@ snapshots: dependencies: eslint: 8.57.1 eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(eslint@8.57.1) - eslint-plugin-n: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: 6.6.0(eslint@8.57.1) + eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-n: 15.7.0(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-promise: 6.6.0(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@8.57.1) standard-engine: 15.1.0 version-guard: 1.1.3 @@ -13868,8 +13810,6 @@ snapshots: text-table@0.2.0: {} - three@0.159.0: {} - three@0.165.0: {} three@0.176.0: {} @@ -13936,6 +13876,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.3.0: {} + tslib@2.8.1: {} tsparticles@3.9.1: @@ -14145,23 +14087,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - vite-plugin-eslint@1.8.1(eslint@9.39.2(jiti@2.6.1))(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(terser@5.46.0)): dependencies: '@rollup/pluginutils': 4.2.1 @@ -14216,6 +14141,10 @@ snapshots: w3c-keyname@2.2.8: {} + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -14378,4 +14307,8 @@ snapshots: zod@3.25.76: {} + zrender@6.1.0: + dependencies: + tslib: 2.3.0 + zwitch@2.0.4: {} diff --git a/public/desktop/.gitkeep b/public/desktop/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/public/desktop/.gitkeep @@ -0,0 +1 @@ + diff --git a/public/icons/actionsicon.svg b/public/icons/actionsicon.svg new file mode 100644 index 00000000..512ece97 --- /dev/null +++ b/public/icons/actionsicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/apppasswordicon.svg b/public/icons/apppasswordicon.svg new file mode 100644 index 00000000..d74a6e73 --- /dev/null +++ b/public/icons/apppasswordicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/arrowdownicon.svg b/public/icons/arrowdownicon.svg new file mode 100644 index 00000000..fc8fb26d --- /dev/null +++ b/public/icons/arrowdownicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/arrowlefticon.svg b/public/icons/arrowlefticon.svg new file mode 100644 index 00000000..3a8dac68 --- /dev/null +++ b/public/icons/arrowlefticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/arrowrighticon.svg b/public/icons/arrowrighticon.svg new file mode 100644 index 00000000..0c184309 --- /dev/null +++ b/public/icons/arrowrighticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/arrowupicon.svg b/public/icons/arrowupicon.svg new file mode 100644 index 00000000..1427ca01 --- /dev/null +++ b/public/icons/arrowupicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/auditlogicon.svg b/public/icons/auditlogicon.svg new file mode 100644 index 00000000..932aee0b --- /dev/null +++ b/public/icons/auditlogicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/bellicon.svg b/public/icons/bellicon.svg new file mode 100644 index 00000000..369955e1 --- /dev/null +++ b/public/icons/bellicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/binicon.svg b/public/icons/binicon.svg new file mode 100644 index 00000000..9b26e0e6 --- /dev/null +++ b/public/icons/binicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/blockquoteicon.svg b/public/icons/blockquoteicon.svg new file mode 100644 index 00000000..3eabf649 --- /dev/null +++ b/public/icons/blockquoteicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/boldicon.svg b/public/icons/boldicon.svg new file mode 100644 index 00000000..8d49a11a --- /dev/null +++ b/public/icons/boldicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/bulletlisticon.svg b/public/icons/bulletlisticon.svg new file mode 100644 index 00000000..28fff17e --- /dev/null +++ b/public/icons/bulletlisticon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/checkcircleicon.svg b/public/icons/checkcircleicon.svg new file mode 100644 index 00000000..250f2918 --- /dev/null +++ b/public/icons/checkcircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/checkicon.svg b/public/icons/checkicon.svg new file mode 100644 index 00000000..454bd641 --- /dev/null +++ b/public/icons/checkicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/chevronlefticon.svg b/public/icons/chevronlefticon.svg new file mode 100644 index 00000000..d81c9ddd --- /dev/null +++ b/public/icons/chevronlefticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/chevronrighticon.svg b/public/icons/chevronrighticon.svg new file mode 100644 index 00000000..c3182117 --- /dev/null +++ b/public/icons/chevronrighticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/clienticon.svg b/public/icons/clienticon.svg new file mode 100644 index 00000000..cbf4d000 --- /dev/null +++ b/public/icons/clienticon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/cloudicon.svg b/public/icons/cloudicon.svg new file mode 100644 index 00000000..64a8cdef --- /dev/null +++ b/public/icons/cloudicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/collapsesidebaricon.svg b/public/icons/collapsesidebaricon.svg new file mode 100644 index 00000000..b6a538e4 --- /dev/null +++ b/public/icons/collapsesidebaricon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/contracticon.svg b/public/icons/contracticon.svg new file mode 100644 index 00000000..44f8a233 --- /dev/null +++ b/public/icons/contracticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/controlicon.svg b/public/icons/controlicon.svg new file mode 100644 index 00000000..58005dad --- /dev/null +++ b/public/icons/controlicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/copyicon.svg b/public/icons/copyicon.svg new file mode 100644 index 00000000..4585fd85 --- /dev/null +++ b/public/icons/copyicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/couriericon.svg b/public/icons/couriericon.svg new file mode 100644 index 00000000..26bfd215 --- /dev/null +++ b/public/icons/couriericon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/courierserviceicon.svg b/public/icons/courierserviceicon.svg new file mode 100644 index 00000000..0047f959 --- /dev/null +++ b/public/icons/courierserviceicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/csvicon.svg b/public/icons/csvicon.svg new file mode 100644 index 00000000..cf008cab --- /dev/null +++ b/public/icons/csvicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/designicon.svg b/public/icons/designicon.svg new file mode 100644 index 00000000..22e71f0e --- /dev/null +++ b/public/icons/designicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/developericon.svg b/public/icons/developericon.svg new file mode 100644 index 00000000..bd293fe9 --- /dev/null +++ b/public/icons/developericon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/documenticon.svg b/public/icons/documenticon.svg new file mode 100644 index 00000000..cf630001 --- /dev/null +++ b/public/icons/documenticon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/documentjobicon.svg b/public/icons/documentjobicon.svg new file mode 100644 index 00000000..cc1ba748 --- /dev/null +++ b/public/icons/documentjobicon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/documentprintericon.svg b/public/icons/documentprintericon.svg new file mode 100644 index 00000000..84c7ec57 --- /dev/null +++ b/public/icons/documentprintericon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/icons/documentsizeicon.svg b/public/icons/documentsizeicon.svg new file mode 100644 index 00000000..bcf621ee --- /dev/null +++ b/public/icons/documentsizeicon.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/documenttemplateicon.svg b/public/icons/documenttemplateicon.svg new file mode 100644 index 00000000..38dd9658 --- /dev/null +++ b/public/icons/documenttemplateicon.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/downloadicon.svg b/public/icons/downloadicon.svg new file mode 100644 index 00000000..d2d6812d --- /dev/null +++ b/public/icons/downloadicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/duplicateicon.svg b/public/icons/duplicateicon.svg new file mode 100644 index 00000000..6eac6aa3 --- /dev/null +++ b/public/icons/duplicateicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/editicon.svg b/public/icons/editicon.svg new file mode 100644 index 00000000..3c604e4c --- /dev/null +++ b/public/icons/editicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/eject.svg b/public/icons/eject.svg deleted file mode 100644 index 194f8fc5..00000000 --- a/public/icons/eject.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/public/icons/emailaccounticon.svg b/public/icons/emailaccounticon.svg new file mode 100644 index 00000000..8019073d --- /dev/null +++ b/public/icons/emailaccounticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/equalicon.svg b/public/icons/equalicon.svg new file mode 100644 index 00000000..5f4db9ff --- /dev/null +++ b/public/icons/equalicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/excelicon.svg b/public/icons/excelicon.svg new file mode 100644 index 00000000..97530af3 --- /dev/null +++ b/public/icons/excelicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/icons/exclamationoctagonicon.svg b/public/icons/exclamationoctagonicon.svg new file mode 100644 index 00000000..f3377ae1 --- /dev/null +++ b/public/icons/exclamationoctagonicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/expandicon.svg b/public/icons/expandicon.svg new file mode 100644 index 00000000..c8501605 --- /dev/null +++ b/public/icons/expandicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/expandsidebaricon.svg b/public/icons/expandsidebaricon.svg new file mode 100644 index 00000000..e681ec6a --- /dev/null +++ b/public/icons/expandsidebaricon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/exporticon.svg b/public/icons/exporticon.svg new file mode 100644 index 00000000..c3d4ca05 --- /dev/null +++ b/public/icons/exporticon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/eyeicon.svg b/public/icons/eyeicon.svg new file mode 100644 index 00000000..bc9d2f65 --- /dev/null +++ b/public/icons/eyeicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/eyeslashicon.svg b/public/icons/eyeslashicon.svg new file mode 100644 index 00000000..f59e1ec4 --- /dev/null +++ b/public/icons/eyeslashicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/filamenticon.svg b/public/icons/filamenticon.svg new file mode 100644 index 00000000..6dce463c --- /dev/null +++ b/public/icons/filamenticon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/filamentprofileicon.svg b/public/icons/filamentprofileicon.svg new file mode 100644 index 00000000..9d232304 --- /dev/null +++ b/public/icons/filamentprofileicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/filamentskuicon.svg b/public/icons/filamentskuicon.svg new file mode 100644 index 00000000..59c490e4 --- /dev/null +++ b/public/icons/filamentskuicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/filamentstockicon.svg b/public/icons/filamentstockicon.svg new file mode 100644 index 00000000..5a0ef831 --- /dev/null +++ b/public/icons/filamentstockicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/fileicon.svg b/public/icons/fileicon.svg new file mode 100644 index 00000000..a5088c23 --- /dev/null +++ b/public/icons/fileicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/filtercirclefilledicon.svg b/public/icons/filtercirclefilledicon.svg new file mode 100644 index 00000000..0ced6ccc --- /dev/null +++ b/public/icons/filtercirclefilledicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/filtercircleicon.svg b/public/icons/filtercircleicon.svg new file mode 100644 index 00000000..6bafa21b --- /dev/null +++ b/public/icons/filtercircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/filtericon.svg b/public/icons/filtericon.svg new file mode 100644 index 00000000..f25e149f --- /dev/null +++ b/public/icons/filtericon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/financeicon.svg b/public/icons/financeicon.svg new file mode 100644 index 00000000..43072607 --- /dev/null +++ b/public/icons/financeicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/fulfillmentpolicyicon.svg b/public/icons/fulfillmentpolicyicon.svg new file mode 100644 index 00000000..70f5993c --- /dev/null +++ b/public/icons/fulfillmentpolicyicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/functionicon.svg b/public/icons/functionicon.svg new file mode 100644 index 00000000..426d23b1 --- /dev/null +++ b/public/icons/functionicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/gcodefileicon.svg b/public/icons/gcodefileicon.svg new file mode 100644 index 00000000..94bddf31 --- /dev/null +++ b/public/icons/gcodefileicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/graphicon.svg b/public/icons/graphicon.svg new file mode 100644 index 00000000..1d99c694 --- /dev/null +++ b/public/icons/graphicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/greaterthanicon.svg b/public/icons/greaterthanicon.svg new file mode 100644 index 00000000..5fc932db --- /dev/null +++ b/public/icons/greaterthanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/greaterthanorequaltoicon.svg b/public/icons/greaterthanorequaltoicon.svg new file mode 100644 index 00000000..9c494929 --- /dev/null +++ b/public/icons/greaterthanorequaltoicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/gridicon.svg b/public/icons/gridicon.svg new file mode 100644 index 00000000..e64bb4f0 --- /dev/null +++ b/public/icons/gridicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/icons/homeicon.svg b/public/icons/homeicon.svg new file mode 100644 index 00000000..fde4206e --- /dev/null +++ b/public/icons/homeicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/hosticon.svg b/public/icons/hosticon.svg new file mode 100644 index 00000000..460702dc --- /dev/null +++ b/public/icons/hosticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/hotendicon.svg b/public/icons/hotendicon.svg new file mode 100644 index 00000000..311395ec --- /dev/null +++ b/public/icons/hotendicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/infocircleicon.svg b/public/icons/infocircleicon.svg new file mode 100644 index 00000000..613c1d77 --- /dev/null +++ b/public/icons/infocircleicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/infoicon.svg b/public/icons/infoicon.svg new file mode 100644 index 00000000..7167f5dd --- /dev/null +++ b/public/icons/infoicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/inventoryicon.svg b/public/icons/inventoryicon.svg new file mode 100644 index 00000000..4b878b7c --- /dev/null +++ b/public/icons/inventoryicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/invoiceicon.svg b/public/icons/invoiceicon.svg new file mode 100644 index 00000000..e147b649 --- /dev/null +++ b/public/icons/invoiceicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/italicicon.svg b/public/icons/italicicon.svg new file mode 100644 index 00000000..06e09b57 --- /dev/null +++ b/public/icons/italicicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/jobicon.svg b/public/icons/jobicon.svg new file mode 100644 index 00000000..c35f6c9d --- /dev/null +++ b/public/icons/jobicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/jsonarrayicon.svg b/public/icons/jsonarrayicon.svg new file mode 100644 index 00000000..1866bc23 --- /dev/null +++ b/public/icons/jsonarrayicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/icons/jsonboolicon.svg b/public/icons/jsonboolicon.svg new file mode 100644 index 00000000..5b639b1d --- /dev/null +++ b/public/icons/jsonboolicon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/jsonnumbericon.svg b/public/icons/jsonnumbericon.svg new file mode 100644 index 00000000..c76184ee --- /dev/null +++ b/public/icons/jsonnumbericon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/jsonobjecticon.svg b/public/icons/jsonobjecticon.svg new file mode 100644 index 00000000..05634266 --- /dev/null +++ b/public/icons/jsonobjecticon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/jsonstringicon.svg b/public/icons/jsonstringicon.svg new file mode 100644 index 00000000..eeeccd18 --- /dev/null +++ b/public/icons/jsonstringicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/kanbanicon.svg b/public/icons/kanbanicon.svg new file mode 100644 index 00000000..1900d1e4 --- /dev/null +++ b/public/icons/kanbanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/lessthanicon.svg b/public/icons/lessthanicon.svg new file mode 100644 index 00000000..d0ecfd86 --- /dev/null +++ b/public/icons/lessthanicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/lessthanorequaltoicon.svg b/public/icons/lessthanorequaltoicon.svg new file mode 100644 index 00000000..5094130f --- /dev/null +++ b/public/icons/lessthanorequaltoicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/levelbedicon.svg b/public/icons/levelbedicon.svg new file mode 100644 index 00000000..af737c71 --- /dev/null +++ b/public/icons/levelbedicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/linkicon.svg b/public/icons/linkicon.svg new file mode 100644 index 00000000..ce2b1a11 --- /dev/null +++ b/public/icons/linkicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/listicon.svg b/public/icons/listicon.svg new file mode 100644 index 00000000..344c6449 --- /dev/null +++ b/public/icons/listicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/listingicon.svg b/public/icons/listingicon.svg new file mode 100644 index 00000000..ed412f60 --- /dev/null +++ b/public/icons/listingicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/listingvarienticon.svg b/public/icons/listingvarienticon.svg new file mode 100644 index 00000000..34082ec3 --- /dev/null +++ b/public/icons/listingvarienticon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/loadingicon.svg b/public/icons/loadingicon.svg new file mode 100644 index 00000000..5836c96e --- /dev/null +++ b/public/icons/loadingicon.svg @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/public/icons/lockicon.svg b/public/icons/lockicon.svg new file mode 100644 index 00000000..cd9f0bcd --- /dev/null +++ b/public/icons/lockicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/logouticon.svg b/public/icons/logouticon.svg new file mode 100644 index 00000000..003d2c82 --- /dev/null +++ b/public/icons/logouticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/magicwandicon.svg b/public/icons/magicwandicon.svg new file mode 100644 index 00000000..e50833c2 --- /dev/null +++ b/public/icons/magicwandicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/mailcheckicon.svg b/public/icons/mailcheckicon.svg new file mode 100644 index 00000000..aee41eb6 --- /dev/null +++ b/public/icons/mailcheckicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/mailicon.svg b/public/icons/mailicon.svg new file mode 100644 index 00000000..c6cd9f19 --- /dev/null +++ b/public/icons/mailicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/mailtemplateicon.svg b/public/icons/mailtemplateicon.svg new file mode 100644 index 00000000..41290340 --- /dev/null +++ b/public/icons/mailtemplateicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/marketplaceicon.svg b/public/icons/marketplaceicon.svg new file mode 100644 index 00000000..58f212ec --- /dev/null +++ b/public/icons/marketplaceicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/materialicon.svg b/public/icons/materialicon.svg new file mode 100644 index 00000000..6816f534 --- /dev/null +++ b/public/icons/materialicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/menuicon.svg b/public/icons/menuicon.svg new file mode 100644 index 00000000..26e448a2 --- /dev/null +++ b/public/icons/menuicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/icons/minusicon.svg b/public/icons/minusicon.svg new file mode 100644 index 00000000..f499b326 --- /dev/null +++ b/public/icons/minusicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/multimotionicon.svg b/public/icons/multimotionicon.svg new file mode 100644 index 00000000..378a2151 --- /dev/null +++ b/public/icons/multimotionicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/newmailicon.svg b/public/icons/newmailicon.svg new file mode 100644 index 00000000..d570f577 --- /dev/null +++ b/public/icons/newmailicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/newwindowicon.svg b/public/icons/newwindowicon.svg new file mode 100644 index 00000000..38c396ae --- /dev/null +++ b/public/icons/newwindowicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/noteicon.svg b/public/icons/noteicon.svg new file mode 100644 index 00000000..77e13962 --- /dev/null +++ b/public/icons/noteicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/notequalicon.svg b/public/icons/notequalicon.svg new file mode 100644 index 00000000..a0ac9dd4 --- /dev/null +++ b/public/icons/notequalicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/notetypeicon.svg b/public/icons/notetypeicon.svg new file mode 100644 index 00000000..cac3e53d --- /dev/null +++ b/public/icons/notetypeicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/icons/numberedlisticon.svg b/public/icons/numberedlisticon.svg new file mode 100644 index 00000000..7401bbd6 --- /dev/null +++ b/public/icons/numberedlisticon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/odataicon.svg b/public/icons/odataicon.svg new file mode 100644 index 00000000..65d7813f --- /dev/null +++ b/public/icons/odataicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/openappicon.svg b/public/icons/openappicon.svg new file mode 100644 index 00000000..eb4ab94a --- /dev/null +++ b/public/icons/openappicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/openinappicon.svg b/public/icons/openinappicon.svg new file mode 100644 index 00000000..28251b80 --- /dev/null +++ b/public/icons/openinappicon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/openinbrowsericon.svg b/public/icons/openinbrowsericon.svg new file mode 100644 index 00000000..0ad76b08 --- /dev/null +++ b/public/icons/openinbrowsericon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/orderitemicon.svg b/public/icons/orderitemicon.svg new file mode 100644 index 00000000..3ddc2b7d --- /dev/null +++ b/public/icons/orderitemicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/otpicon.svg b/public/icons/otpicon.svg new file mode 100644 index 00000000..58a8ce2a --- /dev/null +++ b/public/icons/otpicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/panfilledicon.svg b/public/icons/panfilledicon.svg new file mode 100644 index 00000000..38617ecd --- /dev/null +++ b/public/icons/panfilledicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/panicon.svg b/public/icons/panicon.svg new file mode 100644 index 00000000..ba86dc9e --- /dev/null +++ b/public/icons/panicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/particon.svg b/public/icons/particon.svg new file mode 100644 index 00000000..1b92f644 --- /dev/null +++ b/public/icons/particon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/partskuicon.svg b/public/icons/partskuicon.svg new file mode 100644 index 00000000..0f83ffd2 --- /dev/null +++ b/public/icons/partskuicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/partstockicon.svg b/public/icons/partstockicon.svg new file mode 100644 index 00000000..01531911 --- /dev/null +++ b/public/icons/partstockicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/passkeysicon.svg b/public/icons/passkeysicon.svg new file mode 100644 index 00000000..0861bf9f --- /dev/null +++ b/public/icons/passkeysicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/icons/pausecircleicon.svg b/public/icons/pausecircleicon.svg new file mode 100644 index 00000000..247ffda7 --- /dev/null +++ b/public/icons/pausecircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/pauseicon.svg b/public/icons/pauseicon.svg new file mode 100644 index 00000000..dc7ab032 --- /dev/null +++ b/public/icons/pauseicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/paymenticon.svg b/public/icons/paymenticon.svg new file mode 100644 index 00000000..7c0d9a05 --- /dev/null +++ b/public/icons/paymenticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/paymentpolicyicon.svg b/public/icons/paymentpolicyicon.svg new file mode 100644 index 00000000..f1837f1e --- /dev/null +++ b/public/icons/paymentpolicyicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/permissionsettingsicon.svg b/public/icons/permissionsettingsicon.svg new file mode 100644 index 00000000..78bb70fa --- /dev/null +++ b/public/icons/permissionsettingsicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/persongroupicon.svg b/public/icons/persongroupicon.svg new file mode 100644 index 00000000..f9f288c1 --- /dev/null +++ b/public/icons/persongroupicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/personicon.svg b/public/icons/personicon.svg new file mode 100644 index 00000000..2885563d --- /dev/null +++ b/public/icons/personicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/pictureicon.svg b/public/icons/pictureicon.svg new file mode 100644 index 00000000..e90a6943 --- /dev/null +++ b/public/icons/pictureicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/playcircleicon.svg b/public/icons/playcircleicon.svg new file mode 100644 index 00000000..ae40aec2 --- /dev/null +++ b/public/icons/playcircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/playicon.svg b/public/icons/playicon.svg new file mode 100644 index 00000000..76bb3e81 --- /dev/null +++ b/public/icons/playicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/plusicon.svg b/public/icons/plusicon.svg new file mode 100644 index 00000000..19189482 --- /dev/null +++ b/public/icons/plusicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/plusminusicon.svg b/public/icons/plusminusicon.svg new file mode 100644 index 00000000..1b90591d --- /dev/null +++ b/public/icons/plusminusicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/printedparticon.svg b/public/icons/printedparticon.svg new file mode 100644 index 00000000..0aeb5f95 --- /dev/null +++ b/public/icons/printedparticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/printericon.svg b/public/icons/printericon.svg new file mode 100644 index 00000000..0aca6423 --- /dev/null +++ b/public/icons/printericon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/printerprofileicon.svg b/public/icons/printerprofileicon.svg new file mode 100644 index 00000000..373ce9fb --- /dev/null +++ b/public/icons/printerprofileicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/productcategoryicon.svg b/public/icons/productcategoryicon.svg new file mode 100644 index 00000000..825fee86 --- /dev/null +++ b/public/icons/productcategoryicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/producticon.svg b/public/icons/producticon.svg new file mode 100644 index 00000000..c63c5d59 --- /dev/null +++ b/public/icons/producticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/productionicon.svg b/public/icons/productionicon.svg new file mode 100644 index 00000000..abd2d9f0 --- /dev/null +++ b/public/icons/productionicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/productskuicon.svg b/public/icons/productskuicon.svg new file mode 100644 index 00000000..42a3d8a8 --- /dev/null +++ b/public/icons/productskuicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/productstockicon.svg b/public/icons/productstockicon.svg new file mode 100644 index 00000000..6c6e8872 --- /dev/null +++ b/public/icons/productstockicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/purchaseordericon.svg b/public/icons/purchaseordericon.svg new file mode 100644 index 00000000..db04b342 --- /dev/null +++ b/public/icons/purchaseordericon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/questioncircleicon.svg b/public/icons/questioncircleicon.svg new file mode 100644 index 00000000..90a9e755 --- /dev/null +++ b/public/icons/questioncircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/reloadicon.svg b/public/icons/reloadicon.svg new file mode 100644 index 00000000..cef9db15 --- /dev/null +++ b/public/icons/reloadicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/icons/returnpolicyicon.svg b/public/icons/returnpolicyicon.svg new file mode 100644 index 00000000..8fad746a --- /dev/null +++ b/public/icons/returnpolicyicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/icons/rssicon.svg b/public/icons/rssicon.svg new file mode 100644 index 00000000..b8600a26 --- /dev/null +++ b/public/icons/rssicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/icons/rulericon.svg b/public/icons/rulericon.svg new file mode 100644 index 00000000..d7d70a31 --- /dev/null +++ b/public/icons/rulericon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/salesicon.svg b/public/icons/salesicon.svg new file mode 100644 index 00000000..01207be4 --- /dev/null +++ b/public/icons/salesicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/salesordericon.svg b/public/icons/salesordericon.svg new file mode 100644 index 00000000..c1bea564 --- /dev/null +++ b/public/icons/salesordericon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/searchicon.svg b/public/icons/searchicon.svg new file mode 100644 index 00000000..4798b324 --- /dev/null +++ b/public/icons/searchicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/settingsicon.svg b/public/icons/settingsicon.svg new file mode 100644 index 00000000..219aba6b --- /dev/null +++ b/public/icons/settingsicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/shipmenticon.svg b/public/icons/shipmenticon.svg new file mode 100644 index 00000000..251bd774 --- /dev/null +++ b/public/icons/shipmenticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/softwareupdateicon.svg b/public/icons/softwareupdateicon.svg new file mode 100644 index 00000000..ebe6497c --- /dev/null +++ b/public/icons/softwareupdateicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/sortcirclefilledicon.svg b/public/icons/sortcirclefilledicon.svg new file mode 100644 index 00000000..dfe1d1a8 --- /dev/null +++ b/public/icons/sortcirclefilledicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/sortcircleicon.svg b/public/icons/sortcircleicon.svg new file mode 100644 index 00000000..6c36d5cc --- /dev/null +++ b/public/icons/sortcircleicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/spoolicon.svg b/public/icons/spoolicon.svg new file mode 100644 index 00000000..9024ca80 --- /dev/null +++ b/public/icons/spoolicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/stockauditicon.svg b/public/icons/stockauditicon.svg new file mode 100644 index 00000000..3767a76f --- /dev/null +++ b/public/icons/stockauditicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/stockauditlevelicon.svg b/public/icons/stockauditlevelicon.svg new file mode 100644 index 00000000..dca83d34 --- /dev/null +++ b/public/icons/stockauditlevelicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/icons/stockeventicon.svg b/public/icons/stockeventicon.svg new file mode 100644 index 00000000..3ace3047 --- /dev/null +++ b/public/icons/stockeventicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/stocklocationicon.svg b/public/icons/stocklocationicon.svg new file mode 100644 index 00000000..43e29e55 --- /dev/null +++ b/public/icons/stocklocationicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/stocktransfericon.svg b/public/icons/stocktransfericon.svg new file mode 100644 index 00000000..692eaa21 --- /dev/null +++ b/public/icons/stocktransfericon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/stopcircleicon.svg b/public/icons/stopcircleicon.svg new file mode 100644 index 00000000..f716d1e2 --- /dev/null +++ b/public/icons/stopcircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/subjobicon.svg b/public/icons/subjobicon.svg new file mode 100644 index 00000000..e2bf4179 --- /dev/null +++ b/public/icons/subjobicon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/taxrateicon.svg b/public/icons/taxrateicon.svg new file mode 100644 index 00000000..82d57cc2 --- /dev/null +++ b/public/icons/taxrateicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/taxrecordicon.svg b/public/icons/taxrecordicon.svg new file mode 100644 index 00000000..cf187c9f --- /dev/null +++ b/public/icons/taxrecordicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/icons/taxruleicon.svg b/public/icons/taxruleicon.svg new file mode 100644 index 00000000..4ff9aa2a --- /dev/null +++ b/public/icons/taxruleicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/textformaticon.svg b/public/icons/textformaticon.svg new file mode 100644 index 00000000..f59def55 --- /dev/null +++ b/public/icons/textformaticon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/texticon.svg b/public/icons/texticon.svg new file mode 100644 index 00000000..c35172e1 --- /dev/null +++ b/public/icons/texticon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/icons/threedotsicon.svg b/public/icons/threedotsicon.svg new file mode 100644 index 00000000..cfbdaee2 --- /dev/null +++ b/public/icons/threedotsicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/icons/timelineicon.svg b/public/icons/timelineicon.svg new file mode 100644 index 00000000..8c3a5505 --- /dev/null +++ b/public/icons/timelineicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/icons/underlineicon.svg b/public/icons/underlineicon.svg new file mode 100644 index 00000000..7d1fe91b --- /dev/null +++ b/public/icons/underlineicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/unloadicon.svg b/public/icons/unloadicon.svg new file mode 100644 index 00000000..6b4c0d80 --- /dev/null +++ b/public/icons/unloadicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/uploadicon.svg b/public/icons/uploadicon.svg new file mode 100644 index 00000000..99d9fff1 --- /dev/null +++ b/public/icons/uploadicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/vendoricon.svg b/public/icons/vendoricon.svg new file mode 100644 index 00000000..7a3b392c --- /dev/null +++ b/public/icons/vendoricon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/icons/xmarkcircleicon.svg b/public/icons/xmarkcircleicon.svg new file mode 100644 index 00000000..2e236cbc --- /dev/null +++ b/public/icons/xmarkcircleicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/xmarkicon.svg b/public/icons/xmarkicon.svg new file mode 100644 index 00000000..e37d24ef --- /dev/null +++ b/public/icons/xmarkicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/App.jsx b/src/App.jsx index d88b4cf8..abfdb534 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,6 +19,7 @@ import { } from './components/Dashboard/context/SpotlightContext.jsx' import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx' import { ActionsProvider } from './components/Dashboard/context/ActionsContext.jsx' +import { DashboardObjectToolsProvider } from './components/Dashboard/context/DashboardObjectToolsContext.jsx' import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx' import { @@ -30,11 +31,13 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx' import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx' import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx' +import { TooltipProvider } from './components/Dashboard/context/TooltipContext.jsx' import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx' import AuthCallback from './components/App/AuthCallback.jsx' import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx' import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx' import AuthLaunch from './components/App/AppLaunch.jsx' +import DesktopAuthComplete from './pages/DesktopAuthComplete.jsx' import { TableStateProvider } from './components/Dashboard/context/TableStateContext.jsx' const SlicerIntegration = lazy( () => @@ -85,13 +88,19 @@ const AppContent = () => { - - - - - - - + + + + + + + + + + } + /> } @@ -162,12 +171,14 @@ const AppContent = () => { } /> - - - - - - + + + + + + + + diff --git a/src/codemirror/farmControlTheme.js b/src/codemirror/farmControlTheme.js new file mode 100644 index 00000000..0f417f14 --- /dev/null +++ b/src/codemirror/farmControlTheme.js @@ -0,0 +1,230 @@ +import { EditorView } from '@codemirror/view' +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' +import { tags } from '@lezer/highlight' + +const editorFontFamily = "'DM Mono', monospace" + +const farmControlHighlightStyle = HighlightStyle.define([ + { tag: tags.keyword, color: 'var(--color-purple)' }, + { + tag: [ + tags.function(tags.variableName), + tags.labelName, + tags.definition(tags.function(tags.variableName)) + ], + color: 'var(--color-magenta)' + }, + { + tag: [tags.string, tags.inserted, tags.special(tags.string)], + color: 'var(--color-success)' + }, + { + tag: [ + tags.number, + tags.bool, + tags.atom, + tags.changed, + tags.typeName, + tags.className, + tags.annotation, + tags.modifier, + tags.self, + tags.namespace, + tags.color, + tags.constant(tags.name), + tags.standard(tags.name) + ], + color: 'var(--color-warning)' + }, + { + tag: [ + tags.operator, + tags.operatorKeyword, + tags.url, + tags.escape, + tags.regexp, + tags.link + ], + color: 'var(--color-cyan)' + }, + { tag: tags.invalid, color: 'var(--color-error)' }, + { + tag: [tags.meta, tags.comment], + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)', + fontStyle: 'italic' + }, + { + tag: [ + tags.name, + tags.deleted, + tags.character, + tags.propertyName, + tags.macroName, + tags.definition(tags.name), + tags.separator, + tags.variableName + ], + color: 'var(--color-text)' + }, + { tag: tags.heading, fontWeight: 'bold', color: 'var(--color-error)' }, + { tag: tags.strong, fontWeight: 'bold' }, + { tag: tags.emphasis, fontStyle: 'italic' }, + { tag: tags.strikethrough, textDecoration: 'line-through' }, + { + tag: tags.link, + color: 'var(--color-link)', + textDecoration: 'underline' + }, + { + tag: [tags.processingInstruction], + color: 'var(--color-info)' + } +]) + +const farmControlLightTheme = EditorView.theme( + { + '&': { + color: 'var(--color-text)', + backgroundColor: 'var(--layout-modal-bg)', + fontFamily: editorFontFamily, + fontSize: '14px' + }, + '.cm-content': { + caretColor: 'var(--color-primary)' + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-primary)' + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 25%, transparent)' + }, + '.cm-activeLine': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 8%, transparent)' + }, + '.cm-selectionMatch': { + backgroundColor: + 'color-mix(in srgb, var(--color-success) 15%, transparent)' + }, + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: + 'color-mix(in srgb, var(--color-warning) 20%, transparent)' + }, + '.cm-gutters': { + backgroundColor: 'var(--layout-modal-bg)', + color: 'color-mix(in srgb, var(--color-text) 45%, transparent)', + border: 'none' + }, + '.cm-activeLineGutter': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 8%, transparent)' + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)' + }, + '.cm-tooltip': { + border: 'none', + backgroundColor: 'var(--layout-modal-bg)', + color: 'var(--color-text)' + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent' + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: 'var(--layout-modal-bg)', + borderBottomColor: 'var(--layout-modal-bg)' + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 12%, transparent)', + color: 'var(--color-text)' + } + } + }, + { dark: false } +) + +const farmControlDarkTheme = EditorView.theme( + { + '&': { + color: 'var(--color-text)', + backgroundColor: 'var(--layout-modal-bg)', + fontFamily: editorFontFamily, + fontSize: '14px' + }, + '.cm-content': { + caretColor: 'var(--color-primary)' + }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-primary)' + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 30%, transparent)' + }, + '.cm-activeLine': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 10%, transparent)' + }, + '.cm-selectionMatch': { + backgroundColor: + 'color-mix(in srgb, var(--color-success) 18%, transparent)' + }, + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: + 'color-mix(in srgb, var(--color-warning) 22%, transparent)' + }, + '.cm-gutters': { + backgroundColor: 'var(--layout-modal-bg)', + color: 'color-mix(in srgb, var(--color-text) 45%, transparent)', + border: 'none' + }, + '.cm-activeLineGutter': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 10%, transparent)' + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: 'color-mix(in srgb, var(--color-text) 55%, transparent)' + }, + '.cm-tooltip': { + border: 'none', + backgroundColor: 'var(--layout-modal-bg)', + color: 'var(--color-text)' + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent' + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: 'var(--layout-modal-bg)', + borderBottomColor: 'var(--layout-modal-bg)' + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: + 'color-mix(in srgb, var(--color-primary) 15%, transparent)', + color: 'var(--color-text)' + } + } + }, + { dark: true } +) + +export const farmControlLight = [ + farmControlLightTheme, + syntaxHighlighting(farmControlHighlightStyle) +] + +export const farmControlDark = [ + farmControlDarkTheme, + syntaxHighlighting(farmControlHighlightStyle) +] diff --git a/src/codemirror/fcTemplateLang/ejs.grammar b/src/codemirror/fcTemplateLang/ejs.grammar new file mode 100644 index 00000000..b366b988 --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.grammar @@ -0,0 +1,45 @@ +// FarmControl EJS outer grammar for fcTemplateLang. +// Regenerate the parser with: +// pnpm exec lezer-generator src/codemirror/fcTemplateLang/ejs.grammar --output src/codemirror/fcTemplateLang/ejs.parser.js +// +// Outer language = EJS scriptlets; Text regions are overlaid with XML and +// JavascriptExpression nodes are mounted with the JS parser (parseMixed). + +@top Template { content* } + +content { + Scriptlet | + Output | + Comment | + Text +} + +Scriptlet { OpeningTag JavascriptExpression ClosingTag } +Output { OutputTag JavascriptExpression ClosingTag } + +@skip {} { + Comment { CommentTag (CommentContent | commentNewLine)* commentEnd } +} + +@local tokens { + commentEnd { ClosingTag } + commentNewLine { "\n" } + @else CommentContent +} + +@tokens { + // Prefer longer tags via @precedence below + OpeningTag[closedBy=ClosingTag] { "<%" } + OutputTag[closedBy=ClosingTag] { "<%=" | "<%-" | "<%_" } + CommentTag[closedBy=ClosingTag] { "<%#" } + ClosingTag[openedBy="OpeningTag OutputTag CommentTag"] { ("-" | "_")? "%>" } + + @precedence { OutputTag, CommentTag, OpeningTag } +} + +@external tokens javascriptExpressionOrText from "./tokens.js" { + JavascriptExpression, + Text +} + +@detectDelim diff --git a/src/codemirror/fcTemplateLang/ejs.parser.js b/src/codemirror/fcTemplateLang/ejs.parser.js new file mode 100644 index 00000000..1d46438c --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.parser.js @@ -0,0 +1,21 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import {LRParser, LocalTokenGroup} from "@lezer/lr" +import {javascriptExpressionOrText} from "./tokens.js" +export const parser = LRParser.deserialize({ + version: 14, + states: "#YQQOROOO`OQO'#C`OeOQO'#CcOjOSO'#CeOOOQ'#Ck'#CkOOOQ'#Ch'#ChQQOROOOuOPO,58zOzOPO,58}OOOO'#Ci'#CiO!POSO,59POOOQ,59P,59POOOQ-E6f-E6fOOOQ1G.f1G.fOOOQ1G.i1G.iOOOO-E6g-E6gOOOQ1G.k1G.k", + stateData: "!a~OQSOTPOWQOYRO~OPVO~OPWO~OZXO`XOaZO~OU]O~OU^O~OZXO`XOa`O~OWYTY~", + goto: "u`PPPPaPPaPaPPekPqTSOUQUOR[UQYRR_YTTOU", + nodeNames: "⚠ JavascriptExpression Text Template Scriptlet OpeningTag ClosingTag Output OutputTag Comment CommentTag CommentContent", + maxTerm: 17, + nodeProps: [ + ["closedBy", -3,5,8,10,"ClosingTag"], + ["openedBy", 6,"OpeningTag OutputTag CommentTag"] + ], + skippedNodes: [0], + repeatNodeCount: 2, + tokenData: "!c~RSuv_}!Oj!^!_p#R#Sj~bP!`!ae~jOU~~mPuv_~sPuvv~{ST~st!X}!O!^!_!`!^#R#S!^~!^OY~~!cOW~", + tokenizers: [1, javascriptExpressionOrText, new LocalTokenGroup("v~RSYZ_uvd}!Oo#R#So~dO`~~gP!`!aj~oOa~~rPuvd~", 37, 11)], + topRules: {"Template":[0,3]}, + tokenPrec: 58 +}) diff --git a/src/codemirror/fcTemplateLang/ejs.parser.terms.js b/src/codemirror/fcTemplateLang/ejs.parser.terms.js new file mode 100644 index 00000000..036ce73a --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.parser.terms.js @@ -0,0 +1,13 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +export const + JavascriptExpression = 1, + Text = 2, + Template = 3, + Scriptlet = 4, + OpeningTag = 5, + ClosingTag = 6, + Output = 7, + OutputTag = 8, + Comment = 9, + CommentTag = 10, + CommentContent = 11 diff --git a/src/codemirror/fcTemplateLang/index.js b/src/codemirror/fcTemplateLang/index.js new file mode 100644 index 00000000..84188706 --- /dev/null +++ b/src/codemirror/fcTemplateLang/index.js @@ -0,0 +1,160 @@ +import { + LRLanguage, + LanguageSupport, + indentNodeProp, + delimitedIndent, + foldNodeProp, + foldInside, + syntaxTree +} from '@codemirror/language' +import { styleTags, tags as t } from '@lezer/highlight' +import { parseMixed } from '@lezer/common' +import { javascriptLanguage } from '@codemirror/lang-javascript' +import { javascriptCompletionSupport } from '../javascriptLang/index.js' +import { + xmlLanguage, + completeFromSchema, + autoCloseTags +} from '@codemirror/lang-xml' +import { parser as ejsParser } from './ejs.parser.js' +import { + fcTemplateElements, + fcTemplateAttributes, + getFcTemplateHelpers, + placeholderCompletionSource +} from './schema.js' +import { normalizeTypedScope } from '../javascriptLang/typedScope.js' + +/** + * FarmControl document template language: custom XML tags with EJS + * scriptlets (`<% %>`, `<%= %>`, etc.). Uses mixed-language parsing so + * XML and JavaScript each get their own highlighting/support. + * + * @see https://codemirror.net/examples/mixed-language/ + * @see https://codemirror.net/examples/autocompletion/ + */ +const fcTemplateParser = ejsParser.configure({ + props: [ + indentNodeProp.add({ + JavascriptExpression: delimitedIndent({ closing: '%>', align: false }) + }), + foldNodeProp.add({ + Scriptlet: foldInside, + Output: foldInside, + Comment: foldInside + }), + styleTags({ + 'OpeningTag OutputTag CommentTag ClosingTag': t.controlKeyword, + CommentContent: t.blockComment + }) + ], + wrap: parseMixed((node) => { + // Mount JS inside EJS bodies + if (node.name === 'JavascriptExpression') { + return { parser: javascriptLanguage.parser } + } + // Overlay XML across all Text regions so tags that wrap EJS + // (e.g. <%= x %>) stay one coherent XML tree. + if (node.type.isTop) { + return { + parser: xmlLanguage.parser, + overlay: (n) => n.name === 'Text' + } + } + return null + }) +}) + +export const fcTemplateLanguage = LRLanguage.define({ + name: 'fcTemplateLang', + parser: fcTemplateParser, + languageData: { + commentTokens: { block: { open: '<%#', close: '%>' } }, + closeBrackets: { brackets: ['(', '[', '{', '"', "'", '`'] } + } +}) + +function buildJsScope(autoCompleteObject, templateType) { + const typed = normalizeTypedScope(autoCompleteObject) + typed.properties = { + ...typed.properties, + fc: getFcTemplateHelpers(templateType) + } + return typed +} + +function isResourceValueContext(state, position) { + let node = syntaxTree(state).resolveInner(position, -1) + let insideTemplateInterpolation = false + + while (node) { + if (node.name === 'Interpolation') { + insideTemplateInterpolation = true + } + if (node.name === 'AttributeValue' || node.name === 'String') { + return true + } + if (node.name === 'TemplateString') { + return !insideTemplateInterpolation + } + node = node.parent + } + + return false +} + +function resourceCompletionSource(resourceNames) { + const options = [...new Set(resourceNames)] + .filter((name) => typeof name === 'string' && name.length > 0) + .map((name) => ({ + label: `@[${name}]`, + apply: `@[${name}]`, + type: 'variable', + detail: 'Template resource' + })) + + return (context) => { + if (options.length === 0) return null + + const before = context.state.sliceDoc(0, context.pos) + const match = before.match(/@(?:\[[^\]\r\n]*)?$/) + if (!match || !isResourceValueContext(context.state, context.pos)) { + return null + } + + return { + from: context.pos - match[0].length, + options + } + } +} + +/** + * Language support for FarmControl EJS+XML document templates. + * @param {{ autoCompleteObject?: object|string, templateType?: string, resourceNames?: string[] }} [options] + */ +export function fcTemplateLang(options = {}) { + const { + autoCompleteObject, + templateType = 'documentTemplate', + resourceNames = [] + } = options + const jsScope = buildJsScope(autoCompleteObject, templateType) + const completeResources = resourceCompletionSource(resourceNames) + const completePlaceholders = placeholderCompletionSource() + + return new LanguageSupport(fcTemplateLanguage, [ + xmlLanguage.data.of({ + autocomplete: completeFromSchema( + fcTemplateElements, + fcTemplateAttributes + ) + }), + xmlLanguage.data.of({ autocomplete: completeResources }), + xmlLanguage.data.of({ autocomplete: completePlaceholders }), + fcTemplateLanguage.data.of({ autocomplete: completePlaceholders }), + javascriptLanguage.data.of({ autocomplete: completeResources }), + autoCloseTags, + ...javascriptCompletionSupport(jsScope) + ]) +} diff --git a/src/codemirror/fcTemplateLang/schema.js b/src/codemirror/fcTemplateLang/schema.js new file mode 100644 index 00000000..ba84d953 --- /dev/null +++ b/src/codemirror/fcTemplateLang/schema.js @@ -0,0 +1,220 @@ +/** + * XML schema for FarmControl document template tags. + * Mirrors custom elements / style attrs handled in farmcontrol-ws TemplateManager. + */ + +import { syntaxTree } from '@codemirror/language' + +const styleAttrs = [ + 'padding', + 'margin', + 'width', + 'height', + 'minWidth', + 'maxWidth', + 'minHeight', + 'maxHeight', + 'gap', + 'justify', + 'align', + 'border', + 'borderRadius', + 'vertical', + 'header', + 'footer', + 'grow', + 'shrink', + 'color', + 'background', + 'scale', + 'textAlign', + 'textSize', + 'wordWrap' +] + +const justifyValues = [ + 'flex-start', + 'flex-end', + 'center', + 'space-between', + 'space-around', + 'space-evenly' +] + +const alignValues = [ + 'flex-start', + 'flex-end', + 'center', + 'stretch', + 'baseline' +] + +/** @type {import('@codemirror/lang-xml').AttrSpec[]} */ +export const fcTemplateAttributes = [ + ...styleAttrs.map((name) => { + const attr = { name, global: true } + if (name === 'vertical' || name === 'header' || name === 'footer') { + attr.values = ['true', 'false'] + } + if (name === 'justify') attr.values = justifyValues + if (name === 'align') attr.values = alignValues + if (name === 'textAlign') { + attr.values = ['left', 'center', 'right', 'justify'] + } + return attr + }), + { + name: 'format', + values: ['code128', 'code39', 'ean13', 'ean8', 'upc', 'qrcode'] + }, + { name: 'barcodeWidth' }, + { + name: 'type', + values: ['header', 'footer', 'body'] + } +] + +const allTags = [ + 'Flex', + 'Container', + 'Title1', + 'Title2', + 'Title3', + 'Title4', + 'Text', + 'Bold', + 'Divider', + 'PageBreak', + 'ProgressBar', + 'Barcode', + 'DateTime', + 'Table', + 'Row', + 'Col' +] + +/** @type {import('@codemirror/lang-xml').ElementSpec[]} */ +export const fcTemplateElements = allTags.map((name) => { + const el = { name, top: true, children: allTags } + + if (name === 'Barcode') { + el.attributes = ['format', 'barcodeWidth', ...styleAttrs] + } else if (name === 'Row') { + el.attributes = ['type', ...styleAttrs] + } else if (name === 'Divider' || name === 'PageBreak') { + el.children = [] + } + + return el +}) + +/** Runtime helpers available as `fc` inside templates. */ +const fcSharedHelpers = { + listObjects: { + $type: 'function', + detail: '(objectType, filter?, populate?) => Promise' + }, + getObject: { + $type: 'function', + detail: '(objectType, id, populate?) => Promise' + }, + formatDate: { + $type: 'function', + detail: '(date, format) => string' + } +} + +export const fcDocumentTemplateHelpers = { + $type: 'object', + name: 'fc', + properties: { + ...fcSharedHelpers, + renderDocumentTemplate: { + $type: 'function', + detail: '(reference, object?) => Promise' + } + } +} + +export const fcEmailTemplateHelpers = { + $type: 'object', + name: 'fc', + properties: { + ...fcSharedHelpers, + renderEmailTemplate: { + $type: 'function', + detail: '(reference, object?) => Promise' + } + } +} + +/** Pagination placeholders replaced after layout (`!pageNumber`, `!totalPages`). */ +export const fcTemplatePlaceholders = [ + { + name: '!pageNumber', + detail: 'Current page number' + }, + { + name: '!totalPages', + detail: 'Total page count' + } +] + +export function placeholderCompletionSource() { + const options = fcTemplatePlaceholders.map((placeholder) => ({ + label: placeholder.name, + apply: placeholder.name, + type: 'variable', + detail: placeholder.detail + })) + + return (context) => { + if (isPlaceholderBlockedContext(context.state, context.pos)) { + return null + } + + const before = context.state.sliceDoc(Math.max(0, context.pos - 48), context.pos) + const match = before.match(/![a-zA-Z]*$/) + if (!match) { + return null + } + + return { + from: context.pos - match[0].length, + options + } + } +} + +function isPlaceholderBlockedContext(state, position) { + let node = syntaxTree(state).resolveInner(position, -1) + + while (node) { + if ( + node.name === 'AttributeValue' || + node.name === 'AttributeName' || + node.name === 'Attribute' || + node.name === 'OpenTag' || + node.name === 'CloseTag' || + node.name === 'StartTag' || + node.name === 'TagName' || + node.name === 'JavascriptExpression' || + node.name === 'Scriptlet' || + node.name === 'Output' || + node.name === 'Comment' + ) { + return true + } + node = node.parent + } + + return false +} + +export const fcTemplateHelpers = fcDocumentTemplateHelpers + +export function getFcTemplateHelpers(templateType) { + return templateType === 'emailTemplate' + ? fcEmailTemplateHelpers + : fcDocumentTemplateHelpers +} diff --git a/src/codemirror/fcTemplateLang/tokens.js b/src/codemirror/fcTemplateLang/tokens.js new file mode 100644 index 00000000..afbb5825 --- /dev/null +++ b/src/codemirror/fcTemplateLang/tokens.js @@ -0,0 +1,37 @@ +import { ExternalTokenizer } from '@lezer/lr' +import { JavascriptExpression, Text } from './ejs.parser.terms.js' + +const percent = 37 +const closeAngle = 62 +const dash = 45 +const underscore = 95 +const openAngle = 60 + +export const javascriptExpressionOrText = new ExternalTokenizer( + (input, stack) => { + let { next } = input + const start = input.pos + + while (next !== -1) { + if (next === openAngle && input.peek(1) === percent) break + if ( + ([dash, underscore].includes(next) && + input.peek(1) === percent && + input.peek(2) === closeAngle) || + (next === percent && input.peek(1) === closeAngle) + ) { + break + } + next = input.advance() + } + + if (stack.canShift(JavascriptExpression)) { + // Empty expressions are valid: <% %> + input.acceptToken(JavascriptExpression) + } else if (stack.canShift(Text) && input.pos > start) { + // Never emit empty Text — that loops forever at EOF + input.acceptToken(Text) + } + }, + { contextual: true } +) diff --git a/src/codemirror/gcodeLang/commands.js b/src/codemirror/gcodeLang/commands.js new file mode 100644 index 00000000..2ccbca58 --- /dev/null +++ b/src/codemirror/gcodeLang/commands.js @@ -0,0 +1,102 @@ +/** + * G-code command descriptions for hover tooltips. + * Adapted from gcode-lang-codemirror (MIT): + * https://github.com/oneislandearth/gcode-lang-codemirror + */ +export const GCodeCommands = { + G0: 'Rapid movement', + G1: 'Linear movement', + G2: 'Clockwise arc movement', + G3: 'Counter-clockwise arc movement', + G4: 'Dwell (wait P milliseconds or S seconds)', + G10: 'Retract (firmware retraction)', + G11: 'Recover (firmware retraction)', + G20: 'Set units to inches', + G21: 'Set units to millimeters', + G28: 'Home axes', + G29: 'Automatic bed leveling', + G80: 'Mesh bed leveling (Prusa) / cancel canned cycle', + G90: 'Use absolute positioning', + G91: 'Use relative positioning', + G92: 'Set current position', + M0: 'Stop after the buffer is empty', + M17: 'Enable stepper motors', + M18: 'Disable stepper motors', + M20: 'List SD card files', + M21: 'Initialize (mount) SD card', + M22: 'Release (unmount) SD card', + M23: 'Select SD file for printing', + M24: 'Start / resume SD print', + M25: 'Pause SD print', + M26: 'Set SD position in bytes', + M27: 'Report SD print status', + M28: 'Start writing to SD card', + M29: 'Stop writing to SD card', + M40: 'Eject part', + M42: 'Set pin state / stop if out of material', + M73: 'Set print progress', + M80: 'Turn ATX power on', + M81: 'Turn ATX power off', + M82: 'Extruder absolute mode', + M83: 'Extruder relative mode', + M84: 'Disable idle hold / steppers', + M92: 'Set steps per unit', + M104: 'Set extruder temperature', + M105: 'Get temperatures', + M106: 'Set fan speed', + M107: 'Turn fan off', + M109: 'Set extruder temperature and wait', + M110: 'Set current line number', + M112: 'Emergency stop', + M114: 'Get current position', + M115: 'Get firmware version and capabilities', + M117: 'Set LCD / status message', + M119: 'Get endstop status', + M140: 'Set bed temperature', + M141: 'Set chamber temperature', + M190: 'Set bed temperature and wait', + M191: 'Set chamber temperature and wait', + M220: 'Set feedrate percentage', + M221: 'Set flow percentage', + M300: 'Beep (S Hz for P ms)', + M600: 'Filament change', + M601: 'Pause print (Prusa)', + M862: 'G-code compatibility check (Prusa)', + M900: 'Linear / pressure advance (K)', + T0: 'Select tool / extruder 0', + T1: 'Select tool / extruder 1', + X: 'X axis', + Y: 'Y axis', + Z: 'Z axis', + E: 'Extruder axis', + A: 'A axis', + B: 'B axis', + C: 'C axis', + F: 'Feedrate', + S: 'Speed, temperature, or value', + P: 'Parameter / time', + I: 'Arc X offset / parameter', + J: 'Arc Y offset / parameter', + K: 'Arc Z offset / linear advance', + R: 'Arc radius / RPM' +} + +export function lookupGCodeCommand(word) { + const upper = String(word || '').toUpperCase() + const commandMatch = upper.match(/^([GMT])0*(\d+)(\.\d+)?/) + if (commandMatch) { + const base = commandMatch[1] + String(parseInt(commandMatch[2], 10)) + const canonical = base + (commandMatch[3] || '') + return ( + GCodeCommands[canonical] || + GCodeCommands[base] || + GCodeCommands[upper] || + null + ) + } + const axis = upper.charAt(0) + if (axis && GCodeCommands[axis]) { + return GCodeCommands[axis] + } + return null +} diff --git a/src/codemirror/gcodeLang/index.js b/src/codemirror/gcodeLang/index.js new file mode 100644 index 00000000..09258d3b --- /dev/null +++ b/src/codemirror/gcodeLang/index.js @@ -0,0 +1,109 @@ +import { + StreamLanguage, + LanguageSupport +} from '@codemirror/language' +import { EditorView, hoverTooltip } from '@codemirror/view' +import { lookupGCodeCommand } from './commands.js' + +/** + * G-code language support for CodeMirror 6. + * Highlighting and command tooltips adapted from gcode-lang-codemirror: + * https://github.com/oneislandearth/gcode-lang-codemirror + * + * Implemented as a StreamLanguage so typical 3D-printer G-code (no `%` + * wrappers, E axis, slicer `{placeholders}` / `[placeholders]`) highlights + * without the CodeMirror 0.19 dependencies of the original package. + */ +const wordChar = /[A-Za-z0-9.]/ + +function gcodeToken(stream) { + if (stream.eatSpace()) return null + + if (stream.eat(';')) { + stream.skipToEnd() + return 'comment' + } + + if (stream.eat('(')) { + if (!stream.skipTo(')')) stream.skipToEnd() + else stream.eat(')') + return 'comment' + } + + if (stream.eat('{')) { + if (!stream.skipTo('}')) stream.skipToEnd() + else stream.eat('}') + return 'processingInstruction' + } + + if (stream.eat('[')) { + if (!stream.skipTo(']')) stream.skipToEnd() + else stream.eat(']') + return 'processingInstruction' + } + + if (stream.match(/^[GgMmTt]\d+(\.\d+)?/)) return 'keyword' + if (stream.match(/^[Nn]\d+/)) return 'meta' + if (stream.match(/^\*\d+/)) return 'meta' + if (stream.eat('%')) return 'meta' + + if (stream.match(/^[A-Za-z]/)) return 'strong' + if (stream.match(/^[+-]?(\d+\.?\d*|\.\d+)/)) return 'number' + + stream.next() + return null +} + +export const gcodeLanguage = StreamLanguage.define({ + name: 'gcode', + token: gcodeToken, + languageData: { + commentTokens: { line: ';', block: { open: '(', close: ')' } } + } +}) + +export const gcodeCommandTooltips = hoverTooltip((view, pos, side) => { + const line = view.state.doc.lineAt(pos) + let start = pos + let end = pos + const charAt = (offset) => line.text.charAt(offset - line.from) + + while (start > line.from && wordChar.test(charAt(start - 1))) start-- + while (end < line.to && wordChar.test(charAt(end))) end++ + if ((start === pos && side < 0) || (end === pos && side > 0)) { + return null + } + + const word = line.text.slice(start - line.from, end - line.from) + const description = lookupGCodeCommand(word) + if (!description) return null + + return { + pos: start, + end, + above: true, + create() { + const dom = document.createElement('div') + dom.className = 'cm-tooltip-gcode' + dom.textContent = description + return { dom } + } + } +}) + +const gcodeTooltipTheme = EditorView.theme({ + '.cm-tooltip-gcode': { + padding: '4px 8px', + fontSize: '12px' + } +}) + +export function gcodeLang() { + return new LanguageSupport(gcodeLanguage, [ + gcodeCommandTooltips, + gcodeTooltipTheme + ]) +} + +export { gcodeLang as GCodeLanguage } +export { GCodeCommands, lookupGCodeCommand } from './commands.js' diff --git a/src/codemirror/javascriptLang/index.js b/src/codemirror/javascriptLang/index.js new file mode 100644 index 00000000..57daaa30 --- /dev/null +++ b/src/codemirror/javascriptLang/index.js @@ -0,0 +1,52 @@ +import { + javascript, + javascriptLanguage, + scopeCompletionSource +} from '@codemirror/lang-javascript' +import { nodeJsScope } from './nodeScope.js' +import { typedScopeCompletionSource } from './typedScope.js' + +export function nodeScopeCompletionSource() { + return scopeCompletionSource(nodeJsScope) +} + +/** + * Support extensions for mixed languages that mount + * `javascriptLanguage` without `javascript()` itself (snippets, + * keywords, locals, and Node globals). + * + * Each source must be a separate `languageData` entry. CodeMirror + * treats an `autocomplete` *array* as `completeFromList` options, not + * as multiple completion sources. + */ +export function javascriptCompletionSupport(autoCompleteObject) { + return [ + javascript().support, + javascriptLanguage.data.of({ + autocomplete: scopeCompletionSource(nodeJsScope) + }), + javascriptLanguage.data.of({ + autocomplete: typedScopeCompletionSource(autoCompleteObject) + }) + ] +} + +/** + * JavaScript language support with Node.js globals, snippets, and + * local-variable completion. + * @param {{ autoCompleteObject?: object|string }} [options] + */ +export function javascriptLang(options = {}) { + const { autoCompleteObject } = options + return [ + javascript(), + javascriptLanguage.data.of({ + autocomplete: scopeCompletionSource(nodeJsScope) + }), + javascriptLanguage.data.of({ + autocomplete: typedScopeCompletionSource(autoCompleteObject) + }) + ] +} + +export { nodeJsScope } diff --git a/src/codemirror/javascriptLang/nodeScope.js b/src/codemirror/javascriptLang/nodeScope.js new file mode 100644 index 00000000..fca44de0 --- /dev/null +++ b/src/codemirror/javascriptLang/nodeScope.js @@ -0,0 +1,200 @@ +/** No-op used so Node APIs enumerate as functions in completions. */ +function fn() {} + +const streamLike = { + write: fn, + end: fn, + cork: fn, + uncork: fn, + on: fn, + once: fn, + off: fn, + emit: fn, + pipe: fn, + isTTY: false +} + +const nodeProcess = { + env: {}, + argv: ['node'], + argv0: 'node', + execArgv: [], + execPath: '', + cwd: fn, + chdir: fn, + exit: fn, + exitCode: 0, + nextTick: fn, + pid: 0, + ppid: 0, + platform: 'linux', + arch: 'x64', + version: 'v20.0.0', + versions: { node: '20.0.0' }, + title: 'node', + stdout: streamLike, + stderr: { ...streamLike }, + stdin: { on: fn, once: fn, read: fn, isTTY: false }, + hrtime: Object.assign(fn, { bigint: fn }), + uptime: fn, + memoryUsage: fn, + cpuUsage: fn, + resourceUsage: fn, + abort: fn, + kill: fn, + getuid: fn, + getgid: fn, + setuid: fn, + setgid: fn, + umask: fn, + binding: fn, + dlopen: fn, + features: {}, + config: {}, + release: { name: 'node' } +} + +const BufferStub = Object.assign(fn, { + from: fn, + alloc: fn, + allocUnsafe: fn, + allocUnsafeSlow: fn, + concat: fn, + isBuffer: fn, + isEncoding: fn, + byteLength: fn, + compare: fn, + prototype: {} +}) + +const requireFn = Object.assign(fn, { + resolve: Object.assign(fn, { paths: fn }), + cache: {}, + extensions: {}, + main: {} +}) + +/** + * Scope object for CodeMirror `scopeCompletionSource`. + * Browser-only globals (`window`, `document`, …) are omitted. + */ +export function createNodeJsScope() { + const scope = { + process: nodeProcess, + Buffer: BufferStub, + require: requireFn, + module: { + exports: {}, + require: requireFn, + id: '.', + filename: '', + dirname: '', + paths: [], + loaded: false, + children: [], + parent: null + }, + exports: {}, + __dirname: '', + __filename: '', + console, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + setImmediate: fn, + clearImmediate: fn, + queueMicrotask, + fetch, + URL, + URLSearchParams, + TextEncoder, + TextDecoder, + AbortController, + AbortSignal, + Event, + EventTarget, + Request, + Response, + Headers, + FormData, + Blob, + structuredClone, + performance, + crypto, + atob, + btoa, + WebAssembly, + Promise, + Map, + Set, + WeakMap, + WeakSet, + Array, + Object, + String, + Number, + Boolean, + Symbol, + BigInt, + Date, + RegExp, + Error, + TypeError, + RangeError, + SyntaxError, + URIError, + EvalError, + ReferenceError, + JSON, + Math, + Intl, + Proxy, + Reflect, + ArrayBuffer, + DataView, + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, + Infinity, + NaN, + parseInt, + parseFloat, + isNaN, + isFinite, + encodeURI, + decodeURI, + encodeURIComponent, + decodeURIComponent, + eval, + Function + } + + if (typeof File !== 'undefined') scope.File = File + if (typeof globalThis.WeakRef !== 'undefined') { + scope.WeakRef = globalThis.WeakRef + } + if (typeof globalThis.FinalizationRegistry !== 'undefined') { + scope.FinalizationRegistry = globalThis.FinalizationRegistry + } + if (typeof globalThis.AggregateError !== 'undefined') { + scope.AggregateError = globalThis.AggregateError + } + if (typeof SharedArrayBuffer !== 'undefined') { + scope.SharedArrayBuffer = SharedArrayBuffer + } + + scope.global = scope + scope.globalThis = scope + return scope +} + +export const nodeJsScope = createNodeJsScope() diff --git a/src/codemirror/javascriptLang/typedScope.js b/src/codemirror/javascriptLang/typedScope.js new file mode 100644 index 00000000..0f01681c --- /dev/null +++ b/src/codemirror/javascriptLang/typedScope.js @@ -0,0 +1,319 @@ +import { completionPath } from '@codemirror/lang-javascript' + +const IDENTIFIER = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/ +const ISO_DATE_RE = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/ +const OBJECT_ID_RE = /^[a-fA-F0-9]{24}$/ +const MAX_STRING_LENGTH = 80 + +function fn(detail) { + return { $type: 'function', detail } +} + +const STRING_MEMBERS = { + length: { $type: 'number' }, + charAt: fn('(index) => string'), + includes: fn('(search) => boolean'), + indexOf: fn('(search) => number'), + padStart: fn('(length, fill?) => string'), + replace: fn('(pattern, replacement) => string'), + slice: fn('(start, end?) => string'), + split: fn('(separator) => string[]'), + startsWith: fn('(search) => boolean'), + endsWith: fn('(search) => boolean'), + substring: fn('(start, end?) => string'), + toLowerCase: fn('() => string'), + toUpperCase: fn('() => string'), + trim: fn('() => string') +} + +const NUMBER_MEMBERS = { + toFixed: fn('(digits?) => string'), + toString: fn('() => string'), + valueOf: fn('() => number') +} + +const DATE_MEMBERS = { + getDate: fn('() => number'), + getDay: fn('() => number'), + getFullYear: fn('() => number'), + getHours: fn('() => number'), + getMinutes: fn('() => number'), + getMonth: fn('() => number'), + getSeconds: fn('() => number'), + getTime: fn('() => number'), + toISOString: fn('() => string'), + toLocaleDateString: fn('() => string'), + toLocaleString: fn('() => string'), + toString: fn('() => string'), + valueOf: fn('() => number') +} + +const ARRAY_MEMBERS = { + length: { $type: 'number' }, + at: fn('(index) => any'), + concat: fn('(...items) => any[]'), + every: fn('(predicate) => boolean'), + filter: fn('(predicate) => any[]'), + find: fn('(predicate) => any'), + forEach: fn('(callback) => void'), + includes: fn('(value) => boolean'), + join: fn('(separator?) => string'), + map: fn('(callback) => any[]'), + reduce: fn('(callback, initial?) => any'), + slice: fn('(start, end?) => any[]'), + some: fn('(predicate) => boolean') +} + +const BOOLEAN_MEMBERS = { + toString: fn('() => string'), + valueOf: fn('() => boolean') +} + +export function isTypedNode(value) { + return ( + value != null && + typeof value === 'object' && + !Array.isArray(value) && + typeof value.$type === 'string' + ) +} + +function inferStringType(value, key) { + const truncated = + value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) : value + if (ISO_DATE_RE.test(value)) { + return { $type: 'Date', value: truncated } + } + if ( + (key === '_id' || key === 'id' || /Id$/.test(String(key || ''))) && + OBJECT_ID_RE.test(value) + ) { + return { $type: 'ObjectId', value: truncated } + } + return { $type: 'string', value: truncated } +} + +export function inferTypedNode(value, key = null, depth = 0, seen = new WeakSet()) { + if (isTypedNode(value)) { + return value + } + if (value === undefined) { + return undefined + } + if (value === null) { + return { $type: 'null' } + } + const valueType = typeof value + if (valueType === 'string') { + return inferStringType(value, key) + } + if (valueType === 'number') { + return { $type: 'number', value } + } + if (valueType === 'boolean') { + return { $type: 'boolean', value } + } + if (valueType === 'function') { + return { $type: 'function', detail: 'fn' } + } + if (value instanceof Date) { + return { $type: 'Date', value: value.toISOString() } + } + if (valueType !== 'object') { + return { $type: 'any' } + } + if (seen.has(value) || depth > 6) { + return Array.isArray(value) + ? { $type: 'array', element: { $type: 'any' } } + : { $type: 'object', properties: {} } + } + seen.add(value) + if (Array.isArray(value)) { + let element = null + for (const item of value.slice(0, 3)) { + const typed = inferTypedNode(item, null, depth + 1, seen) + if (typed && typed.$type !== 'null') { + element = typed + break + } + } + return { $type: 'array', element: element || { $type: 'any' } } + } + const properties = {} + for (const [nestedKey, nested] of Object.entries(value)) { + const typed = inferTypedNode(nested, nestedKey, depth + 1, seen) + if (typed) { + properties[nestedKey] = typed + } + } + return { $type: 'object', properties } +} + +export function normalizeTypedScope(autoCompleteObject) { + let data = autoCompleteObject + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + data = {} + } + } + if (isTypedNode(data) && data.$type === 'object') { + return { + $type: 'object', + ...(data.name ? { name: data.name } : {}), + properties: { ...(data.properties || {}) } + } + } + if (data != null && typeof data === 'object' && !Array.isArray(data)) { + const values = Object.values(data) + if ( + values.length > 0 && + values.every((value) => isTypedNode(value)) + ) { + return { $type: 'object', properties: { ...data } } + } + return inferTypedNode(data) + } + return { $type: 'object', properties: {} } +} + +export function typeLabel(node) { + if (!isTypedNode(node)) { + return 'any' + } + switch (node.$type) { + case 'array': + return `${typeLabel(node.element)}[]` + case 'object': + return node.name || 'object' + case 'ObjectId': + return node.name ? `ObjectId<${node.name}>` : 'ObjectId' + case 'function': + return node.detail || 'function' + case 'null': + return 'null' + default: + return node.$type + } +} + +function builtinMembers(node) { + switch (node?.$type) { + case 'string': + case 'ObjectId': + return STRING_MEMBERS + case 'number': + return NUMBER_MEMBERS + case 'Date': + return DATE_MEMBERS + case 'array': + return ARRAY_MEMBERS + case 'boolean': + return BOOLEAN_MEMBERS + default: + return null + } +} + +function resolveTypedPath(root, path) { + let node = root + for (const step of path) { + if (!isTypedNode(node)) { + return null + } + if (node.$type === 'object') { + node = node.properties?.[step] + continue + } + if (node.$type === 'array' && step === 'length') { + node = { $type: 'number' } + continue + } + const members = builtinMembers(node) + if (members?.[step]) { + node = members[step] + continue + } + return null + } + return isTypedNode(node) ? node : null +} + +function completionType(node, topLevel) { + if (node?.$type === 'function') { + return topLevel ? 'function' : 'method' + } + return topLevel ? 'variable' : 'property' +} + +function enumerateTypedCompletions(node, topLevel) { + if (!isTypedNode(node)) { + return [] + } + const options = [] + const seen = new Set() + if (node.$type === 'object') { + for (const [name, child] of Object.entries(node.properties || {})) { + if (!IDENTIFIER.test(name) || seen.has(name)) { + continue + } + seen.add(name) + options.push({ + label: name, + type: completionType(child, topLevel), + detail: typeLabel(child), + boost: 1 + }) + } + } + const members = builtinMembers(node) + if (members) { + for (const [name, child] of Object.entries(members)) { + if (!IDENTIFIER.test(name) || seen.has(name)) { + continue + } + seen.add(name) + options.push({ + label: name, + type: completionType(child, false), + detail: typeLabel(child), + boost: 0 + }) + } + } + return options +} + +/** + * Completes template locals from a typed intellisense tree, showing + * JS/mongoose types in the dropdown detail column. + */ +export function typedScopeCompletionSource(autoCompleteObject) { + const scope = normalizeTypedScope(autoCompleteObject) + const cache = new Map() + return (context) => { + const path = completionPath(context) + if (!path) { + return null + } + const target = path.path.length === 0 ? scope : resolveTypedPath(scope, path.path) + if (!target) { + return null + } + let options = cache.get(target) + if (!options) { + options = enumerateTypedCompletions(target, path.path.length === 0) + cache.set(target, options) + } + if (options.length === 0) { + return null + } + return { + from: context.pos - path.name.length, + options, + validFor: IDENTIFIER + } + } +} diff --git a/src/components/Dashboard/Finance/FinanceOverview.jsx b/src/components/Dashboard/Finance/FinanceOverview.jsx index 60f7089d..8fa6e20c 100644 --- a/src/components/Dashboard/Finance/FinanceOverview.jsx +++ b/src/components/Dashboard/Finance/FinanceOverview.jsx @@ -1,72 +1,175 @@ -import { Flex } from 'antd' -import useCollapseState from '../hooks/useCollapseState' +import DashboardOverviewPage from '../common/DashboardOverviewPage' import StatsDisplay from '../common/StatsDisplay' -import InfoCollapse from '../common/InfoCollapse' -import ScrollBox from '../common/ScrollBox' +import ModelHistoryDisplay from '../common/ModelHistoryDisplay' +import ObjectTable from '../common/ObjectTable' + +const collapseDefaults = { + invoiceStats: true, + paymentStats: true, + taxRecordStats: true, + invoiceCountHistory: true, + invoiceAmountHistory: true, + paymentCountHistory: true, + paymentAmountHistory: true, + taxRecordCountHistory: true, + taxRecordAmountHistory: true, + overdueInvoices: true, + partiallyPaidInvoices: true, + dueInvoices: true, + authorisedPayments: true +} + +const sections = [ + { + key: 'invoiceStats', + title: 'Invoice Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'paymentStats', + title: 'Payment Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'taxRecordStats', + title: 'Tax Record Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'invoiceHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'invoiceCountHistory', + title: 'Invoice Count History', + content: ( + + ) + } + ], + [ + { + key: 'invoiceAmountHistory', + title: 'Invoice Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'paymentHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'paymentCountHistory', + title: 'Payment Count History', + content: ( + + ) + } + ], + [ + { + key: 'paymentAmountHistory', + title: 'Payment Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'taxRecordHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'taxRecordCountHistory', + title: 'Tax Record Count History', + content: ( + + ) + } + ], + [ + { + key: 'taxRecordAmountHistory', + title: 'Tax Record Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'overdueAndPartiallyPaidInvoicesRow', + type: 'row', + columns: [ + [ + { + key: 'overdueInvoices', + title: 'Overdue Invoices', + content: ( + + ) + } + ], + [ + { + key: 'partiallyPaidInvoices', + title: 'Partially Paid Invoices', + content: ( + + ) + } + ] + ] + }, + { + key: 'dueInvoicesAndAuthorisedPaymentsRow', + type: 'row', + columns: [ + [ + { + key: 'dueInvoices', + title: 'Due Invoices', + content: ( + + ) + } + ], + [ + { + key: 'authorisedPayments', + title: 'Authorised Payments', + content: ( + + ) + } + ] + ] + } +] const FinanceOverview = () => { - const [collapseState, updateCollapseState] = useCollapseState( - 'FinanceOverview', - { - invoiceStats: true, - paymentStats: true - } - ) - return ( - - - - - updateCollapseState('invoiceStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='invoiceStats' - canCollapse={false} - > - - - - - - updateCollapseState('paymentStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='paymentStats' - canCollapse={false} - > - - - - - - - + ) } diff --git a/src/components/Dashboard/Finance/Invoices.jsx b/src/components/Dashboard/Finance/Invoices.jsx index b6366fc4..071d6d05 100644 --- a/src/components/Dashboard/Finance/Invoices.jsx +++ b/src/components/Dashboard/Finance/Invoices.jsx @@ -1,60 +1,47 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Dropdown, Modal } from 'antd' -import NewInvoice from './Invoices/NewInvoice' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Invoices = () => { - const [newInvoiceOpen, setNewInvoiceOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('invoices') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('invoices') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Invoices') - const actionItems = { - items: [ - { - label: 'New Invoice', - key: 'newInvoice', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newInvoice') { - setNewInvoiceOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Invoices') return ( - <> + - + - - - + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> - + + + { useSortInUrl={true} /> - { - setNewInvoiceOpen(false) - }} - destroyOnHidden={true} - > - { - setNewInvoiceOpen(false) - tableRef.current?.reload() - }} - reset={newInvoiceOpen} - /> - - + ) } diff --git a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx index b5ae8255..6b6023c8 100644 --- a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx +++ b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -20,16 +20,14 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' -import { - getModelByName, - getModelProperty -} from '../../../../database/ObjectModels.js' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx' import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' @@ -41,7 +39,6 @@ const InvoiceInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -65,9 +62,23 @@ const InvoiceInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, invoiceId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -81,223 +92,217 @@ const InvoiceInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, } - const editDisabled = - getModelByName('invoice') - ?.actions?.find((action) => action.name === 'edit') - ?.disabled(objectFormState.objectData) ?? false - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='invoice' - labelWidth='225px' - objectData={objectData} - visibleProperties={{ - invoiceOrderItems: false, - invoiceShipments: false - }} - /> - - } - active={collapseState.invoiceOrderItems} - onToggle={(expanded) => - updateCollapseState('invoiceOrderItems', expanded) - } - collapseKey='invoiceOrderItems' - > - - - } - active={collapseState.invoiceShipments} - onToggle={(expanded) => - updateCollapseState('invoiceShipments', expanded) - } - collapseKey='invoiceShipments' - > - - - - )} - - - } - active={collapseState.payments} - onToggle={(expanded) => updateCollapseState('payments', expanded)} - collapseKey='payments' - > - {objectFormState.loading ? ( - - ) : ( - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='invoice' + labelWidth='225px' + objectData={objectData} + visibleProperties={{ + invoiceOrderItems: false, + invoiceShipments: false + }} + /> + + } + active={collapseState.invoiceOrderItems} + onToggle={(expanded) => + updateCollapseState('invoiceOrderItems', expanded) + } + collapseKey='invoiceOrderItems' + > + + + } + active={collapseState.invoiceShipments} + onToggle={(expanded) => + updateCollapseState('invoiceShipments', expanded) + } + collapseKey='invoiceShipments' + > + + + )} - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + + + } + active={collapseState.payments} + onToggle={(expanded) => updateCollapseState('payments', expanded)} + collapseKey='payments' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Finance/PaymentPolicies.jsx b/src/components/Dashboard/Finance/PaymentPolicies.jsx new file mode 100644 index 00000000..64461393 --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies.jsx @@ -0,0 +1,92 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const PaymentPolicies = () => { + const tableRef = useRef() + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('paymentPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('PaymentPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PaymentPolicies') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default PaymentPolicies diff --git a/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx b/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx new file mode 100644 index 00000000..86627d92 --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewPaymentPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewPaymentPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewPaymentPolicy diff --git a/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx b/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx new file mode 100644 index 00000000..bbd4472d --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx @@ -0,0 +1,264 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('PaymentPolicyInfo') +log.setLevel(config.logLevel) + +const PaymentPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const paymentPolicyId = new URLSearchParams(location.search).get( + 'paymentPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'PaymentPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, paymentPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default PaymentPolicyInfo diff --git a/src/components/Dashboard/Finance/Payments.jsx b/src/components/Dashboard/Finance/Payments.jsx index 03ada21b..847e8a6c 100644 --- a/src/components/Dashboard/Finance/Payments.jsx +++ b/src/components/Dashboard/Finance/Payments.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Dropdown, Modal } from 'antd' -import NewPayment from './Payments/NewPayment' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Payments = () => { - const [newPaymentOpen, setNewPaymentOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('payments') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('payments') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Payments') - const actionItems = { - items: [ - { - label: 'New Payment', - key: 'newPayment', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPayment') { - setNewPaymentOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Payments') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewPaymentOpen(false) - }} - destroyOnHidden={true} - > - { - setNewPaymentOpen(false) - tableRef.current?.reload() - }} - reset={newPaymentOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Finance/Payments/PaymentInfo.jsx b/src/components/Dashboard/Finance/Payments/PaymentInfo.jsx index 25322640..e5cee399 100644 --- a/src/components/Dashboard/Finance/Payments/PaymentInfo.jsx +++ b/src/components/Dashboard/Finance/Payments/PaymentInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,13 +18,14 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' -import ScrollBox from '../../common/ScrollBox.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('PaymentInfo') log.setLevel(config.logLevel) @@ -34,7 +35,6 @@ const PaymentInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -55,9 +55,23 @@ const PaymentInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, paymentId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -71,165 +85,156 @@ const PaymentInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, } - const editDisabled = - getModelByName('payment') - ?.actions?.find((action) => action.name === 'edit') - ?.disabled(objectFormState.objectData) ?? false - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='payment' - labelWidth='225px' - objectData={objectData} - /> - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='payment' + labelWidth='225px' + objectData={objectData} + /> + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Finance/TaxRecords.jsx b/src/components/Dashboard/Finance/TaxRecords.jsx index 56ceea89..449b9580 100644 --- a/src/components/Dashboard/Finance/TaxRecords.jsx +++ b/src/components/Dashboard/Finance/TaxRecords.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewTaxRecord from './TaxRecords/NewTaxRecord' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const TaxRecords = () => { - const [newTaxRecordOpen, setNewTaxRecordOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('taxRecord') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('taxRecord') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('TaxRecords') - const actionItems = { - items: [ - { - label: 'New Tax Record', - key: 'newTaxRecord', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newTaxRecord') { - setNewTaxRecordOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('TaxRecords') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateCollapseState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewTaxRecordOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewTaxRecordOpen(false) - tableRef.current?.reload() - }} - reset={!newTaxRecordOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Finance/TaxRecords/TaxRecordInfo.jsx b/src/components/Dashboard/Finance/TaxRecords/TaxRecordInfo.jsx index b1c19c6f..36c8e145 100644 --- a/src/components/Dashboard/Finance/TaxRecords/TaxRecordInfo.jsx +++ b/src/components/Dashboard/Finance/TaxRecords/TaxRecordInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('TaxRecordInfo') @@ -45,9 +48,23 @@ const TaxRecordInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, taxRecordId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -61,10 +78,6 @@ const TaxRecordInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -79,6 +92,7 @@ const TaxRecordInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -178,9 +188,14 @@ const TaxRecordInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index 91460c98..b7d39ae8 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -1,65 +1,55 @@ // src/filamentStocks.js -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewFilamentStock from './FilamentStocks/NewFilamentStock' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const FilamentStocks = () => { const tableRef = useRef() - const [newFilamentStockOpen, setNewFilamentStockOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('filamentStocks') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('filamentStocks') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('FilamentStocks') - const actionItems = { - items: [ - { - label: 'New Filament Stock', - key: 'newFilamentStock', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newFilamentStock') { - setNewFilamentStockOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('FilamentStocks') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewFilamentStockOpen(false) - }} - destroyOnHidden={true} - > - { - setNewFilamentStockOpen(false) - tableRef.current?.reload() - }} - reset={newFilamentStockOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx index ce5650f5..2081d4dd 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx @@ -1,11 +1,13 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import ObjectForm from '../../common/ObjectForm' import ObjectInfo from '../../common/ObjectInfo' import ObjectTable from '../../common/ObjectTable' import ObjectActions from '../../common/ObjectActions' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ViewButton from '../../common/ViewButton' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' @@ -19,9 +21,10 @@ import NoteIcon from '../../../Icons/NoteIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon' import GraphIcon from '../../../Icons/GraphIcon' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder' -import DocumentPrintButton from '../../common/DocumentPrintButton' +import InfoActionButtons from '../../common/InfoActionButtons' import UserNotifierToggle from '../../common/UserNotifierToggle' import ScrollBox from '../../common/ScrollBox' +import { getModelByName } from '../../../../database/ObjectModels.js' import HistoryDisplay from '../../common/HistoryDisplay' const FilamentStockInfo = () => { @@ -47,9 +50,23 @@ const FilamentStockInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, filamentStockId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -77,6 +94,7 @@ const FilamentStockInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { }} editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} - disabled={true} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } loading={objectFormState.editLoading} /> - @@ -181,7 +197,7 @@ const FilamentStockInfo = () => { ) : ( )} @@ -211,9 +227,14 @@ const FilamentStockInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/FilamentStocks/NewFilamentStock.jsx b/src/components/Dashboard/Inventory/FilamentStocks/NewFilamentStock.jsx index 0cb500be..d9a4677f 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/NewFilamentStock.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/NewFilamentStock.jsx @@ -8,7 +8,7 @@ const NewFilamentStock = ({ onOk, reset, defaultValues }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ @@ -38,7 +38,8 @@ const NewFilamentStock = ({ onOk, reset, defaultValues }) => { _id: false, _reference: false, createdAt: false, - updatedAt: false + updatedAt: false, + postedAt: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/FilamentStocks/PostFilamentStock.jsx b/src/components/Dashboard/Inventory/FilamentStocks/PostFilamentStock.jsx new file mode 100644 index 00000000..e9cf7ec7 --- /dev/null +++ b/src/components/Dashboard/Inventory/FilamentStocks/PostFilamentStock.jsx @@ -0,0 +1,46 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const PostFilamentStock = ({ onOk, objectData }) => { + const [postLoading, setPostLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handlePost = async () => { + setPostLoading(true) + try { + const result = await sendObjectFunction( + objectData._id, + 'FilamentStock', + 'post' + ) + if (result) { + message.success('Filament stock posted successfully') + onOk(result) + } + } catch (error) { + console.error('Error posting filament stock:', error) + } finally { + setPostLoading(false) + } + } + + return ( + + ) +} + +PostFilamentStock.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default PostFilamentStock diff --git a/src/components/Dashboard/Inventory/InventoryOverview.jsx b/src/components/Dashboard/Inventory/InventoryOverview.jsx index 2edbe6a1..ddde526b 100644 --- a/src/components/Dashboard/Inventory/InventoryOverview.jsx +++ b/src/components/Dashboard/Inventory/InventoryOverview.jsx @@ -1,115 +1,373 @@ -import { Flex } from 'antd' -import useCollapseState from '../hooks/useCollapseState' +import DashboardOverviewPage from '../common/DashboardOverviewPage' import StatsDisplay from '../common/StatsDisplay' -import InfoCollapse from '../common/InfoCollapse' -import ScrollBox from '../common/ScrollBox' +import ModelHistoryDisplay from '../common/ModelHistoryDisplay' +import ObjectTable from '../common/ObjectTable' + +const collapseDefaults = { + partStockStats: true, + filamentStockStats: true, + productStockStats: true, + purchaseOrderStats: true, + stockEventStats: true, + stockTransferStats: true, + stockLocationStats: true, + shipmentStats: true, + partStockCountHistory: true, + partStockValueHistory: true, + newPartStocks: true, + filamentStockCountHistory: true, + filamentStockValueHistory: true, + unconsumedFilamentStocks: true, + productStockCountHistory: true, + productStockValueHistory: true, + draftProductStocks: true, + purchaseOrderCountHistory: true, + purchaseOrderAmountHistory: true, + draftPurchaseOrders: true, + stockEventHistory: true, + stockTransferHistory: true, + draftStockTransfers: true, + sentPurchaseOrders: true, + usedPartStocks: true, + plannedShipments: true, + shippedInboundShipments: true +} + +const sections = [ + { + key: 'partStockStats', + title: 'Part Stock Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'filamentStockStats', + title: 'Filament Stock Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'productStockStats', + title: 'Product Stock Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'purchaseOrderStats', + title: 'Purchase Order Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'stockEventStats', + title: 'Stock Event Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'stockTransferStats', + title: 'Stock Transfer Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'stockLocationStats', + title: 'Stock Location Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'shipmentStats', + title: 'Shipment Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'partStockHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'partStockCountHistory', + title: 'Part Stock Count History', + content: ( + + ) + } + ], + [ + { + key: 'partStockValueHistory', + title: 'Part Stock Quantity History', + content: ( + + ) + } + ] + ] + }, + { + key: 'filamentStockHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'filamentStockCountHistory', + title: 'Filament Stock Count History', + content: ( + + ) + } + ], + [ + { + key: 'filamentStockValueHistory', + title: 'Filament Stock Weight History', + content: ( + + ) + } + ] + ] + }, + { + key: 'newPartAndUnconsumedFilamentRow', + type: 'row', + columns: [ + [ + { + key: 'newPartStocks', + title: 'New Part Stocks', + content: ( + + ) + } + ], + [ + { + key: 'unconsumedFilamentStocks', + title: 'Unconsumed Filament Stocks', + content: ( + + ) + } + ] + ] + }, + { + key: 'productStockHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'productStockCountHistory', + title: 'Product Stock Count History', + content: ( + + ) + } + ], + [ + { + key: 'productStockValueHistory', + title: 'Product Stock Quantity History', + content: ( + + ) + } + ] + ] + }, + { + key: 'purchaseOrderHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'purchaseOrderCountHistory', + title: 'Purchase Order Count History', + content: ( + + ) + } + ], + [ + { + key: 'purchaseOrderAmountHistory', + title: 'Purchase Order Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'draftProductAndPurchaseOrderRow', + type: 'row', + columns: [ + [ + { + key: 'draftProductStocks', + title: 'Draft Product Stocks', + content: ( + + ) + } + ], + [ + { + key: 'draftPurchaseOrders', + title: 'Draft Purchase Orders', + content: ( + + ) + } + ] + ] + }, + { + key: 'stockEventAndTransferHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'stockEventHistory', + title: 'Stock Event History', + content: ( + + ) + } + ], + [ + { + key: 'stockTransferHistory', + title: 'Stock Transfer History', + content: + } + ] + ] + }, + { + key: 'draftTransfersAndSentPurchaseOrdersRow', + type: 'row', + columns: [ + [ + { + key: 'draftStockTransfers', + title: 'Draft Stock Transfers', + content: ( + + ) + } + ], + [ + { + key: 'sentPurchaseOrders', + title: 'Sent Purchase Orders', + content: ( + + ) + } + ] + ] + }, + { + key: 'usedPartsAndPlannedShipmentsRow', + type: 'row', + columns: [ + [ + { + key: 'usedPartStocks', + title: 'Used Part Stocks', + content: ( + + ) + } + ], + [ + { + key: 'plannedShipments', + title: 'Planned Shipments', + content: ( + + ) + } + ] + ] + }, + { + key: 'shippedInboundShipmentsRow', + type: 'row', + columns: [ + [ + { + key: 'shippedInboundShipments', + title: 'Inbound Shipments In Transit', + content: ( + + ) + } + ] + ] + } +] const InventoryOverview = () => { - const [collapseState, updateCollapseState] = useCollapseState( - 'InventoryOverview', - { - inventoryStats: true, - purchaseOrderStats: true - } - ) - return ( - - - - - updateCollapseState('inventoryStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='inventoryStats' - canCollapse={false} - > - - - - - - - - - - updateCollapseState('purchaseOrderStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='purchaseOrderStats' - > - - - - - - - - - updateCollapseState('partStockHistory', isActive) - } - collapseKey='partStockHistory' - canCollapse={false} - > - - - - updateCollapseState('filamentStockHistory', isActive) - } - canCollapse={false} - collapseKey='filamentStockHistory' - > - - - - updateCollapseState('stockEventHistory', isActive) - } - canCollapse={false} - collapseKey='stockEventHistory' - > - - - - - + ) } diff --git a/src/components/Dashboard/Inventory/OrderItems.jsx b/src/components/Dashboard/Inventory/OrderItems.jsx index a1cb772c..6719b969 100644 --- a/src/components/Dashboard/Inventory/OrderItems.jsx +++ b/src/components/Dashboard/Inventory/OrderItems.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewOrderItem from './OrderItems/NewOrderItem' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const OrderItems = () => { - const [newOrderItemOpen, setNewOrderItemOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('orderItems') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('orderItems') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('OrderItems') - const actionItems = { - items: [ - { - label: 'New Order Item', - key: 'newOrderItem', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newOrderItem') { - setNewOrderItemOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('OrderItems') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewOrderItemOpen(false) - }} - destroyOnHidden={true} - > - { - setNewOrderItemOpen(false) - tableRef.current?.reload() - }} - reset={newOrderItemOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/OrderItems/OrderItemInfo.jsx b/src/components/Dashboard/Inventory/OrderItems/OrderItemInfo.jsx index d548aa16..62acec34 100644 --- a/src/components/Dashboard/Inventory/OrderItems/OrderItemInfo.jsx +++ b/src/components/Dashboard/Inventory/OrderItems/OrderItemInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,9 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('OrderItemInfo') @@ -46,9 +49,23 @@ const OrderItemInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, orderItemId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -62,10 +79,6 @@ const OrderItemInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -83,6 +96,7 @@ const OrderItemInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> + { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -184,9 +199,14 @@ const OrderItemInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/PartStocks.jsx b/src/components/Dashboard/Inventory/PartStocks.jsx index 32f675b8..4e38664f 100644 --- a/src/components/Dashboard/Inventory/PartStocks.jsx +++ b/src/components/Dashboard/Inventory/PartStocks.jsx @@ -1,65 +1,55 @@ // src/partStocks.js -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewPartStock from './PartStocks/NewPartStock' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const PartStocks = () => { const tableRef = useRef() - const [newPartStockOpen, setNewPartStockOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('partStocks') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('partStocks') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('PartStocks') - const actionItems = { - items: [ - { - label: 'New Part Stock', - key: 'newPartStock', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPartStock') { - setNewPartStockOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PartStocks') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewPartStockOpen(false) - }} - destroyOnHidden={true} - > - { - setNewPartStockOpen(false) - tableRef.current?.reload() - }} - reset={newPartStockOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx b/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx index 4a94467b..a40dc583 100644 --- a/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx +++ b/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx @@ -8,7 +8,7 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ @@ -38,7 +38,8 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => { _id: false, _reference: false, createdAt: false, - updatedAt: false + updatedAt: false, + postedAt: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx b/src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx index 810502ab..c48b0d8d 100644 --- a/src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx +++ b/src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -19,12 +19,14 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import HistoryDisplay from '../../common/HistoryDisplay.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import PartStockIcon from '../../../Icons/PartStockIcon.jsx' const log = loglevel.getLogger('PartStockInfo') @@ -52,9 +54,23 @@ const PartStockInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, partStockId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -85,6 +101,7 @@ const PartStockInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -201,7 +214,7 @@ const PartStockInfo = () => { ) : ( )} @@ -233,9 +246,14 @@ const PartStockInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx b/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx new file mode 100644 index 00000000..92bf9fde --- /dev/null +++ b/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx @@ -0,0 +1,42 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const PostPartStock = ({ onOk, objectData }) => { + const [postLoading, setPostLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handlePost = async () => { + setPostLoading(true) + try { + const result = await sendObjectFunction(objectData._id, 'PartStock', 'post') + if (result) { + message.success('Part stock posted successfully') + onOk(result) + } + } catch (error) { + console.error('Error posting part stock:', error) + } finally { + setPostLoading(false) + } + } + + return ( + + ) +} + +PostPartStock.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default PostPartStock diff --git a/src/components/Dashboard/Inventory/ProductStocks.jsx b/src/components/Dashboard/Inventory/ProductStocks.jsx index 649541c1..cdf24e28 100644 --- a/src/components/Dashboard/Inventory/ProductStocks.jsx +++ b/src/components/Dashboard/Inventory/ProductStocks.jsx @@ -1,66 +1,56 @@ // src/components/Dashboard/Inventory/ProductStocks.jsx // ProductStocks - tracks assembled products consisting of part stocks -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewProductStock from './ProductStocks/NewProductStock' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const ProductStocks = () => { const tableRef = useRef() - const [newProductStockOpen, setNewProductStockOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('productStocks') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('productStock') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('ProductStocks') - const actionItems = { - items: [ - { - label: 'New Product Stock', - key: 'newProductStock', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newProductStock') { - setNewProductStockOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ProductStocks') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewProductStockOpen(false) - }} - destroyOnHidden={true} - > - { - setNewProductStockOpen(false) - tableRef.current?.reload() - }} - reset={newProductStockOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx index d5906af2..2a18c96a 100644 --- a/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx +++ b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx @@ -24,7 +24,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => { required={true} objectData={objectData} visibleProperties={{ - partStocks: false + partStockList: false }} /> ) @@ -42,7 +42,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => { _reference: false, createdAt: false, updatedAt: false, - partStocks: false + partStockList: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx index d0316827..0fd450cf 100644 --- a/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx +++ b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -26,10 +26,11 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import HistoryDisplay from '../../common/HistoryDisplay.jsx' @@ -42,7 +43,6 @@ const ProductStockInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -57,7 +57,7 @@ const ProductStockInfo = () => { 'ProductStockInfo', { info: true, - partStocks: true, + partStockList: true, events: true, history: true, notes: true, @@ -71,9 +71,23 @@ const ProductStockInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, productStockId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -86,226 +100,221 @@ const ProductStockInfo = () => { finishEdit: () => { objectFormRef?.current.handleUpdate() return true - }, - delete: () => { - objectFormRef?.current.handleDelete?.() - return true - }, + } } - const editDisabled = - getModelByName('productStock') - ?.actions?.find((action) => action.name === 'edit') - ?.disabled(objectFormState.objectData) ?? false - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='productStock' + objectData={objectData} + labelWidth='175px' + visibleProperties={{ partStockList: false }} + /> + + } + active={collapseState.partStockList} + onToggle={(expanded) => + updateCollapseState('partStockList', expanded) + } + collapseKey='partStockList' + > + + + + )} + + + + } + active={collapseState.events} + onToggle={(expanded) => updateCollapseState('events', expanded)} + collapseKey='events' + > + {objectFormState.loading ? ( + + ) : ( + - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='productStock' - objectData={objectData} - labelWidth='175px' - visibleProperties={{ partStocks: false }} - /> - - } - active={collapseState.partStocks} - onToggle={(expanded) => - updateCollapseState('partStocks', expanded) - } - collapseKey='partStocks' - > - - - - )} - - + visibleColumns={{ parent: false }} + /> + )} + - } - active={collapseState.events} - onToggle={(expanded) => updateCollapseState('events', expanded)} - collapseKey='events' - > - {objectFormState.loading ? ( - - ) : ( - - )} - + } + active={collapseState.history} + onToggle={(expanded) => updateCollapseState('history', expanded)} + collapseKey='history' + > + {objectFormState.loading ? ( + + ) : ( + + )} + - } - active={collapseState.history} - onToggle={(expanded) => updateCollapseState('history', expanded)} - collapseKey='history' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Inventory/PurchaseOrders.jsx b/src/components/Dashboard/Inventory/PurchaseOrders.jsx index 2cbff6ea..2a0fe2b3 100644 --- a/src/components/Dashboard/Inventory/PurchaseOrders.jsx +++ b/src/components/Dashboard/Inventory/PurchaseOrders.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewPurchaseOrder from './PurchaseOrders/NewPurchaseOrder' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const PurchaseOrders = () => { - const [newPurchaseOrderOpen, setNewPurchaseOrderOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('purchaseOrders') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('purchaseOrders') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('PurchaseOrders') - const actionItems = { - items: [ - { - label: 'New Purchase Order', - key: 'newPurchaseOrder', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPurchaseOrder') { - setNewPurchaseOrderOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PurchaseOrders') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewPurchaseOrderOpen(false) - }} - destroyOnHidden={true} - > - { - setNewPurchaseOrderOpen(false) - tableRef.current?.reload() - }} - reset={newPurchaseOrderOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx b/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx index e82da872..fc3fc4ce 100644 --- a/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx +++ b/src/components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,17 +18,18 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' import InvoiceIcon from '../../../Icons/InvoiceIcon.jsx' -import StockEventIcon from '../../../Icons/StockEventIcon.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' +import StockEventIcon from '../../../Icons/StockEventIcon.jsx' const log = loglevel.getLogger('PurchaseOrderInfo') log.setLevel(config.logLevel) @@ -40,7 +41,6 @@ const PurchaseOrderInfo = () => { const shipmentsTableRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -70,9 +70,23 @@ const PurchaseOrderInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, purchaseOrderId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { orderItemsTableRef?.current?.startEditing?.() @@ -89,251 +103,246 @@ const PurchaseOrderInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, } - const editDisabled = getModelByName('purchaseOrder') - .actions.find((action) => action.name === 'edit') - .disabled(objectFormState.objectData) - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='purchaseOrder' - labelWidth='225px' - objectData={objectData} - visibleProperties={{ - items: false - }} - /> - - } - active={collapseState.orderItems} - onToggle={(expanded) => - updateCollapseState('orderItems', expanded) - } - collapseKey='orderItems' - > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='purchaseOrder' + labelWidth='225px' + objectData={objectData} + visibleProperties={{ + items: false + }} + /> + + } + active={collapseState.orderItems} + onToggle={(expanded) => + updateCollapseState('orderItems', expanded) + } + collapseKey='orderItems' + > + + + } + active={collapseState.shipments} + onToggle={(expanded) => + updateCollapseState('shipments', expanded) + } + collapseKey='shipments' + > + + + } + active={collapseState.invoices} + onToggle={(expanded) => + updateCollapseState('invoices', expanded) + } + collapseKey='invoices' + > + {objectFormState.loading ? ( + + ) : ( - - } - active={collapseState.shipments} - onToggle={(expanded) => - updateCollapseState('shipments', expanded) - } - collapseKey='shipments' - > + )} + + } + active={collapseState.stockEvents} + onToggle={(expanded) => + updateCollapseState('stockEvents', expanded) + } + collapseKey='stockEvents' + > + {objectFormState.loading ? ( + + ) : ( - - } - active={collapseState.invoices} - onToggle={(expanded) => - updateCollapseState('invoices', expanded) - } - collapseKey='invoices' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - } - active={collapseState.stockEvents} - onToggle={(expanded) => - updateCollapseState('stockEvents', expanded) - } - collapseKey='stockEvents' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + )} + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Inventory/Shipments.jsx b/src/components/Dashboard/Inventory/Shipments.jsx index ac743dc5..3c0b833f 100644 --- a/src/components/Dashboard/Inventory/Shipments.jsx +++ b/src/components/Dashboard/Inventory/Shipments.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewShipment from './Shipments/NewShipment' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Shipments = () => { - const [newShipmentOpen, setNewShipmentOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('shipments') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('shipments') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Shipments') - const actionItems = { - items: [ - { - label: 'New Shipment', - key: 'newShipment', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newShipment') { - setNewShipmentOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Shipments') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewShipmentOpen(false) - }} - destroyOnHidden={true} - > - { - setNewShipmentOpen(false) - tableRef.current?.reload() - }} - reset={newShipmentOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx b/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx index 915c3d7d..3fbbb226 100644 --- a/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx +++ b/src/components/Dashboard/Inventory/Shipments/PurchaseOrderInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Modal, Space } from 'antd' +import { Card, Flex, Modal, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,15 +18,17 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' import NewOrderItem from '../OrderItems/NewOrderItem.jsx' import NewShipment from './NewShipment.jsx' import PostPurchaseOrder from '../PurchaseOrders/PostPurchaseOrder.jsx' import AcknowledgePurchaseOrder from '../PurchaseOrders/AcknowledgePurchaseOrder.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' const log = loglevel.getLogger('PurchaseOrderInfo') @@ -61,9 +63,23 @@ const PurchaseOrderInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, purchaseOrderId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { orderItemsTableRef?.current?.startEditing?.() @@ -80,10 +96,6 @@ const PurchaseOrderInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, newOrderItem: () => { setNewOrderItemOpen(true) return true @@ -117,6 +129,7 @@ const PurchaseOrderInfo = () => { { visibleState={collapseState} updateVisibleState={updateCollapseState} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -224,7 +233,7 @@ const PurchaseOrderInfo = () => { { { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/Shipments/ShipmentInfo.jsx b/src/components/Dashboard/Inventory/Shipments/ShipmentInfo.jsx index dfa13acf..7251629f 100644 --- a/src/components/Dashboard/Inventory/Shipments/ShipmentInfo.jsx +++ b/src/components/Dashboard/Inventory/Shipments/ShipmentInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,12 +18,14 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx' const log = loglevel.getLogger('ShipmentInfo') @@ -34,7 +36,6 @@ const ShipmentInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -58,8 +59,22 @@ const ShipmentInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, shipmentId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -72,181 +87,182 @@ const ShipmentInfo = () => { finishEdit: () => { objectFormRef?.current?.handleUpdate?.() return true - }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, + } } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='shipment' - objectData={objectData} - visibleProperties={{ - items: false - }} - labelWidth='175px' - /> - - } - active={collapseState.orderItems} - onToggle={(expanded) => - updateCollapseState('orderItems', expanded) - } - collapseKey='orderItems' - > - - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='shipment' + objectData={objectData} + visibleProperties={{ + items: false + }} + labelWidth='285px' + /> + + } + active={collapseState.orderItems} + onToggle={(expanded) => + updateCollapseState('orderItems', expanded) + } + collapseKey='orderItems' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Inventory/StockAudits.jsx b/src/components/Dashboard/Inventory/StockAudits.jsx index d7b781e5..382dedd8 100644 --- a/src/components/Dashboard/Inventory/StockAudits.jsx +++ b/src/components/Dashboard/Inventory/StockAudits.jsx @@ -1,65 +1,55 @@ // src/stockAudits.js -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewStockAudit from './StockAudits/NewStockAudit' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const StockAudits = () => { const tableRef = useRef() - const [newStockAuditOpen, setNewStockAuditOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('stockAudits') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('stockAudits') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('StockAudits') - const actionItems = { - items: [ - { - label: 'New Stock audit', - key: 'newStockAudit', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newStockAudit') { - setNewStockAuditOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('StockAudits') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewStockAuditOpen(false) - }} - destroyOnHidden={true} - > - { - setNewStockAuditOpen(false) - tableRef.current?.reload() - }} - reset={newStockAuditOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/StockAudits/NewStockAudit.jsx b/src/components/Dashboard/Inventory/StockAudits/NewStockAudit.jsx index 18f511f2..553647c4 100644 --- a/src/components/Dashboard/Inventory/StockAudits/NewStockAudit.jsx +++ b/src/components/Dashboard/Inventory/StockAudits/NewStockAudit.jsx @@ -6,9 +6,13 @@ import WizardView from '../../common/WizardView' const NewStockAudit = ({ onOk, reset, defaultValues }) => { return ( {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ @@ -23,6 +27,7 @@ const NewStockAudit = ({ onOk, reset, defaultValues }) => { isEditing={true} required={true} objectData={objectData} + visibleProperties={{ auditLines: false }} /> ) }, @@ -38,7 +43,9 @@ const NewStockAudit = ({ onOk, reset, defaultValues }) => { _id: false, _reference: false, createdAt: false, - updatedAt: false + postedAt: false, + updatedAt: false, + auditLines: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/StockAudits/PostStockAudit.jsx b/src/components/Dashboard/Inventory/StockAudits/PostStockAudit.jsx new file mode 100644 index 00000000..f4574c4f --- /dev/null +++ b/src/components/Dashboard/Inventory/StockAudits/PostStockAudit.jsx @@ -0,0 +1,46 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const PostStockAudit = ({ onOk, objectData }) => { + const [postLoading, setPostLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handlePost = async () => { + setPostLoading(true) + try { + const result = await sendObjectFunction( + objectData._id, + 'StockAudit', + 'post' + ) + if (result) { + message.success('Stock audit posted') + onOk(result) + } + } catch (error) { + console.error('Error posting stock audit:', error) + } finally { + setPostLoading(false) + } + } + + return ( + + ) +} + +PostStockAudit.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default PostStockAudit diff --git a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx index 2f9044fc..c452b271 100644 --- a/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx +++ b/src/components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -8,20 +8,25 @@ import useCollapseState from '../../hooks/useCollapseState.jsx' import NotesPanel from '../../common/NotesPanel.jsx' import InfoCollapse from '../../common/InfoCollapse.jsx' import ObjectInfo from '../../common/ObjectInfo.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import StockAuditIcon from '../../../Icons/StockAuditIcon.jsx' +import StockEventIcon from '../../../Icons/StockEventIcon.jsx' import ObjectForm from '../../common/ObjectForm.jsx' import EditButtons from '../../common/EditButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('StockAuditInfo') @@ -36,7 +41,8 @@ const StockAuditInfo = () => { 'StockAuditInfo', { info: true, - stocks: true, + auditLines: true, + stockEvents: true, notes: true, auditLogs: false } @@ -48,9 +54,23 @@ const StockAuditInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, stockAuditId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -67,156 +87,204 @@ const StockAuditInfo = () => { } return ( - <> - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' - > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => { - return ( - } - isEditing={isEditing} - type='stockAudit' - objectData={objectData} - visibleProperties={{ - content: false, - testObject: false - }} - /> - ) - }} - - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + } + isEditing={isEditing} + type='stockAudit' + objectData={objectData} + visibleProperties={{ + auditLines: false + }} + /> + + } + active={collapseState.auditLines} + onToggle={(expanded) => + updateCollapseState('auditLines', expanded) + } + collapseKey='auditLines' + > + + + + )} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.stockEvents} + onToggle={(expanded) => + updateCollapseState('stockEvents', expanded) + } + collapseKey='stockEvents' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Inventory/StockEvents.jsx b/src/components/Dashboard/Inventory/StockEvents.jsx index 883b8be7..110d8768 100644 --- a/src/components/Dashboard/Inventory/StockEvents.jsx +++ b/src/components/Dashboard/Inventory/StockEvents.jsx @@ -1,18 +1,23 @@ import { useRef } from 'react' -import { Button, Flex, Space, Dropdown } from 'antd' +import { Flex, Space } from 'antd' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' -import ReloadIcon from '../../Icons/ReloadIcon' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const StockEvents = () => { const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('stockEvents') const [columnVisibility, setColumnVisibility] = useColumnVisibility('stockEvent') @@ -20,29 +25,27 @@ const StockEvents = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('StockEvents') - const actionItems = { - items: [ - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('StockEvents') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/StockLocations.jsx b/src/components/Dashboard/Inventory/StockLocations.jsx index 96add8dd..e0bbea8e 100644 --- a/src/components/Dashboard/Inventory/StockLocations.jsx +++ b/src/components/Dashboard/Inventory/StockLocations.jsx @@ -1,63 +1,53 @@ -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewStockLocation from './StockLocations/NewStockLocation' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const StockLocations = () => { const tableRef = useRef() - const [newOpen, setNewOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('stockLocations') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('stockLocation') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('StockLocations') - const actionItems = { - items: [ - { - label: 'New Stock Location', - key: 'new', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'new') { - setNewOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('StockLocations') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewOpen(false) - }} - destroyOnHidden={true} - > - { - setNewOpen(false) - tableRef.current?.reload() - }} - reset={newOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/StockLocations/StockLocationInfo.jsx b/src/components/Dashboard/Inventory/StockLocations/StockLocationInfo.jsx index eb3121f0..c2fafd0f 100644 --- a/src/components/Dashboard/Inventory/StockLocations/StockLocationInfo.jsx +++ b/src/components/Dashboard/Inventory/StockLocations/StockLocationInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -12,16 +12,22 @@ import ViewButton from '../../common/ViewButton.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import StockAuditIcon from '../../../Icons/StockAuditIcon.jsx' +import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx' +import PartStockIcon from '../../../Icons/PartStockIcon.jsx' +import ProductStockIcon from '../../../Icons/ProductStockIcon.jsx' import ObjectForm from '../../common/ObjectForm.jsx' import EditButtons from '../../common/EditButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('StockLocationInfo') @@ -38,6 +44,10 @@ const StockLocationInfo = () => { 'StockLocationInfo', { info: true, + filamentStocks: true, + partStocks: true, + productStocks: true, + stockAudits: true, notes: true, auditLogs: false } @@ -49,9 +59,23 @@ const StockLocationInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, stockLocationId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -82,6 +106,7 @@ const StockLocationInfo = () => { { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Stock Location' }, + { key: 'filamentStocks', label: 'Filament Stocks' }, + { key: 'partStocks', label: 'Part Stocks' }, + { key: 'productStocks', label: 'Product Stocks' }, + { key: 'stockAudits', label: 'Stock Audits' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -101,7 +130,7 @@ const StockLocationInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -180,6 +205,118 @@ const StockLocationInfo = () => { + } + active={collapseState.filamentStocks} + onToggle={(expanded) => + updateCollapseState('filamentStocks', expanded) + } + collapseKey='filamentStocks' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.partStocks} + onToggle={(expanded) => + updateCollapseState('partStocks', expanded) + } + collapseKey='partStocks' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.productStocks} + onToggle={(expanded) => + updateCollapseState('productStocks', expanded) + } + collapseKey='productStocks' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.stockAudits} + onToggle={(expanded) => + updateCollapseState('stockAudits', expanded) + } + collapseKey='stockAudits' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + } @@ -187,9 +324,14 @@ const StockLocationInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Inventory/StockTransfers.jsx b/src/components/Dashboard/Inventory/StockTransfers.jsx index cafb3e1d..42b5bc10 100644 --- a/src/components/Dashboard/Inventory/StockTransfers.jsx +++ b/src/components/Dashboard/Inventory/StockTransfers.jsx @@ -1,63 +1,53 @@ -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewStockTransfer from './StockTransfers/NewStockTransfer' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const StockTransfers = () => { const tableRef = useRef() - const [newOpen, setNewOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('stockTransfers') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('stockTransfer') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('StockTransfers') - const actionItems = { - items: [ - { - label: 'New Stock Transfer', - key: 'new', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'new') { - setNewOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('StockTransfers') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewOpen(false) - }} - destroyOnHidden={true} - > - { - setNewOpen(false) - tableRef.current?.reload() - }} - reset={newOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx b/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx index 675da7d2..f6e0a859 100644 --- a/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx +++ b/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx @@ -1,19 +1,14 @@ import PropTypes from 'prop-types' -import dayjs from 'dayjs' import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' -const defaultTransferName = () => - `Transfer ${dayjs().format('YYYY-MM-DD HH:mm:ss')}` - const NewStockTransfer = ({ onOk, reset }) => { return ( { return ( { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -50,6 +48,7 @@ const StockTransferInfo = () => { { info: true, lines: true, + stockEvents: true, notes: true, auditLogs: false } @@ -61,9 +60,23 @@ const StockTransferInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, stockTransferId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -77,186 +90,209 @@ const StockTransferInfo = () => { objectFormRef?.current.handleUpdate() return true }, - delete: () => { - objectFormRef?.current.handleDelete?.() - return true - }, } - const editDisabled = - getModelByName('stockTransfer') - ?.actions?.find((action) => action.name === 'edit') - ?.disabled(objectFormState.objectData) ?? false - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='stockTransfer' - objectData={objectData} - labelWidth='175px' - visibleProperties={{ lines: false }} - /> - - } - active={collapseState.lines} - onToggle={(expanded) => - updateCollapseState('lines', expanded) - } - collapseKey='lines' - > - - - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='stockTransfer' + objectData={objectData} + labelWidth='175px' + visibleProperties={{ lines: false }} + /> + + } + active={collapseState.lines} + onToggle={(expanded) => + updateCollapseState('lines', expanded) + } + collapseKey='lines' + > + + + + )} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.stockEvents} + onToggle={(expanded) => + updateCollapseState('stockEvents', expanded) + } + collapseKey='stockEvents' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx index 596a5640..4516ab36 100644 --- a/src/components/Dashboard/Layout.jsx +++ b/src/components/Dashboard/Layout.jsx @@ -10,13 +10,18 @@ import ManagementSidebar from './Management/ManagementSidebar' import DashboardNavigation from './common/DashboardNavigation' import DashboardBreadcrumb from './common/DashboardBreadcrumb' import DeveloperSidebar from './Developer/DeveloperSidebar' +import DashboardSidebarSplitter from './common/DashboardSidebarSplitter' +import MobileFooterNavigation from './common/MobileFooterNavigation' import { useThemeContext } from './context/ThemeContext' import { MessageProvider } from './context/MessageContext' +import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext' +import { useMediaQuery } from 'react-responsive' const { Content } = Layout const DashboardLayout = ({ children }) => { const location = useLocation() + const isMobile = useMediaQuery({ maxWidth: 768 }) const isProduction = location.pathname.startsWith('/dashboard/production') const isInventory = location.pathname.startsWith('/dashboard/inventory') const isFinance = location.pathname.startsWith('/dashboard/finance') @@ -25,42 +30,48 @@ const DashboardLayout = ({ children }) => { const isDeveloper = location.pathname.startsWith('/dashboard/developer') const { isDarkMode } = useThemeContext() + const { currentObjectTools } = useDashboardObjectToolsContext() + + const sidebar = isProduction ? ( + + ) : isInventory ? ( + + ) : isFinance ? ( + + ) : isSales ? ( + + ) : isManagement ? ( + + ) : isDeveloper ? ( + + ) : ( + + ) return ( - - {isProduction ? ( - - ) : isInventory ? ( - - ) : isFinance ? ( - - ) : isSales ? ( - - ) : isManagement ? ( - - ) : isDeveloper ? ( - - ) : ( - // Default to production sidebar - )} - + + - + + {currentObjectTools} - {children} - + + {isMobile ? : null} ) diff --git a/src/components/Dashboard/Management/About.jsx b/src/components/Dashboard/Management/About.jsx index a7022e48..a2ca7d53 100644 --- a/src/components/Dashboard/Management/About.jsx +++ b/src/components/Dashboard/Management/About.jsx @@ -21,14 +21,15 @@ import { AppUpdateContext } from '../context/AppUpdateContext' import { useMediaQuery } from 'react-responsive' import FarmControlAppIcon from '../../Logos/FarmControlAppIcon' import SoftwareUpdateIcon from '../../Icons/SoftwareUpdateIcon' -const { Title, Text, Link } = Typography +import ExternalLink from '../common/ExternalLink' +const { Title, Text } = Typography const About = () => { const [collapseState, updateCollapseState] = useCollapseState('About', { updater: true }) const { token } = useContext(AuthContext) - const { fetchApiServerVersion, fetchWsServerVersion } = + const { fetchApiServerVersion, fetchWsServerVersion, connected } = useContext(ApiServerContext) const { isElectron, getAppEngine } = useContext(ElectronContext) const { checkForUpdates } = useContext(AppUpdateContext) @@ -61,7 +62,7 @@ const About = () => { } useEffect(() => { - if (token) { + if (token && connected) { fetchApiServerVersion().then((version) => { setApiServerVersion(version) }) @@ -69,7 +70,7 @@ const About = () => { setWsServerVersion(version) }) } - }, [fetchApiServerVersion, fetchWsServerVersion, token]) + }, [fetchApiServerVersion, fetchWsServerVersion, token, connected]) useEffect(() => { if (!isElectron) return @@ -171,11 +172,13 @@ const About = () => { Development )} - + Jenkins - + - GitHub + + Git + diff --git a/src/components/Dashboard/Management/AppPasswords.jsx b/src/components/Dashboard/Management/AppPasswords.jsx index 462bcec5..089a95b1 100644 --- a/src/components/Dashboard/Management/AppPasswords.jsx +++ b/src/components/Dashboard/Management/AppPasswords.jsx @@ -1,59 +1,50 @@ -import { useRef, useState } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewAppPassword from './AppPasswords/NewAppPassword' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import useColumnVisibility from '../hooks/useColumnVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const AppPasswords = () => { - const [newAppPasswordOpen, setNewAppPasswordOpen] = useState(false) const tableRef = useRef() - - const [viewMode, setViewMode] = useViewMode('appPassword') const [columnVisibility, setColumnVisibility] = useColumnVisibility('appPassword') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('AppPasswords') - const actionItems = { - items: [ - { - label: 'New App Password', - key: 'newAppPassword', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newAppPassword') { - setNewAppPasswordOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('AppPasswords') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewAppPasswordOpen(false) - }} - > - { - setNewAppPasswordOpen(false) - tableRef.current?.reload() - }} - reset={newAppPasswordOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx b/src/components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx index 8440af8c..0e49cca0 100644 --- a/src/components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx +++ b/src/components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect, useContext } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' import { Flex, Space } from 'antd' import { LoadingOutlined } from '@ant-design/icons' @@ -14,21 +14,20 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' -import ScrollBox from '../../common/ScrollBox.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' -import { AuthContext } from '../../context/AuthContext.jsx' +import ScrollBox from '../../common/ScrollBox.jsx' const AppPasswordInfo = () => { const location = useLocation() const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -36,7 +35,6 @@ const AppPasswordInfo = () => { }) return () => setOnModalOk(null) }, [setOnModalOk]) - const { userProfile } = useContext(AuthContext) const appPasswordId = new URLSearchParams(location.search).get( 'appPasswordId' ) @@ -53,9 +51,23 @@ const AppPasswordInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, appPasswordId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -71,137 +83,131 @@ const AppPasswordInfo = () => { } } - const editDisabled = getModelByName('appPassword') - .actions.find((action) => action.name === 'edit') - .disabled({ ...objectFormState.objectData, _user: userProfile }) - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - - - - - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' - > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - } - isEditing={isEditing} - type='appPassword' - objectData={objectData} - /> - )} - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + } + isEditing={isEditing} + type='appPassword' + objectData={objectData} + /> + )} + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx index 65b5c3de..e36637d5 100644 --- a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx +++ b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx @@ -5,7 +5,7 @@ import { Button, Flex, Modal, Typography, theme, Divider } from 'antd' import CloudIcon from '../../../Icons/CloudIcon' import HostIcon from '../../../Icons/HostIcon' import ReloadIcon from '../../../Icons/ReloadIcon' -import HProgress from '../../common/HProgress' +import ProgressDisplay from '../../common/ProgressDisplay' import CheckCircleIcon from '../../../Icons/CheckCircleIcon' import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' @@ -135,19 +135,16 @@ const UpdateStage = ({ stage, status, percent, detail }) => { {config.labels[status]} {showProgress && ( - - - {detail && ( - - {detail} - - )} - + + {detail} + )} diff --git a/src/components/Dashboard/Management/AuditLogs.jsx b/src/components/Dashboard/Management/AuditLogs.jsx index 482c84e9..1ce0a787 100644 --- a/src/components/Dashboard/Management/AuditLogs.jsx +++ b/src/components/Dashboard/Management/AuditLogs.jsx @@ -1,13 +1,21 @@ import { useRef } from 'react' -import { Button, Flex, Space, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const AuditLogs = () => { const tableRef = useRef() @@ -18,29 +26,23 @@ const AuditLogs = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('AuditLogs') - const actionItems = { - items: [ - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('AuditLogs') return ( - <> + - + - - - + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={updateColumnVisibility} /> + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + + - { useSortInUrl={true} /> - + ) } diff --git a/src/components/Dashboard/Management/CourierServices.jsx b/src/components/Dashboard/Management/CourierServices.jsx index c6191ec6..5cb70382 100644 --- a/src/components/Dashboard/Management/CourierServices.jsx +++ b/src/components/Dashboard/Management/CourierServices.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewCourierService from './CourierServices/NewCourierService' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const CourierServices = () => { - const [newCourierServiceOpen, setNewCourierServiceOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('courierService') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('courierService') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('CourierServices') - const actionItems = { - items: [ - { - label: 'New Courier Service', - key: 'newCourierService', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newCourierService') { - setNewCourierServiceOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('CourierServices') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewCourierServiceOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewCourierServiceOpen(false) - tableRef.current?.reload() - }} - reset={!newCourierServiceOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx b/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx index 8a5fdee6..d005cc43 100644 --- a/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx +++ b/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx @@ -1,26 +1,31 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' import ObjectForm from '../../common/ObjectForm' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('CourierServiceInfo') @@ -37,6 +42,7 @@ const CourierServiceInfo = () => { 'CourierServiceInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false } @@ -47,9 +53,23 @@ const CourierServiceInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, courierServiceId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -63,10 +83,6 @@ const CourierServiceInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -81,6 +97,7 @@ const CourierServiceInfo = () => { { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Courier Service Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -100,7 +118,7 @@ const CourierServiceInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -145,33 +159,56 @@ const CourierServiceInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Couriers.jsx b/src/components/Dashboard/Management/Couriers.jsx index 39184def..6703a671 100644 --- a/src/components/Dashboard/Management/Couriers.jsx +++ b/src/components/Dashboard/Management/Couriers.jsx @@ -1,59 +1,50 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewCourier from './Couriers/NewCourier' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Couriers = () => { - const [newCourierOpen, setNewCourierOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('courier') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('courier') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Couriers') - const actionItems = { - items: [ - { - label: 'New Courier', - key: 'newCourier', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newCourier') { - setNewCourierOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Couriers') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewCourierOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewCourierOpen(false) - tableRef.current?.reload() - }} - reset={!newCourierOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/Couriers/CourierInfo.jsx b/src/components/Dashboard/Management/Couriers/CourierInfo.jsx index cc72d445..3c352f5b 100644 --- a/src/components/Dashboard/Management/Couriers/CourierInfo.jsx +++ b/src/components/Dashboard/Management/Couriers/CourierInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,11 +18,13 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import CourierServiceIcon from '../../../Icons/CourierServiceIcon.jsx' const log = loglevel.getLogger('CourierInfo') @@ -43,9 +46,23 @@ const CourierInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, courierId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -59,10 +76,6 @@ const CourierInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -77,6 +90,7 @@ const CourierInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -183,10 +193,9 @@ const CourierInfo = () => { ) : ( )} @@ -198,9 +207,14 @@ const CourierInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/DocumentJobs.jsx b/src/components/Dashboard/Management/DocumentJobs.jsx index c47b604c..e4b775c2 100644 --- a/src/components/Dashboard/Management/DocumentJobs.jsx +++ b/src/components/Dashboard/Management/DocumentJobs.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewDocumentJob from './DocumentJobs/NewDocumentJob' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const DocumentJobs = () => { - const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('documentJob') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('documentJob') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('DocumentJobs') - const actionItems = { - items: [ - { - label: 'New Document Job', - key: 'newDocumentJob', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newDocumentJob') { - setNewDocumentJobOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('DocumentJobs') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewDocumentJobOpen(false)} - footer={null} - destroyOnHidden={true} - width={900} - > - { - setNewDocumentJobOpen(false) - tableRef.current?.reload() - }} - reset={!newDocumentJobOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx b/src/components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx index 37948744..c866e7e6 100644 --- a/src/components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx +++ b/src/components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('DocumentJobInfo') @@ -48,9 +50,23 @@ const DocumentJobInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, documentJobId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -78,6 +94,7 @@ const DocumentJobInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -178,9 +191,14 @@ const DocumentJobInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/DocumentJobs/NewDocumentJob.jsx b/src/components/Dashboard/Management/DocumentJobs/NewDocumentJob.jsx index 20fe4316..12cb0631 100644 --- a/src/components/Dashboard/Management/DocumentJobs/NewDocumentJob.jsx +++ b/src/components/Dashboard/Management/DocumentJobs/NewDocumentJob.jsx @@ -3,121 +3,252 @@ import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' import TemplatePreview from '../../common/TemplatePreview' +import ProgressDisplay from '../../common/ProgressDisplay' import { ApiServerContext } from '../../context/ApiServerContext' -import { useContext, useRef, useState } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' +import { Modal } from 'antd' import dayjs from 'dayjs' +import { toTemplateObjectData } from '../../utils/templateObjectData' const NewDocumentJob = ({ onOk, defaultValues = {} }) => { - const { sendObjectAction, downloadTemplatePDF, formatFileName } = - useContext(ApiServerContext) + const { + sendObjectAction, + downloadTemplate, + formatFileName, + subscribeToObjectUpdates, + connected + } = useContext(ApiServerContext) const [downloading, setDownloading] = useState(false) + const [deployingJob, setDeployingJob] = useState(null) + const onOkRef = useRef(onOk) + const pendingJobRef = useRef(null) + const deploySentRef = useRef(false) + const sendObjectActionRef = useRef(sendObjectAction) + + onOkRef.current = onOk + sendObjectActionRef.current = sendObjectAction + + const deployingJobId = deployingJob?._id + ? String(deployingJob._id).toLowerCase() + : null + + const handleJobUpdate = useCallback((updatedJob) => { + setDeployingJob((prev) => (prev ? { ...prev, ...updatedJob } : updatedJob)) + }, []) + + useEffect(() => { + if (!deployingJobId || connected != true) { + return + } + + const unsubscribe = subscribeToObjectUpdates( + deployingJobId, + 'documentJob', + handleJobUpdate + ) + + if (!deploySentRef.current && pendingJobRef.current) { + deploySentRef.current = true + sendObjectActionRef.current( + pendingJobRef.current.documentPrinter._id, + 'documentPrinter', + { + type: 'deploy', + data: pendingJobRef.current + } + ) + } + + return () => { + if (unsubscribe) unsubscribe() + } + }, [deployingJobId, connected, subscribeToObjectUpdates, handleJobUpdate]) + + useEffect(() => { + const stateType = deployingJob?.state?.type + if (stateType === 'queued' || stateType === 'error') { + onOkRef.current?.() + } + }, [deployingJob?.state?.type]) // Capture initial default values so later prop changes don't re-initialize the form const defaultValuesRef = useRef({ objectType: 'documentJob', + quantity: 1, ...defaultValues, saveToFile: false }) return ( - - {({ handleSubmit, submitLoading, objectData, formValid }) => { - const steps = [ - { - title: 'Required', - key: 'required', - content: ( - - ) - } - ] - const fileName = - formatFileName( - objectData?.name + ' ' + dayjs().format('YYYY-MM-DD HH:mm:ss') - ) || 'document' - return ( - - + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + - + ) } - onSubmit={async () => { - const newDocumentJob = await handleSubmit() - if (newDocumentJob) { - await sendObjectAction( - newDocumentJob.documentPrinter._id, - 'documentPrinter', - { - type: 'deploy', - data: newDocumentJob - } - ) - if (onOk) { - onOk() + ] + const fileName = + formatFileName( + objectData?.name + ' ' + dayjs().format('YYYY-MM-DD HH:mm:ss') + ) || 'document' + return ( + + + + } + onSubmit={async () => { + const newDocumentJob = await handleSubmit() + if (!newDocumentJob?._id) { + return } - } - }} - actions={[ - { - label: 'Download', - steps: ['required'], - loading: downloading == true, - disabled: downloading == true || submitLoading == true, - children: [ - { - label: 'PDF', - key: 'pdf', - onClick: () => { - setDownloading(true) - downloadTemplatePDF( - objectData.documentTemplate._id, - objectData.documentTemplate.content, - objectData.object, - fileName, - () => { - setDownloading(false) - } - ) + + pendingJobRef.current = newDocumentJob + deploySentRef.current = false + setDeployingJob({ + ...newDocumentJob, + state: { type: 'deploying', progress: 0 } + }) + }} + actions={[ + { + label: 'Download', + steps: ['required'], + loading: downloading == true, + disabled: downloading == true || submitLoading == true, + children: [ + { + label: 'PDF', + key: 'pdf', + onClick: () => { + setDownloading(true) + downloadTemplate( + objectData.documentTemplate._id, + objectData.documentTemplate.content, + toTemplateObjectData(objectData.objects), + fileName, + 'pdf', + () => { + setDownloading(false) + } + ) + } + }, + { + label: 'PNG', + key: 'png', + onClick: () => { + setDownloading(true) + downloadTemplate( + objectData.documentTemplate._id, + objectData.documentTemplate.content, + toTemplateObjectData(objectData.objects), + fileName, + 'png', + () => { + setDownloading(false) + } + ) + } + }, + { + label: 'JPEG', + key: 'jpeg', + onClick: () => { + setDownloading(true) + downloadTemplate( + objectData.documentTemplate._id, + objectData.documentTemplate.content, + toTemplateObjectData(objectData.objects), + fileName, + 'jpeg', + () => { + setDownloading(false) + } + ) + } + }, + { + label: 'SVG', + key: 'svg', + onClick: () => { + setDownloading(true) + downloadTemplate( + objectData.documentTemplate._id, + objectData.documentTemplate.content, + toTemplateObjectData(objectData.objects), + fileName, + 'svg', + () => { + setDownloading(false) + } + ) + } } - }, - { - label: 'PNG', - key: 'png' - }, - { - label: 'JPEG', - key: 'jpeg' - } - ] - } - ]} - /> - ) - }} - + ] + } + ]} + /> + ) + }} + + document.body} + > + + Please wait... + + + ) } diff --git a/src/components/Dashboard/Management/DocumentPrinters.jsx b/src/components/Dashboard/Management/DocumentPrinters.jsx index aa56da86..11ed8981 100644 --- a/src/components/Dashboard/Management/DocumentPrinters.jsx +++ b/src/components/Dashboard/Management/DocumentPrinters.jsx @@ -1,21 +1,23 @@ -import { useRef, useState } from 'react' -import { Button, Flex, Space, Dropdown, Modal } from 'antd' -import PlusIcon from '../../Icons/PlusIcon' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' -import NewDocumentPrinter from './DocumentPrinters/NewDocumentPrinter' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const DocumentPrinters = () => { const tableRef = useRef() - const [newDocumentPrinterOpen, setNewDocumentPrinterOpen] = useState(false) - const [viewMode, setViewMode] = useViewMode('documentPrinter') const [columnVisibility, setColumnVisibility] = useColumnVisibility('documentPrinter') @@ -23,37 +25,27 @@ const DocumentPrinters = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('DocumentPrinters') - const actionItems = { - items: [ - { - label: 'New Document Printer', - key: 'newDocumentPrinter', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newDocumentPrinter') { - setNewDocumentPrinterOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('DocumentPrinters') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewDocumentPrinterOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewDocumentPrinterOpen(false) - tableRef.current?.reload() - }} - reset={!newDocumentPrinterOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx b/src/components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx index d5d5b99d..0c47aaa9 100644 --- a/src/components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx +++ b/src/components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('DocumentPrinterInfo') @@ -50,9 +52,23 @@ const DocumentPrinterInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, documentPrinterId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -83,6 +99,7 @@ const DocumentPrinterInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -191,9 +204,14 @@ const DocumentPrinterInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - - - + } + > + + + + { ) : ( )} diff --git a/src/components/Dashboard/Management/DocumentPrinters/NewDocumentPrinter.jsx b/src/components/Dashboard/Management/DocumentPrinters/NewDocumentPrinter.jsx index 20c77821..50fdd259 100644 --- a/src/components/Dashboard/Management/DocumentPrinters/NewDocumentPrinter.jsx +++ b/src/components/Dashboard/Management/DocumentPrinters/NewDocumentPrinter.jsx @@ -1,29 +1,75 @@ import PropTypes from 'prop-types' +import { Flex } from 'antd' import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' +import HostDeviceScanner, { + getHostId, + isNetworkScanCompatible +} from '../../common/HostDeviceScanner' const NewDocumentPrinter = ({ onOk, defaultValues }) => { return ( - {({ handleSubmit, submitLoading, objectData, formValid }) => { + {({ + handleSubmit, + submitLoading, + objectData, + formValid, + form, + setObjectData + }) => { const steps = [ { title: 'Required', key: 'required', content: ( - + + + {getHostId(objectData.host) && + isNetworkScanCompatible(objectData.connection?.protocol) ? ( + + ) : null} + + ) }, { diff --git a/src/components/Dashboard/Management/DocumentSizes.jsx b/src/components/Dashboard/Management/DocumentSizes.jsx index d951c11a..810e2d84 100644 --- a/src/components/Dashboard/Management/DocumentSizes.jsx +++ b/src/components/Dashboard/Management/DocumentSizes.jsx @@ -1,58 +1,50 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewDocumentSize from './DocumentSizes/NewDocumentSize' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const DocumentSizes = () => { - const [newDocumentSizeOpen, setNewDocumentSizeOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('documentSize') const [columnVisibility, setColumnVisibility] = useColumnVisibility('documentSize') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('DocumentSizes') - const actionItems = { - items: [ - { - label: 'New Document Size', - key: 'newDocumentSize', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newDocumentSize') { - setNewDocumentSizeOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('DocumentSizes') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateCollapseState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewDocumentSizeOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewDocumentSizeOpen(false) - tableRef.current?.reload() - }} - reset={!newDocumentSizeOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx b/src/components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx index ecd491aa..1b47fd56 100644 --- a/src/components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx +++ b/src/components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('DocumentSizeInfo') @@ -48,9 +50,23 @@ const DocumentSizeInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, documentSizeId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -78,6 +94,7 @@ const DocumentSizeInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -179,9 +192,14 @@ const DocumentSizeInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/DocumentSizes/NewDocumentSize.jsx b/src/components/Dashboard/Management/DocumentSizes/NewDocumentSize.jsx index 3d0f72e6..137b7c3b 100644 --- a/src/components/Dashboard/Management/DocumentSizes/NewDocumentSize.jsx +++ b/src/components/Dashboard/Management/DocumentSizes/NewDocumentSize.jsx @@ -7,7 +7,15 @@ const NewDocumentSize = ({ onOk, defaultValues }) => { return ( {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ diff --git a/src/components/Dashboard/Management/DocumentTemplates.jsx b/src/components/Dashboard/Management/DocumentTemplates.jsx index b42e1b5a..22d72381 100644 --- a/src/components/Dashboard/Management/DocumentTemplates.jsx +++ b/src/components/Dashboard/Management/DocumentTemplates.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewDocumentTemplate from './DocumentTemplates/NewDocumentTemplate' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const DocumentTemplates = () => { - const [newDocumentTemplateOpen, setNewDocumentTemplateOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('documentTemplate') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('documentTemplate') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('DocumentTemplates') - const actionItems = { - items: [ - { - label: 'New Document Template', - key: 'newDocumentTemplate', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newDocumentTemplate') { - setNewDocumentTemplateOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('DocumentTemplates') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewDocumentTemplateOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewDocumentTemplateOpen(false) - tableRef.current?.reload() - }} - reset={!newDocumentTemplateOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx b/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx index 38ddce3d..8239ea34 100644 --- a/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx +++ b/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx @@ -1,6 +1,7 @@ -import { useState, useRef } from 'react' +import { useState, useRef, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Space, Flex, Card } from 'antd' +import { Space, Flex, Card, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' import useCollapseState from '../../hooks/useCollapseState.jsx' @@ -10,9 +11,11 @@ import ViewButton from '../../common/ViewButton.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import ObjectForm from '../../common/ObjectForm.jsx' import EditButtons from '../../common/EditButtons.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ScrollBox from '../../common/ScrollBox.jsx' import TemplateEditor from '../../common/TemplateEditor.jsx' @@ -46,6 +49,19 @@ const DocumentTemplateDesign = () => { objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, documentTemplateId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -75,6 +91,7 @@ const DocumentTemplateDesign = () => { { setEditFormState((prev) => ({ ...prev, ...state })) }} > - {({ loading, isEditing, objectData }) => { + {({ loading, isEditing, objectData, form, setObjectData }) => { return ( { style={{ height: '72vh' }} collapseState={collapseState} isEditing={isEditing} + form={form} + setObjectData={setObjectData} /> ) }} @@ -155,9 +174,14 @@ const DocumentTemplateDesign = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + diff --git a/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx b/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx index 23064039..c9e2298e 100644 --- a/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx +++ b/src/components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -8,20 +8,27 @@ import useCollapseState from '../../hooks/useCollapseState.jsx' import NotesPanel from '../../common/NotesPanel.jsx' import InfoCollapse from '../../common/InfoCollapse.jsx' import ObjectInfo from '../../common/ObjectInfo.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import FileIcon from '../../../Icons/FileIcon.jsx' import ObjectForm from '../../common/ObjectForm.jsx' import EditButtons from '../../common/EditButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelByName, + getModelProperty +} from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('DocumentTemplateInfo') @@ -38,6 +45,7 @@ const DocumentTemplateInfo = () => { 'DocumentTemplateInfo', { info: true, + resources: true, stocks: true, notes: true, auditLogs: false @@ -50,9 +58,23 @@ const DocumentTemplateInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, documentTemplateId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -83,6 +105,7 @@ const DocumentTemplateInfo = () => { { disabled={objectFormState.loading} items={[ { key: 'info', label: 'DocumentTemplate Information' }, + { key: 'resources', label: 'Resources' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -102,7 +126,7 @@ const DocumentTemplateInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -148,25 +168,27 @@ const DocumentTemplateInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => { - return ( + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > } @@ -175,14 +197,32 @@ const DocumentTemplateInfo = () => { objectData={objectData} visibleProperties={{ content: false, - testObject: false + testObject: false, + resources: false }} - labelWidth='190px' + labelWidth='210px' /> - ) - }} - - + + } + active={collapseState.resources} + onToggle={(expanded) => + updateCollapseState('resources', expanded) + } + collapseKey='resources' + > + + + + )} + { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - - - + } + > + + + + { ) : ( )} diff --git a/src/components/Dashboard/Management/DocumentTemplates/NewDocumentTemplate.jsx b/src/components/Dashboard/Management/DocumentTemplates/NewDocumentTemplate.jsx index e0d90562..8dbc388a 100644 --- a/src/components/Dashboard/Management/DocumentTemplates/NewDocumentTemplate.jsx +++ b/src/components/Dashboard/Management/DocumentTemplates/NewDocumentTemplate.jsx @@ -36,7 +36,11 @@ const NewDocumentTemplate = ({ onOk, defaultValues }) => { bordered={false} isEditing={true} required={false} - visibleProperties={{ content: false, testObject: false }} + visibleProperties={{ + content: false, + testObject: false, + referencedTemplates: false + }} objectData={objectData} /> ) @@ -53,7 +57,8 @@ const NewDocumentTemplate = ({ onOk, defaultValues }) => { _id: false, _reference: false, createdAt: false, - updatedAt: false + updatedAt: false, + referencedTemplates: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Management/EmailAccounts.jsx b/src/components/Dashboard/Management/EmailAccounts.jsx new file mode 100644 index 00000000..bd7b60b9 --- /dev/null +++ b/src/components/Dashboard/Management/EmailAccounts.jsx @@ -0,0 +1,90 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const EmailAccounts = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailAccount') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailAccounts') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailAccounts') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailAccounts diff --git a/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx b/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx new file mode 100644 index 00000000..f885c91f --- /dev/null +++ b/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx @@ -0,0 +1,238 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('EmailAccountInfo') +log.setLevel(config.logLevel) + +const EmailAccountInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailAccountId = new URLSearchParams(location.search).get( + 'emailAccountId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailAccountInfo', + { + info: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, emailAccountId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef?.current.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef?.current.handleUpdate() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + )} + + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailAccountInfo diff --git a/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx b/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx new file mode 100644 index 00000000..83bd42a9 --- /dev/null +++ b/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewEmailAccount = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewEmailAccount.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewEmailAccount diff --git a/src/components/Dashboard/Management/EmailMessages.jsx b/src/components/Dashboard/Management/EmailMessages.jsx new file mode 100644 index 00000000..175cbe18 --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages.jsx @@ -0,0 +1,90 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const EmailMessages = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailMessage') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailMessages') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailMessages') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailMessages diff --git a/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx b/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx new file mode 100644 index 00000000..b092217b --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx @@ -0,0 +1,251 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import EmailMessageIcon from '../../../Icons/EmailMessageIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' +import TemplatePreview from '../../common/TemplatePreview.jsx' + +const log = loglevel.getLogger('EmailMessageInfo') +log.setLevel(config.logLevel) + +const EmailMessageInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const emailMessageId = new URLSearchParams(location.search).get( + 'emailMessageId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailMessageInfo', + { + info: true, + recipients: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, emailMessageId] + ) + useDashboardObjectTools(currentObjectTools) + + return ( + <> + + + + + + + + + + + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.recipients} + onToggle={(expanded) => + updateCollapseState('recipients', expanded) + } + collapseKey='recipients' + > + + + } + active={collapseState.content} + onToggle={(expanded) => + updateCollapseState('content', expanded) + } + collapseKey='content' + > + } + > + + + + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailMessageInfo diff --git a/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx b/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx new file mode 100644 index 00000000..5ec2200b --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx @@ -0,0 +1,380 @@ +import PropTypes from 'prop-types' +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react' +import { Form, Modal } from 'antd' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectChildCards from '../../common/ObjectChildCards' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' +import TemplatePreview from '../../common/TemplatePreview' +import ProgressDisplay from '../../common/ProgressDisplay' +import { ApiServerContext } from '../../context/ApiServerContext' +import { + candidatesToRecipients, + getReferenceId, + resolveEmailRecipientsForObjects +} from '../../utils/emailRecipients' +import { toTemplateObjectData } from '../../utils/templateObjectData' + +const EMPTY_RECIPIENT_CANDIDATES = [] +const EMPTY_DEFAULT_VALUES = {} + +const EmailWizardContent = ({ + accounts, + defaultCandidates, + handleSubmit, + submitLoading, + objectData, + setObjectData, + form, + formValid, + onCreated +}) => { + const { fetchObject } = useContext(ApiServerContext) + const [candidates, setCandidates] = useState(defaultCandidates) + const [recipientIndex, setRecipientIndex] = useState(0) + const recipients = Array.isArray(objectData?.recipients) + ? objectData.recipients + : [] + const currentRecipient = recipients[recipientIndex] || null + const previewObject = useMemo(() => { + const selected = currentRecipient?.object + const selectedId = getReferenceId(selected) + const objects = Array.isArray(objectData?.objects) ? objectData.objects : [] + if (selectedId) { + const match = objects.find((item) => getReferenceId(item) === selectedId) + if (match && typeof match === 'object' && !Array.isArray(match)) { + return match + } + } + if (selected && typeof selected === 'object' && !Array.isArray(selected)) { + return selected + } + return null + }, [currentRecipient?.object, objectData?.objects]) + + const relatedObjects = Array.isArray(objectData?.objects) + ? objectData.objects + : objectData?.object + ? [objectData.object] + : [] + const objectIdsKey = relatedObjects + .map((item) => getReferenceId(item)) + .filter(Boolean) + .join('|') + const relatedObjectsRef = useRef(relatedObjects) + relatedObjectsRef.current = relatedObjects + + useEffect(() => { + const objects = relatedObjectsRef.current + if (!objects.length) { + setCandidates([]) + return undefined + } + let cancelled = false + resolveEmailRecipientsForObjects({ + type: objectData?.objectType, + objects, + fetchObject + }).then((result) => { + if (!cancelled) setCandidates(result) + }) + return () => { + cancelled = true + } + }, [fetchObject, objectData?.objectType, objectIdsKey]) + + useEffect(() => { + setObjectData((previous) => ({ + ...previous, + _recipientCandidates: candidates + })) + }, [candidates, setObjectData]) + + useEffect(() => { + const nextRecipients = candidatesToRecipients(candidates) + form.setFieldsValue({ recipients: nextRecipients }) + setRecipientIndex((index) => (index < nextRecipients.length ? index : 0)) + setObjectData((previous) => ({ + ...previous, + recipients: nextRecipients + })) + }, [candidates, form, setObjectData]) + + useEffect(() => { + setObjectData((previous) => { + if (previous?.currentRecipient === currentRecipient) return previous + return { ...previous, currentRecipient } + }) + }, [currentRecipient, setObjectData]) + + useEffect(() => { + const currentAccount = objectData?.emailAccount + const account = + accounts.find( + (entry) => entry._id === (currentAccount?._id || currentAccount) + ) || accounts[0] + if (!account || objectData?.emailAccount) return + form.setFieldsValue({ emailAccount: account, fromEmail: account.fromEmail }) + setObjectData((previous) => ({ + ...previous, + emailAccount: account, + fromEmail: account.fromEmail + })) + }, [accounts, form, objectData?.emailAccount, setObjectData]) + + return ( + + + + + + + ) + } + ]} + submitText='Send' + title='Send Email' + formValid={ + formValid && + recipients.some((recipient) => recipient?.recipientEmail) && + accounts.length > 0 + } + loading={submitLoading} + sideBarGrow + sideBar={ +
+ +
+ } + onSubmit={async () => { + const result = await handleSubmit() + if (result?._id) onCreated(result) + }} + /> + ) +} + +EmailWizardContent.propTypes = { + accounts: PropTypes.array.isRequired, + defaultCandidates: PropTypes.array.isRequired, + handleSubmit: PropTypes.func.isRequired, + submitLoading: PropTypes.bool, + objectData: PropTypes.object, + setObjectData: PropTypes.func.isRequired, + form: PropTypes.object.isRequired, + formValid: PropTypes.bool, + onCreated: PropTypes.func.isRequired +} + +const NewEmailMessage = ({ + onOk, + defaultValues = EMPTY_DEFAULT_VALUES, + recipientCandidates = EMPTY_RECIPIENT_CANDIDATES +}) => { + const { + fetchObject, + fetchObjects, + connected, + subscribeToObjectUpdates, + subscribeToObjectTypeUpdates + } = useContext(ApiServerContext) + const [accounts, setAccounts] = useState([]) + const [sendingMessage, setSendingMessage] = useState(null) + const fetchObjectRef = useRef(fetchObject) + const fetchObjectsRef = useRef(fetchObjects) + const fetchedMessageIdRef = useRef(null) + fetchObjectRef.current = fetchObject + fetchObjectsRef.current = fetchObjects + const initialValues = useMemo(() => ({ ...defaultValues }), [defaultValues]) + + const loadAccounts = useCallback(async () => { + const result = await fetchObjectsRef.current('emailAccount', { + filter: { active: true }, + limit: 100 + }) + setAccounts(Array.isArray(result?.data) ? result.data : []) + }, []) + + useEffect(() => { + loadAccounts() + }, [loadAccounts]) + + useEffect(() => { + if (!connected) return undefined + return subscribeToObjectTypeUpdates( + 'emailAccount', + { active: true }, + loadAccounts + ) + }, [connected, loadAccounts, subscribeToObjectTypeUpdates]) + + useEffect(() => { + if (!connected) return undefined + const unsubscribes = accounts + .filter((account) => account?._id) + .map((account) => + subscribeToObjectUpdates( + account._id.toLowerCase(), + 'emailAccount', + (update) => { + const merged = { ...account, ...update } + setAccounts((previous) => + merged.active === true + ? previous.map((entry) => + entry._id === account._id ? merged : entry + ) + : previous.filter((entry) => entry._id !== account._id) + ) + } + ) + ) + return () => unsubscribes.forEach((unsubscribe) => unsubscribe?.()) + }, [accounts, connected, subscribeToObjectUpdates]) + + useEffect(() => { + const id = sendingMessage?._id + if (!id) return undefined + + let cancelled = false + if (fetchedMessageIdRef.current !== id) { + fetchedMessageIdRef.current = id + fetchObjectRef.current(id, 'emailMessage').then((current) => { + if (!cancelled && current?._id) { + setSendingMessage((previous) => ({ ...previous, ...current })) + } + }) + } + + const unsubscribe = + connected === true + ? subscribeToObjectUpdates(id.toLowerCase(), 'emailMessage', (update) => + setSendingMessage((previous) => ({ ...previous, ...update })) + ) + : undefined + + return () => { + cancelled = true + unsubscribe?.() + } + }, [connected, sendingMessage?._id, subscribeToObjectUpdates]) + + useEffect(() => { + if (sendingMessage?.state?.type === 'sent') onOk?.(sendingMessage) + }, [onOk, sendingMessage]) + + return ( + <> + + {(formProps) => ( + + )} + + setSendingMessage(null)} + maskClosable={false} + centered + destroyOnHidden + zIndex={2000} + width={360} + getContainer={() => document.body} + > + + {sendingMessage?.state?.message || + `Email ${sendingMessage?.state?.type || 'queued'}...`} + + + + ) +} + +NewEmailMessage.propTypes = { + onOk: PropTypes.func, + defaultValues: PropTypes.object, + recipientCandidates: PropTypes.array +} + +export default NewEmailMessage diff --git a/src/components/Dashboard/Management/EmailTemplates.jsx b/src/components/Dashboard/Management/EmailTemplates.jsx new file mode 100644 index 00000000..23bb6fe9 --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates.jsx @@ -0,0 +1,90 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const EmailTemplates = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailTemplate') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailTemplates') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailTemplates') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailTemplates diff --git a/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx new file mode 100644 index 00000000..c8f314ae --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx @@ -0,0 +1,159 @@ +import { useMemo, useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ViewButton from '../../common/ViewButton' +import NoteIcon from '../../../Icons/NoteIcon' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons' +import ActivityIndicator from '../../common/ActivityIndicator' +import ActionHandler from '../../common/ActionHandler' +import ObjectActions from '../../common/ObjectActions' +import TemplateEditor from '../../common/TemplateEditor' +import ScrollBox from '../../common/ScrollBox' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' + +const EmailTemplateDesign = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailTemplateId = new URLSearchParams(location.search).get( + 'emailTemplateId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailTemplateDesign', + { preview: true, editor: true, notes: true } + ) + const [formState, setFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + objectData: {} + }) + + useDashboardObjectTools( + useMemo( + () => ( + + ), + [emailTemplateId, formState.isEditing, formState.loading] + ) + ) + + return ( + + + + + + + + + + actionHandlerRef.current.callAction('finishEdit')} + cancelEditing={() => + actionHandlerRef.current.callAction('cancelEdit') + } + startEditing={() => actionHandlerRef.current.callAction('edit')} + editLoading={formState.editLoading} + formValid={formState.formValid} + disabled={formState.beingEditedByOther || formState.loading} + loading={formState.editLoading} + /> + + + + { + objectFormRef.current?.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef.current?.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef.current?.handleUpdate() + return true + } + }} + loading={formState.loading} + ref={actionHandlerRef} + > + + setFormState((previous) => ({ ...previous, ...state })) + } + > + {({ loading, isEditing, objectData, form, setObjectData }) => ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + }> + + + + + + + + + ) +} + +export default EmailTemplateDesign diff --git a/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx new file mode 100644 index 00000000..eed3c9e1 --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx @@ -0,0 +1,289 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import FileIcon from '../../../Icons/FileIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('EmailTemplateInfo') +log.setLevel(config.logLevel) + +const EmailTemplateInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailTemplateId = new URLSearchParams(location.search).get( + 'emailTemplateId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailTemplateInfo', + { + info: true, + resources: true, + attachments: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, emailTemplateId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef?.current.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef?.current.handleUpdate() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='emailTemplate' + objectData={objectData} + visibleProperties={{ + content: false, + testObject: false, + resources: false, + attachments: false + }} + labelWidth='210px' + /> + + } + active={collapseState.resources} + onToggle={(expanded) => + updateCollapseState('resources', expanded) + } + collapseKey='resources' + > + + + } + active={collapseState.attachments} + onToggle={(expanded) => + updateCollapseState('attachments', expanded) + } + collapseKey='attachments' + > + + + + )} + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailTemplateInfo diff --git a/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx b/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx new file mode 100644 index 00000000..d5bca084 --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx @@ -0,0 +1,97 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewEmailTemplate = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewEmailTemplate.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewEmailTemplate diff --git a/src/components/Dashboard/Management/FilamentSkus.jsx b/src/components/Dashboard/Management/FilamentSkus.jsx index b020e653..58a6dc6f 100644 --- a/src/components/Dashboard/Management/FilamentSkus.jsx +++ b/src/components/Dashboard/Management/FilamentSkus.jsx @@ -1,63 +1,53 @@ -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewFilamentSku from './FilamentSkus/NewFilamentSku' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const FilamentSkus = () => { const tableRef = useRef() - const [newFilamentSkuOpen, setNewFilamentSkuOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('filamentSkus') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('filamentSku') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('FilamentSkus') - const actionItems = { - items: [ - { - label: 'New Filament SKU', - key: 'newFilamentSku', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newFilamentSku') { - setNewFilamentSkuOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('FilamentSkus') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewFilamentSkuOpen(false) - }} - destroyOnHidden={true} - > - { - setNewFilamentSkuOpen(false) - tableRef.current?.reload() - }} - reset={newFilamentSkuOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx b/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx index bfb95f6a..f0eaee4d 100644 --- a/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx +++ b/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' @@ -15,10 +16,12 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const FilamentSkuInfo = () => { @@ -42,9 +45,23 @@ const FilamentSkuInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, filamentSkuId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -58,10 +75,6 @@ const FilamentSkuInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -76,6 +89,7 @@ const FilamentSkuInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -175,9 +185,14 @@ const FilamentSkuInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Filaments.jsx b/src/components/Dashboard/Management/Filaments.jsx index 13e054d3..53c91516 100644 --- a/src/components/Dashboard/Management/Filaments.jsx +++ b/src/components/Dashboard/Management/Filaments.jsx @@ -1,27 +1,29 @@ // src/filaments.js -import { useRef, useState } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { useRef } from 'react' +import { Flex, Space } from 'antd' -import NewFilament from './Filaments/NewFilament' import useColumnVisibility from '../hooks/useColumnVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' -import useViewMode from '../hooks/useViewMode' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' const Filaments = () => { - const [newFilamentOpen, setNewFilamentOpen] = useState(false) const tableRef = useRef() // View mode state (cards/list), persisted in sessionStorage via custom hook - const [viewMode, setViewMode] = useViewMode('filament') const [columnVisibility, setColumnVisibility] = useColumnVisibility('filament') @@ -29,37 +31,27 @@ const Filaments = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Filaments') - const actionItems = { - items: [ - { - label: 'New Filament', - key: 'newFilament', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newFilament') { - setNewFilamentOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Filaments') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewFilamentOpen(false) - }} - > - { - setNewFilamentOpen(false) - tableRef.current?.reload() - }} - reset={newFilamentOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx index dccc9047..e24660a4 100644 --- a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx +++ b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' @@ -18,13 +18,15 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import FilamentIcon from '../../../Icons/FilamentIcon.jsx' import FilamentSkuIcon from '../../../Icons/FilamentSkuIcon.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' const log = loglevel.getLogger('FilamentInfo') @@ -61,9 +63,23 @@ const FilamentInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, filamentId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -77,10 +93,6 @@ const FilamentInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -97,6 +109,7 @@ const FilamentInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -211,7 +220,7 @@ const FilamentInfo = () => { )} @@ -229,7 +238,7 @@ const FilamentInfo = () => { ) : ( { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Files.jsx b/src/components/Dashboard/Management/Files.jsx index e79593f3..86f76400 100644 --- a/src/components/Dashboard/Management/Files.jsx +++ b/src/components/Dashboard/Management/Files.jsx @@ -1,48 +1,49 @@ import { useRef } from 'react' -import { Button, Flex, Space, Dropdown } from 'antd' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Files = () => { const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('file') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('file') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Files') - const actionItems = { - items: [ - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Files') return ( - <> - - + + + - - - + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + { useSortInUrl={true} /> - + ) } diff --git a/src/components/Dashboard/Management/Files/FileInfo.jsx b/src/components/Dashboard/Management/Files/FileInfo.jsx index 487eaaee..e6f3c0be 100644 --- a/src/components/Dashboard/Management/Files/FileInfo.jsx +++ b/src/components/Dashboard/Management/Files/FileInfo.jsx @@ -1,6 +1,7 @@ -import { useContext, useRef, useState } from 'react' +import { useContext, useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' import useCollapseState from '../../hooks/useCollapseState.jsx' @@ -17,9 +18,10 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import FileIcon from '../../../Icons/FileIcon.jsx' import FilePreview from '../../common/FilePreview.jsx' @@ -28,7 +30,10 @@ import { ApiServerContext } from '../../context/ApiServerContext.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import JsonObjectIcon from '../../../Icons/JsonObjectIcon.jsx' import ObjectProperty from '../../common/ObjectProperty.jsx' -import { getModelProperty } from '../../../../database/ObjectModels.js' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' const log = loglevel.getLogger('FileInfo') log.setLevel(config.logLevel) @@ -49,12 +54,26 @@ const FileInfo = () => { formValid: false, lock: null, loading: true, + editDisabled: false, objectData: { _id: fileId } }) const { fetchFileContent } = useContext(ApiServerContext) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, fileId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -71,10 +90,6 @@ const FileInfo = () => { download: () => { fetchFileContent(objectFormState?.objectData, true) return true - }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true } } @@ -90,6 +105,7 @@ const FileInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -194,12 +206,20 @@ const FileInfo = () => { collapseKey='preview' > {objectFormState?.objectData?._id ? ( - - - + } + > + + + + ) : ( )} @@ -229,9 +249,14 @@ const FileInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - - - + } + > + + + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Hosts.jsx b/src/components/Dashboard/Management/Hosts.jsx index a4d18045..f143788c 100644 --- a/src/components/Dashboard/Management/Hosts.jsx +++ b/src/components/Dashboard/Management/Hosts.jsx @@ -1,64 +1,56 @@ // src/hosts.js -import { useRef, useState } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { useRef } from 'react' +import { Flex, Space } from 'antd' -import NewHost from './Hosts/NewHost' import useColumnVisibility from '../hooks/useColumnVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' -import useViewMode from '../hooks/useViewMode' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' const Hosts = () => { - const [newHostOpen, setNewHostOpen] = useState(false) const tableRef = useRef() // View mode state (cards/list), persisted in sessionStorage via custom hook - const [viewMode, setViewMode] = useViewMode('host') const [columnVisibility, setColumnVisibility] = useColumnVisibility('host') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Hosts') - const actionItems = { - items: [ - { - label: 'New Host', - key: 'newHost', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newHost') { - setNewHostOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Hosts') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewHostOpen(false) - }} - > - { - setNewHostOpen(false) - tableRef.current?.reload() - }} - reset={newHostOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Management/Hosts/HostInfo.jsx b/src/components/Dashboard/Management/Hosts/HostInfo.jsx index cade8247..6fd010b7 100644 --- a/src/components/Dashboard/Management/Hosts/HostInfo.jsx +++ b/src/components/Dashboard/Management/Hosts/HostInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,13 +18,15 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import PrinterIcon from '../../../Icons/PrinterIcon.jsx' import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('HostInfo') @@ -35,7 +37,6 @@ const HostInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -57,9 +58,23 @@ const HostInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, hostId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -73,203 +88,202 @@ const HostInfo = () => { objectFormRef?.current.handleUpdate() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( - - + + + - - + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + - - - - - - - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + style={{ height: '100%' }} + ref={objectFormRef} + setCurrentObject={true} + onStateChange={(state) => { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => { - return ( - } - isEditing={isEditing} - type='host' - objectData={objectData} - labelWidth='175px' - /> - ) - }} - - - - - } - active={collapseState.printers} - onToggle={(expanded) => updateCollapseState('printers', expanded)} - collapseKey='printers' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - } - active={collapseState.documentPrinters} - onToggle={(expanded) => - updateCollapseState('documentPrinters', expanded) - } - collapseKey='documentPrinters' - > - {objectFormState.loading ? ( - - ) : ( - - )} + {({ loading, isEditing, objectData }) => { + return ( + } + isEditing={isEditing} + type='host' + objectData={objectData} + labelWidth='175px' + /> + ) + }} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.printers} + onToggle={(expanded) => updateCollapseState('printers', expanded)} + collapseKey='printers' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + } + active={collapseState.documentPrinters} + onToggle={(expanded) => + updateCollapseState('documentPrinters', expanded) + } + collapseKey='documentPrinters' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Management/Hosts/NewHost.jsx b/src/components/Dashboard/Management/Hosts/NewHost.jsx index 61c928e4..b6691571 100644 --- a/src/components/Dashboard/Management/Hosts/NewHost.jsx +++ b/src/components/Dashboard/Management/Hosts/NewHost.jsx @@ -40,14 +40,16 @@ const NewHost = ({ onOk }) => { _id: false, createdAt: false, updatedAt: false, - operatingSystem: false, + 'deviceInfo.os.platform': false, 'deviceInfo.os': false, 'deviceInfo.os.hostname': false, 'deviceInfo.cpu': false, 'deviceInfo.cpu.model': false, 'deviceInfo.user.username': false, 'deviceInfo.user.homedir': false, + 'deviceInfo.user.shell': false, 'deviceInfo.process.nodeVersion': false, + 'deviceInfo.process.execPath': false, files: false }} /> @@ -66,14 +68,16 @@ const NewHost = ({ onOk }) => { createdAt: false, _reference: false, updatedAt: false, - operatingSystem: false, + 'deviceInfo.os.platform': false, 'deviceInfo.os': false, 'deviceInfo.os.hostname': false, 'deviceInfo.cpu': false, 'deviceInfo.cpu.model': false, 'deviceInfo.user.username': false, 'deviceInfo.user.homedir': false, + 'deviceInfo.user.shell': false, 'deviceInfo.process.nodeVersion': false, + 'deviceInfo.process.execPath': false, files: false, connectedAt: false, online: false diff --git a/src/components/Dashboard/Management/Materials.jsx b/src/components/Dashboard/Management/Materials.jsx index d3fd4786..da714340 100644 --- a/src/components/Dashboard/Management/Materials.jsx +++ b/src/components/Dashboard/Management/Materials.jsx @@ -1,62 +1,53 @@ -import { useRef, useState } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { useRef } from 'react' +import { Flex, Space } from 'antd' -import NewMaterial from './Materials/NewMaterial' import useColumnVisibility from '../hooks/useColumnVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Materials = () => { - const [newMaterialOpen, setNewMaterialOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('material') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('material') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Materials') - const actionItems = { - items: [ - { - label: 'New Material', - key: 'newMaterial', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newMaterial') { - setNewMaterialOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Materials') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewMaterialOpen(false) - }} - > - { - setNewMaterialOpen(false) - tableRef.current?.reload() - }} - reset={newMaterialOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Management/Materials/MaterialInfo.jsx b/src/components/Dashboard/Management/Materials/MaterialInfo.jsx index 5617b97e..9c3a8a9c 100644 --- a/src/components/Dashboard/Management/Materials/MaterialInfo.jsx +++ b/src/components/Dashboard/Management/Materials/MaterialInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('MaterialInfo') @@ -45,9 +48,23 @@ const MaterialInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, materialId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -61,10 +78,6 @@ const MaterialInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -79,6 +92,7 @@ const MaterialInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -178,9 +188,14 @@ const MaterialInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/NoteTypes.jsx b/src/components/Dashboard/Management/NoteTypes.jsx index 10c0307d..cacb61a6 100644 --- a/src/components/Dashboard/Management/NoteTypes.jsx +++ b/src/components/Dashboard/Management/NoteTypes.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewNoteType from './NoteTypes/NewNoteType' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const NoteTypes = () => { - const [newNoteTypeOpen, setNewNoteTypeOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('noteType') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('noteType') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('NoteTypes') - const actionItems = { - items: [ - { - label: 'New Note Type', - key: 'newNoteType', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newNoteType') { - setNewNoteTypeOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('NoteTypes') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewNoteTypeOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewNoteTypeOpen(false) - tableRef.current?.reload() - }} - reset={!newNoteTypeOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx index 584b9af1..c8207c2e 100644 --- a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx +++ b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' import { Flex, Space } from 'antd' import { LoadingOutlined } from '@ant-design/icons' @@ -14,10 +14,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const NoteTypeInfo = () => { @@ -38,9 +40,23 @@ const NoteTypeInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, noteTypeId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -68,6 +84,7 @@ const NoteTypeInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -174,8 +187,13 @@ const NoteTypeInfo = () => { ) : ( )} diff --git a/src/components/Dashboard/Management/Notes/NewNote.jsx b/src/components/Dashboard/Management/Notes/NewNote.jsx index bb43c984..980ed4f1 100644 --- a/src/components/Dashboard/Management/Notes/NewNote.jsx +++ b/src/components/Dashboard/Management/Notes/NewNote.jsx @@ -15,7 +15,7 @@ const NewNote = ({ onOk, defaultValues = {} }) => { { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, noteId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -63,10 +78,6 @@ const NoteInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -81,6 +92,7 @@ const NoteInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -207,9 +215,14 @@ const NoteInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/PartSkus.jsx b/src/components/Dashboard/Management/PartSkus.jsx index 2d0e3713..4ec907d0 100644 --- a/src/components/Dashboard/Management/PartSkus.jsx +++ b/src/components/Dashboard/Management/PartSkus.jsx @@ -1,62 +1,52 @@ -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewPartSku from './PartSkus/NewPartSku' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const PartSkus = () => { const tableRef = useRef() - const [newPartSkuOpen, setNewPartSkuOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('partSkus') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('partSku') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('PartSkus') - const actionItems = { - items: [ - { - label: 'New Part SKU', - key: 'newPartSku', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPartSku') { - setNewPartSkuOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PartSkus') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewPartSkuOpen(false) - }} - destroyOnHidden={true} - > - { - setNewPartSkuOpen(false) - tableRef.current?.reload() - }} - reset={newPartSkuOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx b/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx index 40a43080..5314e385 100644 --- a/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx +++ b/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' @@ -15,10 +16,12 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const PartSkuInfo = () => { @@ -37,9 +40,23 @@ const PartSkuInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, partSkuId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -53,10 +70,6 @@ const PartSkuInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -71,6 +84,7 @@ const PartSkuInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -170,9 +180,14 @@ const PartSkuInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Parts.jsx b/src/components/Dashboard/Management/Parts.jsx index 72791bcf..c97847d7 100644 --- a/src/components/Dashboard/Management/Parts.jsx +++ b/src/components/Dashboard/Management/Parts.jsx @@ -1,64 +1,56 @@ // src/gcodefiles.js -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import NewPart from './Parts/NewPart' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' -import useViewMode from '../hooks/useViewMode' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Parts = (filter) => { - const [newPartOpen, setNewPartOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('part') const [columnVisibility, setColumnVisibility] = useColumnVisibility('part') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Parts') - const actionItems = { - items: [ - { - label: 'New Part', - key: 'newPart', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPart') { - setNewPartOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Parts') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewPartOpen(false) - }} - destroyOnHidden={true} - > - { - setNewPartOpen(false) - tableRef.current?.reload() - }} - reset={newPartOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/Parts/PartInfo.jsx b/src/components/Dashboard/Management/Parts/PartInfo.jsx index 65267906..e6622a7f 100644 --- a/src/components/Dashboard/Management/Parts/PartInfo.jsx +++ b/src/components/Dashboard/Management/Parts/PartInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' @@ -15,12 +16,14 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import PartSkuIcon from '../../../Icons/PartSkuIcon.jsx' const PartInfo = () => { @@ -30,7 +33,6 @@ const PartInfo = () => { const partId = new URLSearchParams(location.search).get('partId') const partSkusTableRef = useRef() - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -50,9 +52,23 @@ const PartInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, partId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -69,164 +85,165 @@ const PartInfo = () => { } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - - - )} - - } - active={collapseState.partSkus} - onToggle={(expanded) => - updateCollapseState('partSkus', expanded) - } - collapseKey='partSkus' - > - {objectFormState.loading ? ( - - ) : ( - ( + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + - )} - - + + )} + } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' - > - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' + title='Part SKUs' + icon={} + active={collapseState.partSkus} + onToggle={(expanded) => updateCollapseState('partSkus', expanded)} + collapseKey='partSkus' > {objectFormState.loading ? ( ) : ( )} - - - + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Management/PermissionSettings.jsx b/src/components/Dashboard/Management/PermissionSettings.jsx new file mode 100644 index 00000000..654a719c --- /dev/null +++ b/src/components/Dashboard/Management/PermissionSettings.jsx @@ -0,0 +1,96 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const PermissionSettingsList = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('permissionSetting') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('PermissionSettings') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PermissionSettings') + + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default PermissionSettingsList diff --git a/src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx b/src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx new file mode 100644 index 00000000..3823d35d --- /dev/null +++ b/src/components/Dashboard/Management/PermissionSettings/NewPermissionSetting.jsx @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewPermissionSetting = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewPermissionSetting.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewPermissionSetting diff --git a/src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx b/src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx new file mode 100644 index 00000000..fac61bf8 --- /dev/null +++ b/src/components/Dashboard/Management/PermissionSettings/PermissionSettingInfo.jsx @@ -0,0 +1,268 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelByName, + getModelProperty +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import PermissionSettingIcon from '../../../Icons/PermissionSettingIcon.jsx' + +const PermissionSettingInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const permissionSettingId = new URLSearchParams(location.search).get( + 'permissionSettingId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'PermissionSettingInfo', + { + info: true, + notes: true, + matrix: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, permissionSettingId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + }, + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='permissionSetting' + objectData={objectData} + visibleProperties={{ + permissions: false + }} + /> + + } + active={collapseState.matrix} + onToggle={(expanded) => + updateCollapseState('matrix', expanded) + } + collapseKey='matrix' + > + }> + + + + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default PermissionSettingInfo diff --git a/src/components/Dashboard/Management/ProductCategories.jsx b/src/components/Dashboard/Management/ProductCategories.jsx index 89d23c4a..dfa2a0a5 100644 --- a/src/components/Dashboard/Management/ProductCategories.jsx +++ b/src/components/Dashboard/Management/ProductCategories.jsx @@ -1,62 +1,53 @@ -import { useRef, useState } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { useRef } from 'react' +import { Flex, Space } from 'antd' -import NewProductCategory from './ProductCategories/NewProductCategory' import useColumnVisibility from '../hooks/useColumnVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const ProductCategories = () => { - const [newProductCategoryOpen, setNewProductCategoryOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('productCategory') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('productCategory') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('ProductCategories') - const actionItems = { - items: [ - { - label: 'New Product Category', - key: 'newProductCategory', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newProductCategory') { - setNewProductCategoryOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ProductCategories') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewProductCategoryOpen(false) - }} - > - { - setNewProductCategoryOpen(false) - tableRef.current?.reload() - }} - reset={newProductCategoryOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx index 7edaf30f..1904db1c 100644 --- a/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx +++ b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx @@ -3,9 +3,9 @@ import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' -const NewProductCategory = ({ onOk }) => { +const NewProductCategory = ({ onOk, defaultValues }) => { return ( - + {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ { @@ -23,6 +23,21 @@ const NewProductCategory = ({ onOk }) => { /> ) }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, { title: 'Summary', key: 'summary', @@ -39,7 +54,7 @@ const NewProductCategory = ({ onOk }) => { }} isEditing={false} objectData={objectData} - labelWidth={70} + labelWidth={120} /> ) } @@ -65,7 +80,8 @@ const NewProductCategory = ({ onOk }) => { NewProductCategory.propTypes = { onOk: PropTypes.func.isRequired, - reset: PropTypes.bool + reset: PropTypes.bool, + defaultValues: PropTypes.object } export default NewProductCategory diff --git a/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx index 7b9c272c..6361c5c1 100644 --- a/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx +++ b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx @@ -1,24 +1,29 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' import ObjectForm from '../../common/ObjectForm' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const ProductCategoryInfo = () => { @@ -32,6 +37,7 @@ const ProductCategoryInfo = () => { 'ProductCategoryInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false } @@ -42,9 +48,23 @@ const ProductCategoryInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, productCategoryId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -58,10 +78,6 @@ const ProductCategoryInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -76,6 +92,7 @@ const ProductCategoryInfo = () => { { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Product Category Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -95,7 +113,7 @@ const ProductCategoryInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -140,33 +154,56 @@ const ProductCategoryInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/ProductSkus.jsx b/src/components/Dashboard/Management/ProductSkus.jsx index d8cb56e5..92d8d88b 100644 --- a/src/components/Dashboard/Management/ProductSkus.jsx +++ b/src/components/Dashboard/Management/ProductSkus.jsx @@ -1,63 +1,53 @@ -import { useState, useRef } from 'react' +import { useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import { Flex, Space } from 'antd' -import NewProductSku from './ProductSkus/NewProductSku' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const ProductSkus = () => { const tableRef = useRef() - const [newProductSkuOpen, setNewProductSkuOpen] = useState(false) - - const [viewMode, setViewMode] = useViewMode('productSkus') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('productSku') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('ProductSkus') - const actionItems = { - items: [ - { - label: 'New Product SKU', - key: 'newProductSku', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newProductSku') { - setNewProductSkuOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ProductSkus') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewProductSkuOpen(false) - }} - destroyOnHidden={true} - > - { - setNewProductSkuOpen(false) - tableRef.current?.reload() - }} - reset={newProductSkuOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx index 443f39a4..72bc80bb 100644 --- a/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx +++ b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' @@ -15,13 +16,14 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import ObjectProperty from '../../common/ObjectProperty.jsx' -import { getModelProperty } from '../../../../database/ObjectModels.js' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import PartIcon from '../../../Icons/PartIcon.jsx' const ProductSkuInfo = () => { @@ -44,9 +46,23 @@ const ProductSkuInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, productSkuId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -60,10 +76,6 @@ const ProductSkuInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -78,6 +90,7 @@ const ProductSkuInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -203,9 +212,14 @@ const ProductSkuInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/Products.jsx b/src/components/Dashboard/Management/Products.jsx index be5eae6d..343d7264 100644 --- a/src/components/Dashboard/Management/Products.jsx +++ b/src/components/Dashboard/Management/Products.jsx @@ -1,12 +1,11 @@ // src/gcodefiles.js -import { useState, useRef } from 'react' +import { useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, - Modal, Dropdown, Tag, Checkbox, @@ -17,27 +16,28 @@ import { DownloadOutlined } from '@ant-design/icons' import IdDisplay from '../common/IdDisplay' import TimeDisplay from '../common/TimeDisplay' import ObjectTable from '../common/ObjectTable' -import NewProduct from './Products/NewProduct' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ProductIcon from '../../Icons/ProductIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Products = () => { const navigate = useNavigate() - const [newProductOpen, setNewProductOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('Products') - const getProductActionItems = (id) => { return { items: [ @@ -239,28 +239,9 @@ const Products = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Products') - const actionItems = { - items: [ - { - label: 'New Product', - key: 'newProduct', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newProduct') { - setNewProductOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Products') + const getFilterDropdown = ({ setSelectedKeys, @@ -323,14 +304,24 @@ const Products = () => { } return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + { - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewProductOpen(false) - }} - destroyOnHidden={true} - > - { - setNewProductOpen(false) - tableRef.current?.reload() - }} - reset={newProductOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx index b0d8fdbe..1bc31fdc 100644 --- a/src/components/Dashboard/Management/Products/ProductInfo.jsx +++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' @@ -16,11 +17,13 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ProductSkuIcon from '../../../Icons/ProductSkuIcon.jsx' const ProductInfo = () => { @@ -36,7 +39,6 @@ const ProductInfo = () => { }) const productSkusTableRef = useRef() - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -50,9 +52,23 @@ const ProductInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, productId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -69,167 +85,172 @@ const ProductInfo = () => { } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.productSkus} + onToggle={(expanded) => + updateCollapseState('productSkus', expanded) + } + collapseKey='productSkus' + > + {objectFormState.loading ? ( + + ) : ( + - - } - active={collapseState.productSkus} - onToggle={(expanded) => - updateCollapseState('productSkus', expanded) - } - collapseKey='productSkus' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - )} - - + )} + + + )} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Management/Settings.jsx b/src/components/Dashboard/Management/Settings.jsx index ecb918b1..4b594525 100644 --- a/src/components/Dashboard/Management/Settings.jsx +++ b/src/components/Dashboard/Management/Settings.jsx @@ -1,10 +1,8 @@ import { useContext, useEffect, useMemo, useState } from 'react' -import { Descriptions, Flex, Select, Space, Spin, Typography } from 'antd' -import { LoadingOutlined, SettingOutlined } from '@ant-design/icons' +import { Flex, Space } from 'antd' import { useThemeContext } from '../context/ThemeContext' import { ApiServerContext } from '../context/ApiServerContext' import { ElectronContext } from '../context/ElectronContext' -import { AuthContext } from '../context/AuthContext' import { normalizeAppUpdateEngine, useAppUpdateContext @@ -12,155 +10,217 @@ import { import { useMessageContext } from '../context/MessageContext' import useCollapseState from '../hooks/useCollapseState' import InfoCollapse from '../common/InfoCollapse' +import ObjectInfo from '../common/ObjectInfo' import ViewButton from '../common/ViewButton' import EditButtons from '../common/EditButtons' +import ScrollBox from '../common/ScrollBox' +import settingsSchema, { + APP_SETTING_NAMES, + USER_SETTING_NAMES, + areSettingsValid, + getEffectiveAppearance, + getSettingsDefaults, + getVisibleSettingsSections, + pickSettings +} from '../../../database/Settings' -const { Text } = Typography -const { Option } = Select const DEFAULT_UPDATE_BRANCH = 'main' const DEFAULT_UPDATE_ENGINE = 'native' - -const engineLabel = (engine) => { - const normalized = normalizeAppUpdateEngine(engine) - if (normalized === 'chromium') return 'Chromium' - if (normalized === 'native') return 'Native' - return 'Not configured' -} +const LEGACY_ELECTRON_USER_KEYS = [ + 'theme', + 'density', + 'showNavigationLabels', + 'dateTimeFormat', + 'timezone' +] const Settings = () => { - const { isDarkMode, isCompact, isSystem, setThemeMode, setDensityMode } = - useThemeContext() - const { fetchAppUpdateBranches } = useContext(ApiServerContext) + const { + isDarkMode, + isCompact, + isSystem, + setThemeMode, + setDensityMode, + showNavigationLabels, + setShowNavigationLabels + } = useThemeContext() + const { + connected, + fetchAppUpdateBranches, + updateUserSettings, + userSettings, + userSettingsLoaded + } = useContext(ApiServerContext) const { isElectron, getAppSettings, setAppSettings, getAppEngine } = useContext(ElectronContext) const { recheckForUpdates } = useAppUpdateContext() - const { userProfile, setUserProfile } = useContext(AuthContext) const { showSuccess, showError } = useMessageContext() - const [collapseState, updateCollapseState] = useCollapseState('Settings', { - appearance: true, - appUpdates: true - }) + const visibleSections = useMemo( + () => getVisibleSettingsSections({ isElectron }), + [isElectron] + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'Settings', + Object.fromEntries( + settingsSchema.sections.map((section) => [section.key, true]) + ) + ) const [isEditing, setIsEditing] = useState(false) const [settingsLoading, setSettingsLoading] = useState(true) const [saving, setSaving] = useState(false) - const [appSettings, setAppSettingsState] = useState({}) + const [savedSettings, setSavedSettings] = useState({}) const [draftSettings, setDraftSettings] = useState({}) + const [electronSettings, setElectronSettings] = useState({}) const [branches, setBranches] = useState([]) - const [branchLoading, setBranchLoading] = useState(false) - const [runningEngine, setRunningEngine] = useState(DEFAULT_UPDATE_ENGINE) + + const currentThemeValue = isSystem ? 'system' : isDarkMode ? 'dark' : 'light' + const currentDensityValue = isCompact ? 'compact' : 'comfortable' + const userAppearance = userSettings?.appearance || {} + + const resolvedSettings = useMemo(() => { + const defaults = getSettingsDefaults({ isElectron }) + const stored = isEditing ? draftSettings : savedSettings + const defaultBranch = branches.includes(DEFAULT_UPDATE_BRANCH) + ? DEFAULT_UPDATE_BRANCH + : branches[0] + + return { + ...defaults, + theme: currentThemeValue, + density: currentDensityValue, + showNavigationLabels: showNavigationLabels ?? false, + ...(isElectron + ? { + appTheme: stored.appTheme || currentThemeValue, + appShowNavigationLabels: + stored.appShowNavigationLabels ?? showNavigationLabels ?? false, + appUpdateBranch: defaultBranch, + appUpdateEngine: + normalizeAppUpdateEngine(stored.appUpdateEngine) || + DEFAULT_UPDATE_ENGINE + } + : {}), + ...stored + } + }, [ + branches, + currentDensityValue, + currentThemeValue, + draftSettings, + isEditing, + isElectron, + savedSettings, + showNavigationLabels + ]) + + const applyAppearance = (nextSettings, nextElectronSettings = electronSettings) => { + const effective = getEffectiveAppearance({ + isElectron, + userAppearance: nextSettings, + electronSettings: { + ...nextElectronSettings, + appTheme: nextSettings.appTheme, + appShowNavigationLabels: nextSettings.appShowNavigationLabels, + overrideTheme: nextSettings.theme === 'app' + } + }) + if (effective.theme) setThemeMode(effective.theme) + if (effective.density) setDensityMode(effective.density) + if (effective.showNavigationLabels !== undefined) { + setShowNavigationLabels(effective.showNavigationLabels) + } + } useEffect(() => { + if (!userSettingsLoaded || isEditing) return + const loadSettings = async () => { setSettingsLoading(true) - const [storedSettings, detectedEngine] = await Promise.all([ - isElectron ? getAppSettings() : Promise.resolve(userProfile?.settings || {}), + const [storedElectronSettings, detectedEngine] = await Promise.all([ + isElectron ? getAppSettings() : Promise.resolve({}), isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE) ]) const nextEngine = normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE - setRunningEngine(nextEngine) - - const nextSettings = { ...(storedSettings || {}) } - if (isElectron && !normalizeAppUpdateEngine(nextSettings.appUpdateEngine)) { - nextSettings.appUpdateEngine = nextEngine + const nextElectronSettings = { ...(storedElectronSettings || {}) } + if ( + isElectron && + !normalizeAppUpdateEngine(nextElectronSettings.appUpdateEngine) + ) { + nextElectronSettings.appUpdateEngine = nextEngine } - setAppSettingsState(nextSettings) + const nextSettings = { + ...getSettingsDefaults({ isElectron }), + ...pickSettings(userAppearance, USER_SETTING_NAMES), + ...(isElectron + ? { + ...pickSettings(nextElectronSettings, APP_SETTING_NAMES), + appTheme: + nextElectronSettings.appTheme || nextElectronSettings.theme, + appShowNavigationLabels: + nextElectronSettings.appShowNavigationLabels ?? + nextElectronSettings.showNavigationLabels, + theme: nextElectronSettings.overrideTheme + ? 'app' + : userAppearance.theme + } + : {}) + } + + setElectronSettings(nextElectronSettings) + setSavedSettings(nextSettings) setDraftSettings(nextSettings) + applyAppearance(nextSettings, nextElectronSettings) setSettingsLoading(false) } loadSettings() - }, [getAppEngine, getAppSettings, isElectron, userProfile?.settings]) - - useEffect(() => { - if (settingsLoading || isEditing) return - if (appSettings.theme) setThemeMode(appSettings.theme) - if (appSettings.density) setDensityMode(appSettings.density) + // eslint-disable-next-line react-hooks/exhaustive-deps -- reload when stored appearance/app settings change }, [ - appSettings.density, - appSettings.theme, + getAppEngine, + getAppSettings, isEditing, - setDensityMode, - setThemeMode, - settingsLoading + isElectron, + userAppearance.dateTimeFormat, + userAppearance.density, + userAppearance.showNavigationLabels, + userAppearance.theme, + userAppearance.timezone, + userSettingsLoaded ]) useEffect(() => { if (!isElectron) { setBranches([]) - setBranchLoading(false) return } const loadBranches = async () => { - setBranchLoading(true) const availableBranches = await fetchAppUpdateBranches() setBranches(availableBranches) setDraftSettings((previous) => { if (previous.appUpdateBranch) return previous - const defaultBranch = availableBranches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : availableBranches[0] - return defaultBranch ? { ...previous, appUpdateBranch: defaultBranch } : previous }) - - setBranchLoading(false) } loadBranches() }, [fetchAppUpdateBranches, isElectron]) - const branchOptions = useMemo( - () => - branches.map((branch) => ( - - )), - [branches] - ) - const viewItems = [ - { key: 'appearance', label: 'Appearance Settings' }, - ...(isElectron ? [{ key: 'appUpdates', label: 'App Update Settings' }] : []) - ] - - const getCurrentThemeValue = () => { - if (isSystem) return 'system' - return isDarkMode ? 'dark' : 'light' - } - - const currentThemeValue = getCurrentThemeValue() - const currentDensityValue = isCompact ? 'compact' : 'comfortable' - const currentBranch = - appSettings.appUpdateBranch || - (branches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : null) || - branches[0] || - 'Not configured' - const currentEngine = - normalizeAppUpdateEngine(appSettings.appUpdateEngine) || - normalizeAppUpdateEngine(runningEngine) || - DEFAULT_UPDATE_ENGINE - const startEditing = () => { - setDraftSettings({ - ...appSettings, - appUpdateBranch: - currentBranch === 'Not configured' ? undefined : currentBranch, - appUpdateEngine: currentEngine, - theme: currentThemeValue, - density: currentDensityValue - }) + setDraftSettings(resolvedSettings) setIsEditing(true) } const cancelEditing = () => { - setDraftSettings(appSettings) + setDraftSettings(savedSettings) setIsEditing(false) } @@ -168,52 +228,67 @@ const Settings = () => { setSaving(true) try { + const useAppTheme = isElectron && draftSettings.theme === 'app' const nextEngine = normalizeAppUpdateEngine(draftSettings.appUpdateEngine) || - currentEngine + DEFAULT_UPDATE_ENGINE const nextSettings = { - ...appSettings, - theme: draftSettings.theme, - density: draftSettings.density, + ...getSettingsDefaults({ isElectron }), + ...savedSettings, + ...draftSettings, + showNavigationLabels: Boolean(draftSettings.showNavigationLabels), ...(isElectron ? { + appShowNavigationLabels: Boolean( + draftSettings.appShowNavigationLabels + ), appUpdateBranch: draftSettings.appUpdateBranch, appUpdateEngine: nextEngine } : {}) } - const saved = isElectron - ? await setAppSettings(nextSettings) - : Boolean(userProfile) - if (!saved) { + const userUpdates = USER_SETTING_NAMES.filter( + (name) => !(name === 'theme' && useAppTheme) + ).map((name) => updateUserSettings('appearance', name, nextSettings[name])) + + const userResults = await Promise.all(userUpdates) + if (connected && userResults.some((result) => result == null)) { showError('Unable to save settings.') return } - if (!isElectron) { - setUserProfile((previous) => ({ - ...previous, - settings: { - ...(previous?.settings || {}), - theme: draftSettings.theme, - density: draftSettings.density - } - })) + let nextElectronSettings = electronSettings + if (isElectron) { + nextElectronSettings = { ...electronSettings } + for (const key of LEGACY_ELECTRON_USER_KEYS) { + delete nextElectronSettings[key] + } + nextElectronSettings = { + ...nextElectronSettings, + ...pickSettings(nextSettings, APP_SETTING_NAMES), + overrideTheme: useAppTheme + } + + const saved = await setAppSettings(nextElectronSettings) + if (!saved) { + showError('Unable to save settings.') + return + } } - setThemeMode(draftSettings.theme) - setDensityMode(draftSettings.density) - setAppSettingsState(nextSettings) + applyAppearance(nextSettings, nextElectronSettings) + setElectronSettings(nextElectronSettings) + setSavedSettings(nextSettings) setDraftSettings(nextSettings) setIsEditing(false) showSuccess('Settings saved.') const appUpdateSettingsChanged = isElectron && - (nextSettings.appUpdateBranch !== appSettings.appUpdateBranch || + (nextSettings.appUpdateBranch !== savedSettings.appUpdateBranch || nextSettings.appUpdateEngine !== - normalizeAppUpdateEngine(appSettings.appUpdateEngine)) + normalizeAppUpdateEngine(savedSettings.appUpdateEngine)) if (appUpdateSettingsChanged) { void recheckForUpdates() @@ -229,7 +304,10 @@ const Settings = () => { ({ + key: section.key, + label: section.name + }))} visibleState={collapseState} updateVisibleState={updateCollapseState} /> @@ -239,140 +317,51 @@ const Settings = () => { handleUpdate={handleSave} cancelEditing={cancelEditing} startEditing={startEditing} - formValid={ - Boolean(draftSettings.theme && draftSettings.density) && - (!isElectron || - (Boolean(draftSettings.appUpdateBranch) && - Boolean(normalizeAppUpdateEngine(draftSettings.appUpdateEngine)))) - } - disabled={settingsLoading || (!isElectron && !userProfile)} + formValid={areSettingsValid(draftSettings, { isElectron })} + disabled={settingsLoading || !userSettingsLoaded} loading={saving} /> -
- }> - - } - active={collapseState.appearance} - onToggle={(expanded) => - updateCollapseState('appearance', expanded) - } - collapseKey='appearance' - > - - - {isEditing ? ( - - ) : ( - {currentThemeValue} - )} - - - {isEditing ? ( - - ) : ( - {isCompact ? 'Compact' : 'Comfortable'} - )} - - - - {isElectron && ( + + + {visibleSections.map((section) => { + const Icon = section.icon + return ( } - active={collapseState.appUpdates} + key={section.key} + title={section.name} + icon={} + active={collapseState[section.key]} onToggle={(expanded) => - updateCollapseState('appUpdates', expanded) + updateCollapseState(section.key, expanded) } - collapseKey='appUpdates' + collapseKey={section.key} > - - - {isEditing ? ( - - ) : ( - {currentBranch} - )} - - - {isEditing ? ( - - ) : ( - {engineLabel(currentEngine)} - )} - - + : undefined + } + {...(section.column ? { column: section.column } : {})} + /> - )} - - -
+ ) + })} + +
) } diff --git a/src/components/Dashboard/Management/StockAuditLevels.jsx b/src/components/Dashboard/Management/StockAuditLevels.jsx new file mode 100644 index 00000000..4e643ec7 --- /dev/null +++ b/src/components/Dashboard/Management/StockAuditLevels.jsx @@ -0,0 +1,95 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const StockAuditLevels = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('stockAuditLevel') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('StockAuditLevels') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('StockAuditLevels') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default StockAuditLevels diff --git a/src/components/Dashboard/Management/StockAuditLevels/NewStockAuditLevel.jsx b/src/components/Dashboard/Management/StockAuditLevels/NewStockAuditLevel.jsx new file mode 100644 index 00000000..0186f527 --- /dev/null +++ b/src/components/Dashboard/Management/StockAuditLevels/NewStockAuditLevel.jsx @@ -0,0 +1,80 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewStockAuditLevel = ({ onOk, reset, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewStockAuditLevel.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewStockAuditLevel diff --git a/src/components/Dashboard/Management/StockAuditLevels/StockAuditLevelInfo.jsx b/src/components/Dashboard/Management/StockAuditLevels/StockAuditLevelInfo.jsx new file mode 100644 index 00000000..8738b7c1 --- /dev/null +++ b/src/components/Dashboard/Management/StockAuditLevels/StockAuditLevelInfo.jsx @@ -0,0 +1,261 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Flex, Space } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import StockAuditLevelIcon from '../../../Icons/StockAuditLevelIcon.jsx' +import StockAuditIcon from '../../../Icons/StockAuditIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const StockAuditLevelInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const stockAuditLevelId = new URLSearchParams(location.search).get( + 'stockAuditLevelId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'StockAuditLevelInfo', + { + info: true, + auditLines: true, + stockAudits: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, stockAuditLevelId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='stockAuditLevel' + objectData={objectData} + visibleProperties={{ auditLines: false }} + /> + + } + active={collapseState.auditLines} + onToggle={(expanded) => + updateCollapseState('auditLines', expanded) + } + collapseKey='auditLines' + > + + + + )} + + + } + active={collapseState.stockAudits} + onToggle={(expanded) => + updateCollapseState('stockAudits', expanded) + } + collapseKey='stockAudits' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + ) +} + +export default StockAuditLevelInfo diff --git a/src/components/Dashboard/Management/TaxRates.jsx b/src/components/Dashboard/Management/TaxRates.jsx index 304c4a5e..4d66a17c 100644 --- a/src/components/Dashboard/Management/TaxRates.jsx +++ b/src/components/Dashboard/Management/TaxRates.jsx @@ -1,59 +1,50 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewTaxRate from './TaxRates/NewTaxRate' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const TaxRates = () => { - const [newTaxRateOpen, setNewTaxRateOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('taxRate') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('taxRate') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('TaxRates') - const actionItems = { - items: [ - { - label: 'New Tax Rate', - key: 'newTaxRate', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newTaxRate') { - setNewTaxRateOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('TaxRates') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewTaxRateOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewTaxRateOpen(false) - tableRef.current?.reload() - }} - reset={!newTaxRateOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Management/TaxRates/TaxRateInfo.jsx b/src/components/Dashboard/Management/TaxRates/TaxRateInfo.jsx index 782ac7d0..28722e52 100644 --- a/src/components/Dashboard/Management/TaxRates/TaxRateInfo.jsx +++ b/src/components/Dashboard/Management/TaxRates/TaxRateInfo.jsx @@ -1,26 +1,31 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' import ObjectForm from '../../common/ObjectForm' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('TaxRateInfo') @@ -33,6 +38,7 @@ const TaxRateInfo = () => { const taxRateId = new URLSearchParams(location.search).get('taxRateId') const [collapseState, updateCollapseState] = useCollapseState('TaxRateInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false }) @@ -42,9 +48,23 @@ const TaxRateInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, taxRateId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -58,10 +78,6 @@ const TaxRateInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -76,6 +92,7 @@ const TaxRateInfo = () => { { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Tax Rate Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -95,7 +113,7 @@ const TaxRateInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -140,33 +154,56 @@ const TaxRateInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Management/UserGroups.jsx b/src/components/Dashboard/Management/UserGroups.jsx new file mode 100644 index 00000000..427c96fd --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups.jsx @@ -0,0 +1,95 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const UserGroups = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = useColumnVisibility('userGroup') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('UserGroups') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('UserGroups') + + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default UserGroups diff --git a/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx b/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx new file mode 100644 index 00000000..257bd750 --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups/NewUserGroup.jsx @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewUserGroup = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewUserGroup.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewUserGroup diff --git a/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx b/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx new file mode 100644 index 00000000..20832c25 --- /dev/null +++ b/src/components/Dashboard/Management/UserGroups/UserGroupInfo.jsx @@ -0,0 +1,275 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import ScrollBox from '../../common/ScrollBox.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import PersonGroupIcon from '../../../Icons/PersonGroupIcon.jsx' +import PersonIcon from '../../../Icons/PersonIcon.jsx' + +const UserGroupInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const userGroupId = new URLSearchParams(location.search).get('userGroupId') + const [collapseState, updateCollapseState] = useCollapseState( + 'UserGroupInfo', + { + info: true, + permissions: true, + users: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, userGroupId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + }, + } + + return ( + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + } + isEditing={isEditing} + type='userGroup' + labelWidth='190px' + objectData={objectData} + visibleProperties={{ + permissions: false + }} + /> + )} + + + + } + active={collapseState.permissions} + onToggle={(expanded) => + updateCollapseState('permissions', expanded) + } + collapseKey='permissions' + > + } + > + + + + + + } + active={collapseState.users} + onToggle={(expanded) => updateCollapseState('users', expanded)} + collapseKey='users' + > + {!userGroupId ? ( + + ) : ( + + )} + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + ) +} + +export default UserGroupInfo diff --git a/src/components/Dashboard/Management/Users.jsx b/src/components/Dashboard/Management/Users.jsx index 6b7f775f..afd80090 100644 --- a/src/components/Dashboard/Management/Users.jsx +++ b/src/components/Dashboard/Management/Users.jsx @@ -1,47 +1,50 @@ import { useRef } from 'react' -import { Button, Flex, Space, Dropdown } from 'antd' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Users = () => { const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('user') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('user') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Users') - const actionItems = { - items: [ - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Users') + return ( - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - + + ) } diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx index 2df0543f..1fa21082 100644 --- a/src/components/Dashboard/Management/Users/UserInfo.jsx +++ b/src/components/Dashboard/Management/Users/UserInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' @@ -17,14 +17,19 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import ObjectProperty from '../../common/ObjectProperty.jsx' -import { getModelProperty } from '../../../../database/ObjectModels.js' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' import { useMediaQuery } from 'react-responsive' +import PersonIcon from '../../../Icons/PersonIcon.jsx' const UserInfo = () => { const location = useLocation() @@ -44,10 +49,24 @@ const UserInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) const isMobile = useMediaQuery({ maxWidth: 768 }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, userId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -74,6 +93,7 @@ const UserInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -180,8 +196,10 @@ const UserInfo = () => { isEditing={isEditing} type='user' objectData={objectData} + labelWidth='190px' visibleProperties={{ - profileImage: false + profileImage: false, + permissions: false }} /> @@ -189,6 +207,29 @@ const UserInfo = () => { + } + active={collapseState.permissions} + onToggle={(expanded) => + updateCollapseState('permissions', expanded) + } + collapseKey='permissions' + > + } + > + + + + + } @@ -203,7 +244,9 @@ const UserInfo = () => { ) : ( @@ -216,17 +259,20 @@ const UserInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + } active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} collapseKey='auditLogs' > {objectFormState.loading ? ( @@ -234,8 +280,10 @@ const UserInfo = () => { ) : ( )} diff --git a/src/components/Dashboard/Management/Vendors.jsx b/src/components/Dashboard/Management/Vendors.jsx index b2a550b0..64260c46 100644 --- a/src/components/Dashboard/Management/Vendors.jsx +++ b/src/components/Dashboard/Management/Vendors.jsx @@ -1,59 +1,49 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewVendor from './Vendors/NewVendor' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Vendors = () => { - const [newVendorOpen, setNewVendorOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('vendor') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('vendor') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Vendors') - const actionItems = { - items: [ - { - label: 'New Vendor', - key: 'newVendor', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newVendor') { - setNewVendorOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Vendors') return ( - <> - - + + + - - - + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + { useSortInUrl={true} /> - setNewVendorOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewVendorOpen(false) - tableRef.current?.reload() - }} - reset={!newVendorOpen} - /> - - + ) } diff --git a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx index 87c860cd..68ba4649 100644 --- a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx +++ b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('VendorInfo') @@ -42,9 +45,23 @@ const VendorInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, vendorId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -58,10 +75,6 @@ const VendorInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -76,6 +89,7 @@ const VendorInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -175,9 +185,14 @@ const VendorInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Production/FilamentProfiles.jsx b/src/components/Dashboard/Production/FilamentProfiles.jsx index ea736178..d508e191 100644 --- a/src/components/Dashboard/Production/FilamentProfiles.jsx +++ b/src/components/Dashboard/Production/FilamentProfiles.jsx @@ -1,82 +1,79 @@ -import { useRef, useState } from 'react' -import { Button, Dropdown, Flex, Modal, Space } from 'antd' -import NewFilamentProfile from './FilamentProfiles/NewFilamentProfile' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' -import useViewMode from '../hooks/useViewMode' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' const FilamentProfiles = () => { - const [newProfileOpen, setNewProfileOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('FilamentProfiles') const [columnVisibility, setColumnVisibility] = useColumnVisibility('filamentProfile') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('FilamentProfiles') - const actionItems = { - items: [ - { - label: 'New Filament Profile', - key: 'newFilamentProfile', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newFilamentProfile') { - setNewProfileOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('FilamentProfiles') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - - setNewProfileOpen(false)} - destroyOnHidden - > - {newProfileOpen && ( - { - setNewProfileOpen(false) - tableRef.current?.reload() - }} - /> - )} - - + /> + + ) } diff --git a/src/components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx b/src/components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx index 5cb64edf..fb85b995 100644 --- a/src/components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx +++ b/src/components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx @@ -1,10 +1,10 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import ActionHandler from '../../common/ActionHandler' import AuditLogIcon from '../../../Icons/AuditLogIcon' -import DocumentPrintButton from '../../common/DocumentPrintButton' +import InfoActionButtons from '../../common/InfoActionButtons' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import FilamentIcon from '../../../Icons/FilamentIcon' @@ -18,6 +18,7 @@ import MultiMotionIcon from '../../../Icons/MultiMotionIcon' import NoteIcon from '../../../Icons/NoteIcon' import NotesPanel from '../../common/NotesPanel' import ObjectActions from '../../common/ObjectActions' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectForm from '../../common/ObjectForm' import ObjectInfo from '../../common/ObjectInfo' import ObjectTable from '../../common/ObjectTable' @@ -27,6 +28,7 @@ import SettingsIcon from '../../../Icons/SettingsIcon' import UserNotifierToggle from '../../common/UserNotifierToggle' import ViewButton from '../../common/ViewButton' import useCollapseState from '../../hooks/useCollapseState' +import { getModelByName } from '../../../../database/ObjectModels.js' import { FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES, FILAMENT_PROFILE_COMPATIBILITY_PROPERTIES, @@ -80,9 +82,23 @@ const FilamentProfileInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, filamentProfileId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef.current?.startEditing?.() @@ -96,10 +112,6 @@ const FilamentProfileInfo = () => { objectFormRef.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef.current?.handleDelete?.() - return true - } } return ( @@ -113,6 +125,7 @@ const FilamentProfileInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { actionHandlerRef.current.callAction('finishEdit')} + handleUpdate={() => + actionHandlerRef.current.callAction('finishEdit') + } cancelEditing={() => actionHandlerRef.current.callAction('cancelEdit') } startEditing={() => actionHandlerRef.current.callAction('edit')} editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} - disabled={objectFormState.beingEditedByOther || objectFormState.loading} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } loading={objectFormState.editLoading} /> - @@ -511,9 +524,14 @@ const FilamentProfileInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index ac03ed3e..ac3b850e 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -1,24 +1,26 @@ // src/gcodefiles.js -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewGCodeFile from './GCodeFiles/NewGCodeFile' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import useColumnVisibility from '../hooks/useColumnVisibility' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const GCodeFiles = () => { - const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('gcodeFile') const [columnVisibility, setColumnVisibility] = useColumnVisibility('gcodeFile') @@ -26,37 +28,27 @@ const GCodeFiles = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('GCodeFiles') - const actionItems = { - items: [ - { - label: 'New GCodeFile', - key: 'newGCodeFile', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newGCodeFile') { - setNewGCodeFileOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('GCodeFiles') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewGCodeFileOpen(false) - }} - destroyOnHidden={true} - > - { - setNewGCodeFileOpen(false) - tableRef.current?.reload() - }} - reset={newGCodeFileOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx index ef6c490e..69b53563 100644 --- a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -19,12 +19,13 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' -import { getModelProperty } from '../../../../database/ObjectModels.js' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import PartIcon from '../../../Icons/PartIcon.jsx' const log = loglevel.getLogger('GCodeFileInfo') @@ -51,9 +52,23 @@ const GCodeFileInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, gcodeFileId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -84,6 +99,7 @@ const GCodeFileInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -213,9 +225,14 @@ const GCodeFileInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFilePreview.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFilePreview.jsx index ee84453f..9e2e718e 100644 --- a/src/components/Dashboard/Production/GCodeFiles/GCodeFilePreview.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFilePreview.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Space, Flex, Card } from 'antd' +import { Space, Flex, Card, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' import useCollapseState from '../../hooks/useCollapseState.jsx' @@ -12,6 +13,8 @@ import EyeIcon from '../../../Icons/EyeIcon.jsx' import ObjectForm from '../../common/ObjectForm.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ScrollBox from '../../common/ScrollBox.jsx' import FilePreview from '../../common/FilePreview.jsx' import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' @@ -40,6 +43,19 @@ const GCodeFilePreview = () => { lock: null }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, gcodeFileId] + ) + useDashboardObjectTools(currentObjectTools) + return ( { { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index 85a3e74e..6ebf9e64 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -1,64 +1,52 @@ // src/Jobs.js -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewJob from './Jobs/NewJob.jsx' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import useColumnVisibility from '../hooks/useColumnVisibility.jsx' -import PlusIcon from '../../Icons/PlusIcon.jsx' -import ReloadIcon from '../../Icons/ReloadIcon.jsx' import ObjectTable from '../common/ObjectTable.jsx' -import useViewMode from '../hooks/useViewMode.jsx' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ColumnViewButton from '../common/ColumnViewButton.jsx' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility.jsx' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton.jsx' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Jobs = () => { - const [newJobOpen, setNewJobOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('job') const [columnVisibility, setColumnVisibility] = useColumnVisibility('job') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Jobs') - const actionItems = { - items: [ - { - label: 'New Print Job', - key: 'newJob', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'newJob') { - showNewJobModal() - } else if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = useSortSidebarVisibility('Jobs') + - const showNewJobModal = () => { - setNewJobOpen(true) - } return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - { - setNewJobOpen(false) - }} - > - { - setNewJobOpen(false) - tableRef.current?.reload() - }} - reset={newJobOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Production/Jobs/DeployJob.jsx b/src/components/Dashboard/Production/Jobs/DeployJob.jsx index bbe40b7c..43b38fc8 100644 --- a/src/components/Dashboard/Production/Jobs/DeployJob.jsx +++ b/src/components/Dashboard/Production/Jobs/DeployJob.jsx @@ -4,6 +4,7 @@ import WizardView from '../../common/WizardView' import { ApiServerContext } from '../../context/ApiServerContext' import { Descriptions, Flex, Typography } from 'antd' import ObjectTable from '../../common/ObjectTable' +import { getModelByName } from '../../../../database/ObjectModels.js' import ObjectProperty from '../../common/ObjectProperty' const { Text } = Typography const DeployJob = ({ @@ -26,7 +27,7 @@ const DeployJob = ({ while (hasMore == true) { const subJobsPage = await fetchObjects('subJob', { page: currentPage, - filter: { 'job._id': job._id } + filter: { job: job._id } }) subJobs = [...subJobs, ...subJobsPage.data] if (subJobsPage.data.length >= 25) { @@ -150,7 +151,7 @@ const DeployJob = ({ printer: false, job: false }} - masterFilter={{ 'job._id': job?._id }} + masterFilter={{ job: getModelByName('job').prefix + ':' + job?._id }} size={'small'} showActions={!slicerIntegration} /> diff --git a/src/components/Dashboard/Production/Jobs/JobInfo.jsx b/src/components/Dashboard/Production/Jobs/JobInfo.jsx index 37628af9..f704f855 100644 --- a/src/components/Dashboard/Production/Jobs/JobInfo.jsx +++ b/src/components/Dashboard/Production/Jobs/JobInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,14 +18,15 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import JobIcon from '../../../Icons/JobIcon.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' -import ScrollBox from '../../common/ScrollBox.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('JobInfo') log.setLevel(config.logLevel) @@ -35,7 +36,6 @@ const JobInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -57,15 +57,24 @@ const JobInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: { _id: jobId } }) - const editDisabled = - getModelByName('job') - ?.actions?.find((action) => action.name === 'edit') - ?.disabled(objectFormState.objectData) ?? false + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, jobId] + ) + useDashboardObjectTools(currentObjectTools) const actions = { edit: () => { @@ -79,169 +88,172 @@ const JobInfo = () => { finishEdit: () => { objectFormRef?.current.handleUpdate() return true - }, + } } return ( - - + + + - - + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + - - - - - - - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - - - - - - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + style={{ height: '100%' }} + ref={objectFormRef} + setCurrentObject={true} + onStateChange={(state) => { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - } - isEditing={isEditing} - type='job' - objectData={objectData} - /> - )} - - - - - } - active={collapseState.subJobs} - onToggle={(expanded) => updateCollapseState('subJobs', expanded)} - collapseKey='subJobs' - > - + {({ loading, isEditing, objectData }) => ( + } + isEditing={isEditing} + type='job' + objectData={objectData} + /> + )} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.subJobs} + onToggle={(expanded) => updateCollapseState('subJobs', expanded)} + collapseKey='subJobs' + > + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Production/PrinterProfiles.jsx b/src/components/Dashboard/Production/PrinterProfiles.jsx index 8d34d266..5ee14e74 100644 --- a/src/components/Dashboard/Production/PrinterProfiles.jsx +++ b/src/components/Dashboard/Production/PrinterProfiles.jsx @@ -1,81 +1,78 @@ -import { useRef, useState } from 'react' -import { Button, Dropdown, Flex, Modal, Space } from 'antd' -import NewPrinterProfile from './PrinterProfiles/NewPrinterProfile' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' -import useViewMode from '../hooks/useViewMode' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' const PrinterProfiles = () => { - const [newProfileOpen, setNewProfileOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('PrinterProfiles') const [columnVisibility, setColumnVisibility] = useColumnVisibility('printerProfile') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('PrinterProfiles') - const actionItems = { - items: [ - { - label: 'New Printer Profile', - key: 'newPrinterProfile', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPrinterProfile') { - setNewProfileOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PrinterProfiles') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - - setNewProfileOpen(false)} - destroyOnHidden - > - {newProfileOpen && ( - { - setNewProfileOpen(false) - tableRef.current?.reload() - }} - /> - )} - - + /> + + ) } diff --git a/src/components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx b/src/components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx index a3fc445e..6c5aedf2 100644 --- a/src/components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx +++ b/src/components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx @@ -1,10 +1,10 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import ActionHandler from '../../common/ActionHandler' import AuditLogIcon from '../../../Icons/AuditLogIcon' -import DocumentPrintButton from '../../common/DocumentPrintButton' +import InfoActionButtons from '../../common/InfoActionButtons' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import HotEndIcon from '../../../Icons/HotEndIcon' @@ -18,6 +18,7 @@ import ActivityIndicator from '../../common/ActivityIndicator' import NoteIcon from '../../../Icons/NoteIcon' import NotesPanel from '../../common/NotesPanel' import ObjectActions from '../../common/ObjectActions' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectForm from '../../common/ObjectForm' import ObjectInfo from '../../common/ObjectInfo' import ObjectTable from '../../common/ObjectTable' @@ -26,7 +27,7 @@ import UserNotifierToggle from '../../common/UserNotifierToggle' import ViewButton from '../../common/ViewButton' import useCollapseState from '../../hooks/useCollapseState' import ObjectProperty from '../../common/ObjectProperty' -import { getModelProperty } from '../../../../database/ObjectModels' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels' import GCodeFileIcon from '../../../Icons/GCodeFileIcon' import SettingsIcon from '../../../Icons/SettingsIcon' import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon' @@ -59,9 +60,23 @@ const PrinterProfileInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, printerProfileId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef.current?.startEditing?.() @@ -75,10 +90,6 @@ const PrinterProfileInfo = () => { objectFormRef.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef.current?.handleDelete?.() - return true - } } return ( @@ -92,6 +103,7 @@ const PrinterProfileInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { actionHandlerRef.current.callAction('finishEdit')} + handleUpdate={() => + actionHandlerRef.current.callAction('finishEdit') + } cancelEditing={() => actionHandlerRef.current.callAction('cancelEdit') } startEditing={() => actionHandlerRef.current.callAction('edit')} editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} - disabled={objectFormState.beingEditedByOther || objectFormState.loading} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } loading={objectFormState.editLoading} /> - @@ -363,9 +375,14 @@ const PrinterProfileInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Production/Printers.jsx b/src/components/Dashboard/Production/Printers.jsx index 9d3d1e5c..d3dd7928 100644 --- a/src/components/Dashboard/Production/Printers.jsx +++ b/src/components/Dashboard/Production/Printers.jsx @@ -1,26 +1,28 @@ // src/Printers.js -import { useState, useRef } from 'react' -import { Button, Dropdown, Space, Flex, Modal } from 'antd' -import NewPrinter from './Printers/NewPrinter' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import { useRef } from 'react' +import { Space, Flex } from 'antd' import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useColumnVisibility from '../hooks/useColumnVisibility' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' const Printers = () => { - const [newPrinterOpen, setNewPrinterOpen] = useState(false) const tableRef = useRef() // View mode state (cards/list), persisted in sessionStorage via custom hook - const [viewMode, setViewMode] = useViewMode('Printers') // Column visibility state, persisted in sessionStorage via custom hook const [columnVisibility, setColumnVisibility] = useColumnVisibility('printer') @@ -29,37 +31,27 @@ const Printers = () => { const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Printers') - const actionItems = { - items: [ - { - label: 'New Printer', - key: 'newPrinter', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newPrinter') { - setNewPrinterOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Printers') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} - /> - { - setNewPrinterOpen(false) - }} - destroyOnHidden - > - { - setNewPrinterOpen(false) - tableRef.current?.reload() - }} - reset={newPrinterOpen} - /> - - - + /> + + ) } diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index b495fd85..7e52b8b3 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -1,6 +1,7 @@ -import { useState, useRef, useEffect, useContext } from 'react' +import { useState, useRef, useEffect, useContext, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Space, Flex, Card, Splitter, Divider } from 'antd' +import { Space, Flex, Card, Splitter, Divider, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' import useCollapseState from '../../hooks/useCollapseState.jsx' @@ -12,6 +13,7 @@ import ObjectForm from '../../common/ObjectForm.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import PropTypes from 'prop-types' import ObjectInfo from '../../common/ObjectInfo.jsx' @@ -33,6 +35,8 @@ import { ApiServerContext } from '../../context/ApiServerContext.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import PrinterControlButtons from '../../common/PrinterControlButtons.jsx' +import PrinterQueue from '../../common/PrinterQueue.jsx' +import ListIcon from '../../../Icons/ListIcon.jsx' const log = loglevel.getLogger('ControlPrinter') log.setLevel(config.logLevel) @@ -49,6 +53,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => { printer: true, job: true, subjob: true, + queue: true, filamentStock: true, temperature: true, position: true, @@ -105,6 +110,20 @@ const ControlPrinter = ({ slicerIntegration = false }) => { return () => setOnModalOk(null) }, [setOnModalOk]) + const currentObjectTools = useMemo( + () => + !slicerIntegration ? ( + + ) : null, + [objectFormState.loading, printerId, slicerIntegration] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -256,6 +275,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => { { key: 'subJob', label: 'Sub Job' }, + { + key: 'queue', + label: 'Queue' + }, { key: 'filamentStock', label: 'Filament Stock' @@ -319,14 +343,6 @@ const ControlPrinter = ({ slicerIntegration = false }) => { } loading={objectFormState.loading} /> - {!slicerIntegration && ( - - )} @@ -393,6 +409,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => { 'currentJob._id': false, currentSubJob: false, 'currentSubJob._id': false, + queue: false, createdAt: false, updatedAt: false, state: !slicerIntegration, @@ -410,6 +427,29 @@ const ControlPrinter = ({ slicerIntegration = false }) => { }} + } + collapseKey='queue' + active={collapseState.queue} + onToggle={(expanded) => + updateCollapseState('queue', expanded) + } + > + + + + } @@ -451,7 +491,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => { ) : ( )} @@ -499,7 +541,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => { ) : ( )} @@ -544,7 +588,9 @@ const ControlPrinter = ({ slicerIntegration = false }) => { ) : ( )} @@ -575,9 +621,14 @@ const ControlPrinter = ({ slicerIntegration = false }) => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + +
diff --git a/src/components/Dashboard/Production/Printers/NewPrinter.jsx b/src/components/Dashboard/Production/Printers/NewPrinter.jsx index f58e9818..c2c79d69 100644 --- a/src/components/Dashboard/Production/Printers/NewPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/NewPrinter.jsx @@ -1,7 +1,12 @@ import PropTypes from 'prop-types' +import { Flex } from 'antd' import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' +import HostDeviceScanner, { + getHostId, + isNetworkScanCompatible +} from '../../common/HostDeviceScanner' const NewPrinter = ({ onOk, defaultValues }) => { return ( @@ -16,21 +21,52 @@ const NewPrinter = ({ onOk, defaultValues }) => { ...defaultValues }} > - {({ handleSubmit, submitLoading, objectData, formValid }) => { + {({ + handleSubmit, + submitLoading, + objectData, + formValid, + form, + setObjectData + }) => { const steps = [ { title: 'Required', key: 'required', content: ( - + + + {getHostId(objectData.host) && + isNetworkScanCompatible(objectData.moonraker?.protocol) ? ( + + ) : null} + + ) }, { diff --git a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx index 7c275c94..e0f7b55c 100644 --- a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx +++ b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -17,13 +17,15 @@ import EditButtons from '../../common/EditButtons.jsx' import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import PrinterProfileIcon from '../../../Icons/PrinterProfileIcon.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' const log = loglevel.getLogger('PrinterInfo') log.setLevel(config.logLevel) @@ -34,7 +36,6 @@ const PrinterInfo = () => { const profilesTableRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() const printerId = new URLSearchParams(location.search).get('printerId') const [collapseState, updateCollapseState] = useCollapseState('PrinterInfo', { @@ -51,6 +52,7 @@ const PrinterInfo = () => { formValid: false, activities: [], loading: false, + editDisabled: false, objectData: {} }) @@ -62,6 +64,19 @@ const PrinterInfo = () => { return () => setOnModalOk(null) }, [setOnModalOk]) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, printerId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -74,186 +89,191 @@ const PrinterInfo = () => { finishEdit: () => { objectFormRef?.current.handleUpdate() return true - }, + } } return ( - - + + + - - + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + - - - - - - - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + style={{ height: '100%' }} + ref={objectFormRef} + setCurrentObject={true} + onStateChange={(state) => { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - } - isEditing={isEditing} - type='printer' - objectData={objectData} - labelWidth='175px' - visibleProperties={{ - currentFilamentStock: false, - 'currentFilamentStock._id': false, - currentJob: false, - 'currentJob._id': false, - currentSubJob: false, - 'currentSubJob._id': false, - alerts: false, - pendingSlicerUploads: false - }} - /> - )} - - - - - } - active={collapseState.profiles} - onToggle={(expanded) => updateCollapseState('profiles', expanded)} - collapseKey='profiles' - > - {objectFormState.loading ? ( - - ) : ( - - )} + {({ loading, isEditing, objectData }) => ( + } + isEditing={isEditing} + type='printer' + objectData={objectData} + labelWidth='175px' + visibleProperties={{ + currentFilamentStock: false, + 'currentFilamentStock._id': false, + currentJob: false, + 'currentJob._id': false, + currentSubJob: false, + 'currentSubJob._id': false, + alerts: false, + pendingSlicerUploads: false + }} + /> + )} + + - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + } + active={collapseState.profiles} + onToggle={(expanded) => updateCollapseState('profiles', expanded)} + collapseKey='profiles' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + +
+ ) } diff --git a/src/components/Dashboard/Production/ProductionOverview.jsx b/src/components/Dashboard/Production/ProductionOverview.jsx index 93d91268..757a3c13 100644 --- a/src/components/Dashboard/Production/ProductionOverview.jsx +++ b/src/components/Dashboard/Production/ProductionOverview.jsx @@ -1,132 +1,83 @@ -import { Flex } from 'antd' -import useCollapseState from '../hooks/useCollapseState.jsx' +import DashboardOverviewPage from '../common/DashboardOverviewPage' import StatsDisplay from '../common/StatsDisplay' import ModelHistoryDisplay from '../common/ModelHistoryDisplay' -import InfoCollapse from '../common/InfoCollapse' -import ScrollBox from '../common/ScrollBox' import ObjectTable from '../common/ObjectTable' +const collapseDefaults = { + printerStats: true, + printerHistory: true, + onlinePrinters: true, + jobStats: true, + productionStats: true, + jobStatsDetails: true, + jobHistory: true, + queuedJobs: true +} + +const sections = [ + { + key: 'printerStats', + title: 'Printer Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'jobStats', + title: 'Job Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'printerAndJobHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'printerHistory', + title: 'Printer History', + content: + } + ], + [ + { + key: 'jobHistory', + title: 'Job History', + content: + } + ] + ] + }, + { + key: 'onlinePrintersAndQueuedJobsRow', + type: 'row', + columns: [ + [ + { + key: 'onlinePrinters', + title: 'Online Printers', + content: ( + + ) + } + ], + [ + { + key: 'queuedJobs', + title: 'Queued Jobs', + content: + } + ] + ] + } +] + const ProductionOverview = () => { - const [collapseState, updateCollapseState] = useCollapseState( - 'ProductionOverview', - { - printerStats: true, - printerHistory: true, - onlinePrinters: true, - jobStats: true, - productionStats: true, - jobStatsDetails: true - } - ) - return ( - - - - - updateCollapseState('printerStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='printerStats' - > - - - - - - updateCollapseState('jobStats', isActive)} - className='no-t-padding-collapse' - collapseKey='jobStats' - > - - - - - - - - - updateCollapseState('printerHistory', isActive) - } - collapseKey='printerHistory' - canCollapse={false} - > - - - - updateCollapseState('onlinePrinters', isActive) - } - collapseKey='onlinePrinters' - canCollapse={false} - > - - - - - - updateCollapseState('jobHistory', isActive) - } - canCollapse={false} - collapseKey='jobHistory' - > - - - - updateCollapseState('queuedJobs', isActive) - } - canCollapse={false} - collapseKey='queuedJobs' - > - - - - - - - + ) } diff --git a/src/components/Dashboard/Production/SubJobs.jsx b/src/components/Dashboard/Production/SubJobs.jsx index 6851489a..0e7fa221 100644 --- a/src/components/Dashboard/Production/SubJobs.jsx +++ b/src/components/Dashboard/Production/SubJobs.jsx @@ -1,49 +1,52 @@ // src/SubJobs.js import { useRef } from 'react' -import { Button, Flex, Space, Dropdown } from 'antd' +import { Flex, Space } from 'antd' import useColumnVisibility from '../hooks/useColumnVisibility.jsx' -import ReloadIcon from '../../Icons/ReloadIcon.jsx' import ObjectTable from '../common/ObjectTable.jsx' -import useViewMode from '../hooks/useViewMode.jsx' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' import ColumnViewButton from '../common/ColumnViewButton.jsx' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility.jsx' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ExportListButton from '../common/ExportListButton.jsx' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const SubJobs = () => { const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('subJob') const [columnVisibility, setColumnVisibility] = useColumnVisibility('subJob') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('SubJobs') - const actionItems = { - items: [ - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('SubJobs') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - - + { saveSortInUrl={true} useSortInSession={true} useSortInUrl={true} + /> - - + + ) } diff --git a/src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx b/src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx index 99d846f3..0da7b0f0 100644 --- a/src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx +++ b/src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' @@ -16,11 +16,13 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import StockEventIcon from '../../../Icons/StockEventIcon.jsx' const SubJobInfo = () => { @@ -42,6 +44,19 @@ const SubJobInfo = () => { objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, subJobId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current.startEditing() @@ -69,6 +84,7 @@ const SubJobInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { disabled={true} loading={objectFormState.editLoading} /> - @@ -173,11 +183,10 @@ const SubJobInfo = () => { ) : ( )} @@ -189,9 +198,14 @@ const SubJobInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Sales/Clients.jsx b/src/components/Dashboard/Sales/Clients.jsx index 7531f46f..69944333 100644 --- a/src/components/Dashboard/Sales/Clients.jsx +++ b/src/components/Dashboard/Sales/Clients.jsx @@ -1,59 +1,50 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewClient from './Clients/NewClient' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Clients = () => { - const [newClientOpen, setNewClientOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('client') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('client') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Clients') - const actionItems = { - items: [ - { - label: 'New Client', - key: 'newClient', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newClient') { - setNewClientOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Clients') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - setNewClientOpen(false)} - footer={null} - destroyOnHidden={true} - width={700} - > - { - setNewClientOpen(false) - tableRef.current?.reload() - }} - reset={!newClientOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Sales/Clients/ClientInfo.jsx b/src/components/Dashboard/Sales/Clients/ClientInfo.jsx index 8e1e812e..70e50d35 100644 --- a/src/components/Dashboard/Sales/Clients/ClientInfo.jsx +++ b/src/components/Dashboard/Sales/Clients/ClientInfo.jsx @@ -1,6 +1,7 @@ -import { useRef, useState } from 'react' +import { useRef, useState, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,10 +18,12 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('ClientInfo') @@ -42,9 +45,23 @@ const ClientInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, clientId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -58,10 +75,6 @@ const ClientInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - } } return ( @@ -76,6 +89,7 @@ const ClientInfo = () => { { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { editLoading={objectFormState.editLoading} formValid={objectFormState.formValid} disabled={ - objectFormState.beingEditedByOther || objectFormState.loading + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled } loading={objectFormState.editLoading} /> - @@ -175,9 +185,14 @@ const ClientInfo = () => { onToggle={(expanded) => updateCollapseState('notes', expanded)} collapseKey='notes' > - + } + > + - + + { ) : ( )} diff --git a/src/components/Dashboard/Sales/FulfillmentPolicies.jsx b/src/components/Dashboard/Sales/FulfillmentPolicies.jsx new file mode 100644 index 00000000..46b297fd --- /dev/null +++ b/src/components/Dashboard/Sales/FulfillmentPolicies.jsx @@ -0,0 +1,92 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const FulfillmentPolicies = () => { + const tableRef = useRef() + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('fulfillmentPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('FulfillmentPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('FulfillmentPolicies') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default FulfillmentPolicies diff --git a/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx b/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx new file mode 100644 index 00000000..9b937282 --- /dev/null +++ b/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx @@ -0,0 +1,270 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('FulfillmentPolicyInfo') +log.setLevel(config.logLevel) + +const FulfillmentPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const fulfillmentPolicyId = new URLSearchParams(location.search).get( + 'fulfillmentPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'FulfillmentPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, fulfillmentPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default FulfillmentPolicyInfo diff --git a/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx b/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx new file mode 100644 index 00000000..d8070927 --- /dev/null +++ b/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewFulfillmentPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewFulfillmentPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewFulfillmentPolicy diff --git a/src/components/Dashboard/Sales/ListingVarients.jsx b/src/components/Dashboard/Sales/ListingVarients.jsx new file mode 100644 index 00000000..1a63b25f --- /dev/null +++ b/src/components/Dashboard/Sales/ListingVarients.jsx @@ -0,0 +1,96 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const ListingVarients = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('listingVarients') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('ListingVarients') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ListingVarients') + + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default ListingVarients diff --git a/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx index 7c278ce3..07409328 100644 --- a/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx @@ -1,14 +1,18 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import PictureIcon from '../../../Icons/PictureIcon.jsx' +import BulletListIcon from '../../../Icons/BulletListIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ObjectForm from '../../common/ObjectForm' @@ -17,12 +21,17 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' +import { + getModelByName, + getModelProperty +} from '../../../../database/ObjectModels.js' const log = loglevel.getLogger('ListingVarientInfo') log.setLevel(config.logLevel) @@ -32,7 +41,6 @@ const ListingVarientInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -47,6 +55,8 @@ const ListingVarientInfo = () => { 'ListingVarientInfo', { info: true, + listingImages: true, + aspects: true, notes: true, auditLogs: false } @@ -57,9 +67,23 @@ const ListingVarientInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, listingVarientId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -73,152 +97,194 @@ const ListingVarientInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.listingImages} + onToggle={(expanded) => + updateCollapseState('listingImages', expanded) + } + collapseKey='listingImages' + > + + + } + active={collapseState.aspects} + onToggle={(expanded) => + updateCollapseState('aspects', expanded) + } + collapseKey='aspects' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx index 63c649c6..4d7eb768 100644 --- a/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx @@ -37,6 +37,7 @@ const NewListingVarient = ({ onOk, defaultValues }) => { isEditing={true} required={false} objectData={objectData} + visibleProperties={{ listingImages: false }} /> ) }, @@ -53,7 +54,9 @@ const NewListingVarient = ({ onOk, defaultValues }) => { _reference: false, createdAt: false, updatedAt: false, - lastSyncedAt: false + lastSyncedAt: false, + stockQuantity: false, + listingImages: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx index e6c09100..131084d3 100644 --- a/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx @@ -17,7 +17,7 @@ const PublishListingVarient = ({ onOk, objectData }) => { 'publish' ) if (result) { - message.success('Published successfully') + message.success('Publishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx index ae390618..f46f63f5 100644 --- a/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx @@ -17,7 +17,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => { 'unpublish' ) if (result) { - message.success('Unpublished successfully') + message.success('Unpublishing started...') onOk(result) } } catch (error) { @@ -35,7 +35,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => { return ( { - const [newListingOpen, setNewListingOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('listings') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('listings') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Listings') - const actionItems = { - items: [ - { - label: 'New Listing', - key: 'newListing', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newListing') { - setNewListingOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Listings') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewListingOpen(false) - }} - destroyOnHidden={true} - > - { - setNewListingOpen(false) - tableRef.current?.reload() - }} - reset={newListingOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Sales/Listings/ListingInfo.jsx b/src/components/Dashboard/Sales/Listings/ListingInfo.jsx index 6931b22c..0db07718 100644 --- a/src/components/Dashboard/Sales/Listings/ListingInfo.jsx +++ b/src/components/Dashboard/Sales/Listings/ListingInfo.jsx @@ -1,14 +1,18 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, Space } from 'antd' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import TextIcon from '../../../Icons/TextIcon.jsx' +import PictureIcon from '../../../Icons/PictureIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx' @@ -18,11 +22,16 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelByName, + getModelProperty +} from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('ListingInfo') @@ -34,7 +43,6 @@ const ListingInfo = () => { const listingVarientsTableRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -45,6 +53,8 @@ const ListingInfo = () => { const listingId = new URLSearchParams(location.search).get('listingId') const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', { info: true, + listingImages: true, + description: true, listingVarients: true, notes: true, auditLogs: false @@ -55,9 +65,23 @@ const ListingInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, listingId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -70,173 +94,221 @@ const ListingInfo = () => { finishEdit: () => { objectFormRef?.current?.handleUpdate?.() return true - }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, + } } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.listingImages} + onToggle={(expanded) => + updateCollapseState('listingImages', expanded) + } + collapseKey='listingImages' + > + }> + - - } - active={collapseState.listingVarients} - onToggle={(expanded) => - updateCollapseState('listingVarients', expanded) - } - collapseKey='listingVarients' - > - - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + + + + } + active={collapseState.description} + onToggle={(expanded) => + updateCollapseState('description', expanded) + } + collapseKey='description' + > + }> + + + + + + } + active={collapseState.listingVarients} + onToggle={(expanded) => + updateCollapseState('listingVarients', expanded) + } + collapseKey='listingVarients' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + +
+ ) } diff --git a/src/components/Dashboard/Sales/Listings/NewListing.jsx b/src/components/Dashboard/Sales/Listings/NewListing.jsx index cc31176c..ea36025e 100644 --- a/src/components/Dashboard/Sales/Listings/NewListing.jsx +++ b/src/components/Dashboard/Sales/Listings/NewListing.jsx @@ -37,6 +37,7 @@ const NewListing = ({ onOk, defaultValues }) => { isEditing={true} required={false} objectData={objectData} + visibleProperties={{ listingImages: false }} /> ) }, @@ -53,7 +54,9 @@ const NewListing = ({ onOk, defaultValues }) => { _reference: false, createdAt: false, updatedAt: false, - lastSyncedAt: false + lastSyncedAt: false, + stockQuantity: false, + listingImages: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/Listings/PublishListing.jsx b/src/components/Dashboard/Sales/Listings/PublishListing.jsx index 6e35655b..034ba819 100644 --- a/src/components/Dashboard/Sales/Listings/PublishListing.jsx +++ b/src/components/Dashboard/Sales/Listings/PublishListing.jsx @@ -11,9 +11,13 @@ const PublishListing = ({ onOk, objectData }) => { const handlePublish = async () => { setLoading(true) try { - const result = await sendObjectFunction(objectData._id, 'Listing', 'publish') + const result = await sendObjectFunction( + objectData._id, + 'Listing', + 'publish' + ) if (result) { - message.success('Published successfully') + message.success('Publishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx index 732be523..15979105 100644 --- a/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx +++ b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx @@ -11,9 +11,13 @@ const UnpublishListing = ({ onOk, objectData }) => { const handleUnpublish = async () => { setLoading(true) try { - const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish') + const result = await sendObjectFunction( + objectData._id, + 'Listing', + 'unpublish' + ) if (result) { - message.success('Unpublished successfully') + message.success('Unpublishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/Marketplaces.jsx b/src/components/Dashboard/Sales/Marketplaces.jsx index 2b58e8df..945b4570 100644 --- a/src/components/Dashboard/Sales/Marketplaces.jsx +++ b/src/components/Dashboard/Sales/Marketplaces.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewMarketplace from './Marketplaces/NewMarketplace' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const Marketplaces = () => { - const [newMarketplaceOpen, setNewMarketplaceOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('marketplaces') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('marketplaces') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('Marketplaces') - const actionItems = { - items: [ - { - label: 'New Marketplace', - key: 'newMarketplace', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newMarketplace') { - setNewMarketplaceOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('Marketplaces') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewMarketplaceOpen(false) - }} - destroyOnHidden={true} - > - { - setNewMarketplaceOpen(false) - tableRef.current?.reload() - }} - reset={newMarketplaceOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx index f77c8d94..8f938564 100644 --- a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx +++ b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx @@ -1,6 +1,7 @@ -import { useContext, useRef, useState , useEffect} from 'react' +import { useContext, useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Card, Flex, message, Space } from 'antd' +import { Card, Flex, message, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' import useCollapseState from '../../hooks/useCollapseState' @@ -17,14 +18,16 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import { ApiServerContext } from '../../context/ApiServerContext.jsx' import { ElectronContext } from '../../context/ElectronContext.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' import { buildMarketplaceAuthState, storeMarketplaceAuthState @@ -38,7 +41,6 @@ const MarketplaceInfo = () => { const objectFormRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -66,6 +68,7 @@ const MarketplaceInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) @@ -115,6 +118,19 @@ const MarketplaceInfo = () => { objectFormRef?.current?.handleFetchObject?.() } + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, marketplaceId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { objectFormRef?.current?.startEditing?.() @@ -128,10 +144,6 @@ const MarketplaceInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, connect: () => { startAuthorization() return true @@ -147,142 +159,149 @@ const MarketplaceInfo = () => { } return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || objectFormState.loading - } - loading={objectFormState.editLoading} - /> - - - - - - - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' - > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + title='Marketplace Information' + icon={} + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + )} + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx index acda025b..74fbea74 100644 --- a/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx +++ b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx @@ -77,6 +77,7 @@ const NewMarketplace = ({ onOk, defaultValues }) => { 'config.marketplaceId': false, 'config.sandbox': false, 'config.verificationToken': false, + 'config.webhookUrl': false, 'config.redirectUri': false, 'config.accessToken': false, 'config.refreshToken': false, diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx new file mode 100644 index 00000000..c76fb367 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx @@ -0,0 +1,57 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const SyncAccountPolicies = ({ + onOk, + objectData, + functionName, + title, + description, + successMessage +}) => { + const [syncLoading, setSyncLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handleSync = async () => { + setSyncLoading(true) + try { + const result = await sendObjectFunction( + objectData._id, + 'Marketplace', + functionName + ) + if (result) { + message.success(successMessage) + onOk(result) + } + } catch (error) { + console.error(`Error syncing ${functionName}:`, error) + } finally { + setSyncLoading(false) + } + } + + return ( + + ) +} + +SyncAccountPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object, + functionName: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + successMessage: PropTypes.string.isRequired +} + +export default SyncAccountPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx new file mode 100644 index 00000000..acfdb5c7 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncFulfillmentPolicies = ({ onOk, objectData }) => ( + +) + +SyncFulfillmentPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncFulfillmentPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx new file mode 100644 index 00000000..378d5672 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx @@ -0,0 +1,46 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const SyncMarketplace = ({ onOk, objectData }) => { + const [syncLoading, setSyncLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handleSync = async () => { + setSyncLoading(true) + try { + const result = await sendObjectFunction( + objectData._id, + 'Marketplace', + 'sync' + ) + if (result?.success) { + message.success('Marketplace synced successfully') + onOk(result) + } + } catch (error) { + console.error('Error syncing marketplace:', error) + } finally { + setSyncLoading(false) + } + } + + return ( + + ) +} + +SyncMarketplace.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncMarketplace diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncPaymentPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncPaymentPolicies.jsx new file mode 100644 index 00000000..d0b3b050 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncPaymentPolicies.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncPaymentPolicies = ({ onOk, objectData }) => ( + +) + +SyncPaymentPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncPaymentPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx new file mode 100644 index 00000000..a5768db8 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncReturnPolicies = ({ onOk, objectData }) => ( + +) + +SyncReturnPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncReturnPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx new file mode 100644 index 00000000..8589dd46 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncTaxRates = ({ onOk, objectData }) => ( + +) + +SyncTaxRates.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncTaxRates diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncWebhooks.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncWebhooks.jsx new file mode 100644 index 00000000..23dfccc0 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncWebhooks.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncWebhooks = ({ onOk, objectData }) => ( + +) + +SyncWebhooks.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncWebhooks diff --git a/src/components/Dashboard/Sales/ReturnPolicies.jsx b/src/components/Dashboard/Sales/ReturnPolicies.jsx new file mode 100644 index 00000000..0d3e84e2 --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies.jsx @@ -0,0 +1,91 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const ReturnPolicies = () => { + const tableRef = useRef() + const [columnVisibility, setColumnVisibility] = useColumnVisibility('returnPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('ReturnPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ReturnPolicies') + + return ( + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> + + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default ReturnPolicies diff --git a/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx b/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx new file mode 100644 index 00000000..92c4e1e7 --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewReturnPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewReturnPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewReturnPolicy diff --git a/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx b/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx new file mode 100644 index 00000000..e400b23c --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx @@ -0,0 +1,264 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('ReturnPolicyInfo') +log.setLevel(config.logLevel) + +const ReturnPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const returnPolicyId = new URLSearchParams(location.search).get( + 'returnPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'ReturnPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, returnPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default ReturnPolicyInfo diff --git a/src/components/Dashboard/Sales/SalesOrders.jsx b/src/components/Dashboard/Sales/SalesOrders.jsx index a6a6527a..36023d65 100644 --- a/src/components/Dashboard/Sales/SalesOrders.jsx +++ b/src/components/Dashboard/Sales/SalesOrders.jsx @@ -1,60 +1,51 @@ -import { useState, useRef } from 'react' -import { Button, Flex, Space, Modal, Dropdown } from 'antd' -import NewSalesOrder from './SalesOrders/NewSalesOrder' +import { useRef } from 'react' +import { Flex, Space } from 'antd' import ObjectTable from '../common/ObjectTable' -import PlusIcon from '../../Icons/PlusIcon' -import ReloadIcon from '../../Icons/ReloadIcon' +import ObjectActions from '../common/ObjectActions' +import BulkObjectActions from '../common/BulkObjectActions' import useColumnVisibility from '../hooks/useColumnVisibility' import ObjectTableViewButton from '../common/ObjectTableViewButton' import FilterSidebarButton from '../common/FilterSidebarButton' -import useViewMode from '../hooks/useViewMode' +import SortSidebarButton from '../common/SortSidebarButton' import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' import ColumnViewButton from '../common/ColumnViewButton' import ExportListButton from '../common/ExportListButton' +import InfoActionButtons from '../common/InfoActionButtons' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' const SalesOrders = () => { - const [newSalesOrderOpen, setNewSalesOrderOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('salesOrders') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('salesOrders') const [showFilterSidebar, setShowFilterSidebar] = useFilterSidebarVisibility('SalesOrders') - const actionItems = { - items: [ - { - label: 'New Sales Order', - key: 'newSalesOrder', - icon: - }, - { type: 'divider' }, - { - label: 'Reload List', - key: 'reloadList', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reloadList') { - tableRef.current?.reload() - } else if (key === 'newSalesOrder') { - setNewSalesOrderOpen(true) - } - } - } + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('SalesOrders') + return ( - <> - - - - - - + + + + + tableRef.current?.reload()} + /> + tableRef.current?.reload()} + /> { updateVisibleState={setColumnVisibility} /> + - + + + setShowSortSidebar(!showSortSidebar)} + /> setShowFilterSidebar(!showFilterSidebar)} /> + - + + { useSortInSession={true} useSortInUrl={true} /> - - { - setNewSalesOrderOpen(false) - }} - destroyOnHidden={true} - > - { - setNewSalesOrderOpen(false) - tableRef.current?.reload() - }} - reset={newSalesOrderOpen} - /> - - + + ) } diff --git a/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx b/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx index e8982045..c15313c5 100644 --- a/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx +++ b/src/components/Dashboard/Sales/SalesOrders/SalesOrderInfo.jsx @@ -1,6 +1,6 @@ -import { useRef, useState, useEffect } from 'react' +import { useRef, useState, useEffect, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { Space, Flex, Card } from 'antd' +import { Space, Flex, Card, Spin } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config.js' @@ -18,17 +18,18 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt import ActivityIndicator from '../../common/ActivityIndicator.jsx' import ActionHandler from '../../common/ActionHandler.jsx' import { useActions } from '../../context/ActionsContext' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' import InvoiceIcon from '../../../Icons/InvoiceIcon.jsx' -import StockEventIcon from '../../../Icons/StockEventIcon.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' +import StockEventIcon from '../../../Icons/StockEventIcon.jsx' const log = loglevel.getLogger('SalesOrderInfo') log.setLevel(config.logLevel) @@ -40,7 +41,6 @@ const SalesOrderInfo = () => { const shipmentsTableRef = useRef(null) const actionHandlerRef = useRef(null) - const { setOnModalOk } = useActions() useEffect(() => { setOnModalOk(() => () => { @@ -68,9 +68,23 @@ const SalesOrderInfo = () => { formValid: false, lock: null, loading: false, + editDisabled: false, objectData: {} }) + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, salesOrderId] + ) + useDashboardObjectTools(currentObjectTools) + const actions = { edit: () => { orderItemsTableRef?.current?.startEditing?.() @@ -87,251 +101,246 @@ const SalesOrderInfo = () => { objectFormRef?.current?.handleUpdate?.() return true }, - delete: () => { - objectFormRef?.current?.handleDelete?.() - return true - }, } - const editDisabled = getModelByName('salesOrder') - .actions.find((action) => action.name === 'edit') - .disabled(objectFormState.objectData) - return ( - - + + + - - - - - - - + + + - - { - actionHandlerRef.current.callAction('finishEdit') - }} - cancelEditing={() => { - actionHandlerRef.current.callAction('cancelEdit') - }} - startEditing={() => { - actionHandlerRef.current.callAction('edit') - }} - editLoading={objectFormState.editLoading} - formValid={objectFormState.formValid} - disabled={ - objectFormState.beingEditedByOther || - objectFormState.loading || - editDisabled - } - loading={objectFormState.editLoading} - /> - - - + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + - - - + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - } - active={collapseState.info} - onToggle={(expanded) => - updateCollapseState('info', expanded) - } - collapseKey='info' - > - } - isEditing={isEditing} - type='salesOrder' - labelWidth='225px' - objectData={objectData} - visibleProperties={{ - items: false - }} - /> - - } - active={collapseState.orderItems} - onToggle={(expanded) => - updateCollapseState('orderItems', expanded) - } - collapseKey='orderItems' - > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='salesOrder' + labelWidth='225px' + objectData={objectData} + visibleProperties={{ + items: false + }} + /> + + } + active={collapseState.orderItems} + onToggle={(expanded) => + updateCollapseState('orderItems', expanded) + } + collapseKey='orderItems' + > + + + } + active={collapseState.shipments} + onToggle={(expanded) => + updateCollapseState('shipments', expanded) + } + collapseKey='shipments' + > + + + } + active={collapseState.invoices} + onToggle={(expanded) => + updateCollapseState('invoices', expanded) + } + collapseKey='invoices' + > + {objectFormState.loading ? ( + + ) : ( - - } - active={collapseState.shipments} - onToggle={(expanded) => - updateCollapseState('shipments', expanded) - } - collapseKey='shipments' - > + )} + + } + active={collapseState.stockEvents} + onToggle={(expanded) => + updateCollapseState('stockEvents', expanded) + } + collapseKey='stockEvents' + > + {objectFormState.loading ? ( + + ) : ( - - } - active={collapseState.invoices} - onToggle={(expanded) => - updateCollapseState('invoices', expanded) - } - collapseKey='invoices' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - } - active={collapseState.stockEvents} - onToggle={(expanded) => - updateCollapseState('stockEvents', expanded) - } - collapseKey='stockEvents' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - )} - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - collapseKey='notes' + )} + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } > - + - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - collapseKey='auditLogs' - > - {objectFormState.loading ? ( - - ) : ( - - )} - - - - + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => updateCollapseState('auditLogs', expanded)} + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + ) } diff --git a/src/components/Dashboard/Sales/SalesOverview.jsx b/src/components/Dashboard/Sales/SalesOverview.jsx index 0981fa77..a29e34b3 100644 --- a/src/components/Dashboard/Sales/SalesOverview.jsx +++ b/src/components/Dashboard/Sales/SalesOverview.jsx @@ -1,52 +1,291 @@ -import { Flex } from 'antd' -import useCollapseState from '../hooks/useCollapseState' +import DashboardOverviewPage from '../common/DashboardOverviewPage' import StatsDisplay from '../common/StatsDisplay' -import InfoCollapse from '../common/InfoCollapse' -import ScrollBox from '../common/ScrollBox' +import ModelHistoryDisplay from '../common/ModelHistoryDisplay' +import ObjectTable from '../common/ObjectTable' + +const collapseDefaults = { + salesOrderStats: true, + orderItemStats: true, + listingStats: true, + marketplaceStats: true, + clientStats: true, + shipmentStats: true, + salesOrderCountHistory: true, + salesOrderAmountHistory: true, + listingHistory: true, + orderItemCountHistory: true, + orderItemAmountHistory: true, + shipmentCountHistory: true, + shipmentAmountHistory: true, + confirmedSalesOrders: true, + draftListings: true, + marketplaceHistory: true, + clientHistory: true, + readyMarketplaces: true, + activeClients: true, + sentSalesOrders: true, + activeListings: true, + ordersToShip: true, + plannedOutboundShipments: true +} + +const sections = [ + { + key: 'salesOrderStats', + title: 'Sales Order Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'orderItemStats', + title: 'Order Item Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'listingStats', + title: 'Listing Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'marketplaceStats', + title: 'Marketplace Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'clientStats', + title: 'Client Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'shipmentStats', + title: 'Shipment Statistics', + className: 'no-t-padding-collapse', + content: + }, + { + key: 'salesOrderHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'salesOrderCountHistory', + title: 'Sales Order Count History', + content: ( + + ) + } + ], + [ + { + key: 'salesOrderAmountHistory', + title: 'Sales Order Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'listingHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'listingHistory', + title: 'Listing History', + content: + } + ] + ] + }, + { + key: 'orderItemHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'orderItemCountHistory', + title: 'Order Item Count History', + content: ( + + ) + } + ], + [ + { + key: 'orderItemAmountHistory', + title: 'Order Item Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'shipmentHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'shipmentCountHistory', + title: 'Shipment Count History', + content: ( + + ) + } + ], + [ + { + key: 'shipmentAmountHistory', + title: 'Shipment Amount History', + content: ( + + ) + } + ] + ] + }, + { + key: 'confirmedSalesOrdersAndDraftListingsRow', + type: 'row', + columns: [ + [ + { + key: 'confirmedSalesOrders', + title: 'Confirmed Sales Orders', + content: ( + + ) + } + ], + [ + { + key: 'draftListings', + title: 'Draft Listings', + content: ( + + ) + } + ] + ] + }, + { + key: 'sentSalesOrdersAndActiveListingsRow', + type: 'row', + columns: [ + [ + { + key: 'sentSalesOrders', + title: 'Sent Sales Orders', + content: ( + + ) + } + ], + [ + { + key: 'activeListings', + title: 'Active Listings', + content: ( + + ) + } + ] + ] + }, + { + key: 'marketplaceAndClientHistoryRow', + type: 'row', + columns: [ + [ + { + key: 'marketplaceHistory', + title: 'Marketplace History', + content: + } + ], + [ + { + key: 'clientHistory', + title: 'Client History', + content: + } + ] + ] + }, + { + key: 'readyMarketplacesAndActiveClientsRow', + type: 'row', + columns: [ + [ + { + key: 'readyMarketplaces', + title: 'Ready Marketplaces', + content: ( + + ) + } + ], + [ + { + key: 'activeClients', + title: 'Active Clients', + content: + } + ] + ] + }, + { + key: 'ordersToShipAndPlannedShipmentsRow', + type: 'row', + columns: [ + [ + { + key: 'ordersToShip', + title: 'Orders To Ship', + content: ( + + ) + } + ], + [ + { + key: 'plannedOutboundShipments', + title: 'Planned Outbound Shipments', + content: ( + + ) + } + ] + ] + } +] const SalesOverview = () => { - const [collapseState, updateCollapseState] = useCollapseState( - 'SalesOverview', - { - clientStats: true, - salesOrderStats: true - } - ) - return ( - - - - - updateCollapseState('salesOrderStats', isActive) - } - className='no-t-padding-collapse' - collapseKey='salesOrderStats' - canCollapse={false} - > - - - - - - - + ) } diff --git a/src/components/Dashboard/common/ActionHandler.jsx b/src/components/Dashboard/common/ActionHandler.jsx index 34686cc4..fda1ec98 100644 --- a/src/components/Dashboard/common/ActionHandler.jsx +++ b/src/components/Dashboard/common/ActionHandler.jsx @@ -16,7 +16,9 @@ const ActionHandler = forwardRef( ) => { const location = useLocation() const navigate = useNavigate() - const action = new URLSearchParams(location.search).get(actionParam) + const searchParams = new URLSearchParams(location.search) + const action = searchParams.get(actionParam) + const actionObjectType = searchParams.get('actionObjectType') // Ref to track last executed action const lastExecutedAction = useRef(null) @@ -44,6 +46,7 @@ const ActionHandler = forwardRef( if ( loading == false && action && + !actionObjectType && actions[action] && lastExecutedAction.current !== action ) { @@ -71,6 +74,7 @@ const ActionHandler = forwardRef( }, [ loading, action, + actionObjectType, actions, actionParam, clearAfterExecute, diff --git a/src/components/Dashboard/common/ActionsButton.jsx b/src/components/Dashboard/common/ActionsButton.jsx new file mode 100644 index 00000000..e56b0810 --- /dev/null +++ b/src/components/Dashboard/common/ActionsButton.jsx @@ -0,0 +1,43 @@ +import { Dropdown, Button } from 'antd' +import PropTypes from 'prop-types' +import KeyboardShortcut from './KeyboardShortcut' + +const ActionsButton = ({ + menu = { items: [] }, + onOpenModal, + disabled = false, + buttonProps = {}, + ...dropdownProps +}) => { + const handleOpenModal = () => { + if (disabled || !onOpenModal) return + onOpenModal() + } + + return ( + + + + + + ) +} + +ActionsButton.propTypes = { + menu: PropTypes.object, + onOpenModal: PropTypes.func, + disabled: PropTypes.bool, + buttonProps: PropTypes.object +} + +export default ActionsButton diff --git a/src/components/Dashboard/common/AdaptiveSplitter.jsx b/src/components/Dashboard/common/AdaptiveSplitter.jsx new file mode 100644 index 00000000..2e505d6a --- /dev/null +++ b/src/components/Dashboard/common/AdaptiveSplitter.jsx @@ -0,0 +1,981 @@ +import { + Children, + isValidElement, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState +} from 'react' +import PropTypes from 'prop-types' +import { + DownOutlined, + LeftOutlined, + RightOutlined, + UpOutlined +} from '@ant-design/icons' + +const SIZE_TRANSITION_MS = 300 +const DEFAULT_EXPAND_DRAG_PX = 10 +const KEYBOARD_STEP_PX = 8 + +const sizeType = PropTypes.oneOfType([PropTypes.number, PropTypes.string]) + +const AdaptiveSplitterPanel = () => null +AdaptiveSplitterPanel.displayName = 'AdaptiveSplitterPanel' +AdaptiveSplitterPanel.isAdaptiveSplitterPanel = true +AdaptiveSplitterPanel.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + style: PropTypes.object, + size: sizeType, + defaultSize: sizeType, + min: sizeType, + max: sizeType, + resizable: PropTypes.bool, + collapsible: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.shape({ + start: PropTypes.bool, + end: PropTypes.bool + }) + ]), + collapsed: PropTypes.bool, + collapsedSize: PropTypes.number, + collapseThreshold: PropTypes.number, + expandDragPx: PropTypes.number, + expandedSize: PropTypes.number, + onCollapse: PropTypes.func +} + +const parseSizeToPx = (size, containerSize) => { + if (size == null || size === '') return null + if (typeof size === 'number') { + return Number.isFinite(size) ? size : null + } + const value = String(size).trim() + if (value.endsWith('%')) { + const pct = Number.parseFloat(value) + return Number.isFinite(pct) ? (pct / 100) * containerSize : null + } + const px = Number.parseFloat(value) + return Number.isFinite(px) ? px : null +} + +const clamp = (value, min, max) => Math.min(max, Math.max(min, value)) + +const getPanelItems = (children) => + Children.toArray(children) + .filter( + (child) => + isValidElement(child) && + (child.type === AdaptiveSplitterPanel || + child.type?.isAdaptiveSplitterPanel) + ) + .map((child) => ({ + ...child.props, + key: child.key + })) + +const getCollapsibleSides = (collapsible) => { + if (!collapsible) return { start: false, end: false } + if (collapsible === true) return { start: true, end: true } + return { + start: !!collapsible.start, + end: !!collapsible.end + } +} + +const getBarEdge = (sizes, barIndex) => { + if (barIndex < 0) return 0 + let edge = 0 + for (let i = 0; i <= barIndex; i += 1) { + edge += sizes[i] || 0 + } + return edge +} + +const SplitBar = ({ + active, + resizable, + isVertical, + lazy, + lazyOffset, + ariaNow, + ariaMin, + ariaMax, + startCollapsible, + endCollapsible, + onPointerDown, + onKeyDown, + onCollapseStart, + onCollapseEnd +}) => { + const startIcon = isVertical ? : + const endIcon = isVertical ? : + + return ( +
+ {lazy && active && ( +
+ )} +
+ {startCollapsible && ( + + )} + {endCollapsible && ( + + )} +
+ ) +} + +SplitBar.propTypes = { + active: PropTypes.bool, + resizable: PropTypes.bool, + isVertical: PropTypes.bool, + lazy: PropTypes.bool, + lazyOffset: PropTypes.number, + ariaNow: PropTypes.number, + ariaMin: PropTypes.number, + ariaMax: PropTypes.number, + startCollapsible: PropTypes.bool, + endCollapsible: PropTypes.bool, + onPointerDown: PropTypes.func, + onKeyDown: PropTypes.func, + onCollapseStart: PropTypes.func, + onCollapseEnd: PropTypes.func +} + +const AdaptiveSplitter = ({ + children, + className = '', + style, + layout, + orientation, + vertical = false, + lazy = false, + onResizeStart, + onResize, + onResizeEnd, + onCollapse, + sizeTransitionMs = SIZE_TRANSITION_MS +}) => { + const isVertical = + orientation === 'vertical' || layout === 'vertical' || vertical + const items = useMemo(() => getPanelItems(children), [children]) + + const containerRef = useRef(null) + const [containerSize, setContainerSize] = useState(0) + const [pxSizes, setPxSizes] = useState([]) + const [dragging, setDragging] = useState(false) + const [activeBar, setActiveBar] = useState(null) + const [sizeTransitioning, setSizeTransitioning] = useState(false) + const [ready, setReady] = useState(false) + const [lazyOffset, setLazyOffset] = useState(0) + + const pxSizesRef = useRef(pxSizes) + pxSizesRef.current = pxSizes + const itemsRef = useRef(items) + itemsRef.current = items + const containerSizeRef = useRef(containerSize) + containerSizeRef.current = containerSize + const dragRef = useRef(null) + const lastExpandedRef = useRef([]) + const sizeTransitionTimeoutRef = useRef(null) + const skipNextTransitionRef = useRef(true) + const lazyOffsetRef = useRef(0) + const pendingAnimateSizesRef = useRef(null) + const animateRafRef = useRef([]) + + const getItemMin = useCallback((item, fallback = 0) => { + const parsed = parseSizeToPx(item?.min, containerSizeRef.current) + return parsed ?? fallback + }, []) + + const getItemMax = useCallback((item, fallback = Infinity) => { + const parsed = parseSizeToPx(item?.max, containerSizeRef.current) + return parsed ?? fallback + }, []) + + const rememberExpandedSize = useCallback((size, index, item) => { + if (!item || item.collapsed) return + if (item.collapseThreshold != null && size < item.collapseThreshold) return + lastExpandedRef.current[index] = size + }, []) + + const resolveExpandedSize = useCallback( + (index, item) => { + if (item?.expandedSize != null) return item.expandedSize + if (lastExpandedRef.current[index] != null) { + return lastExpandedRef.current[index] + } + const fromDefault = parseSizeToPx( + item?.defaultSize, + containerSizeRef.current + ) + if (fromDefault != null) return fromDefault + const max = getItemMax(item) + if (Number.isFinite(max)) return max + return item?.collapsedSize ?? 0 + }, + [getItemMax] + ) + + const applyBarResize = useCallback( + (sizes, barIndex, nextStartSize) => { + const currentItems = itemsRef.current + const startItem = currentItems[barIndex] + const endItem = currentItems[barIndex + 1] + if (!startItem || !endItem) return sizes + + const next = sizes.slice() + const startMin = getItemMin(startItem) + const startMax = getItemMax(startItem) + const endMin = getItemMin(endItem) + const endMax = getItemMax(endItem) + const oldStart = next[barIndex] + const oldEnd = next[barIndex + 1] + const pairTotal = oldStart + oldEnd + + let startSize = clamp(nextStartSize, startMin, startMax) + let endSize = pairTotal - startSize + + if (endSize < endMin) { + endSize = endMin + startSize = pairTotal - endMin + } else if (endSize > endMax) { + endSize = endMax + startSize = pairTotal - endMax + } + + startSize = clamp(startSize, startMin, startMax) + next[barIndex] = startSize + next[barIndex + 1] = pairTotal - startSize + return next + }, + [getItemMax, getItemMin] + ) + + const emitCollapse = useCallback( + (index, collapsed, sizes) => { + itemsRef.current[index]?.onCollapse?.(collapsed) + onCollapse?.( + itemsRef.current.map((panel, panelIndex) => + panelIndex === index ? collapsed : !!panel.collapsed + ), + sizes + ) + }, + [onCollapse] + ) + + const remapDragOrigin = useCallback((pointerPos, edge) => { + const drag = dragRef.current + if (!drag) return + drag.originOffset = pointerPos - edge + drag.lastPointerPos = pointerPos + }, []) + + const endDragSession = useCallback(() => { + const drag = dragRef.current + if (drag?.captureTarget?.hasPointerCapture?.(drag.pointerId)) { + drag.captureTarget.releasePointerCapture(drag.pointerId) + } + dragRef.current = null + lazyOffsetRef.current = 0 + setLazyOffset(0) + setDragging(false) + setActiveBar(null) + }, []) + + const cancelPendingAnimate = useCallback(() => { + animateRafRef.current.forEach((id) => cancelAnimationFrame(id)) + animateRafRef.current = [] + pendingAnimateSizesRef.current = null + }, []) + + const startSizeTransition = useCallback(() => { + setSizeTransitioning(true) + if (sizeTransitionTimeoutRef.current) { + clearTimeout(sizeTransitionTimeoutRef.current) + } + sizeTransitionTimeoutRef.current = setTimeout(() => { + setSizeTransitioning(false) + sizeTransitionTimeoutRef.current = null + }, sizeTransitionMs) + }, [sizeTransitionMs]) + + const scheduleAnimatedSizes = useCallback( + (nextSizes) => { + startSizeTransition() + cancelPendingAnimate() + pendingAnimateSizesRef.current = nextSizes + const ids = [] + ids.push( + requestAnimationFrame(() => { + ids.push( + requestAnimationFrame(() => { + animateRafRef.current = [] + const pending = pendingAnimateSizesRef.current + pendingAnimateSizesRef.current = null + if (pending) setPxSizes(pending) + }) + ) + }) + ) + animateRafRef.current = ids + }, + [cancelPendingAnimate, startSizeTransition] + ) + + const measureContainer = useCallback(() => { + const node = containerRef.current + if (!node) return + const next = isVertical ? node.clientHeight : node.clientWidth + if (next !== containerSizeRef.current) { + setContainerSize(next) + } + }, [isVertical]) + + useLayoutEffect(() => { + measureContainer() + const node = containerRef.current + if (!node || typeof ResizeObserver === 'undefined') return undefined + const observer = new ResizeObserver(() => measureContainer()) + observer.observe(node) + return () => observer.disconnect() + }, [measureContainer]) + + useEffect(() => { + const id = requestAnimationFrame(() => setReady(true)) + return () => cancelAnimationFrame(id) + }, []) + + const deriveSizes = useCallback(() => { + if (!containerSize || !items.length) return [] + + const parsed = items.map((item) => { + if (item.collapsed && item.collapsedSize != null && item.size == null) { + return item.collapsedSize + } + return parseSizeToPx(item.size ?? item.defaultSize, containerSize) + }) + + const knownSum = parsed.reduce( + (sum, size) => sum + (size == null ? 0 : size), + 0 + ) + const unknownCount = parsed.filter((size) => size == null).length + const leftover = Math.max(0, containerSize - knownSum) + const unknownShare = unknownCount ? leftover / unknownCount : 0 + + return parsed.map((size, index) => { + const item = items[index] + const next = size == null ? unknownShare : size + const min = getItemMin(item) + const max = getItemMax(item) + const clamped = clamp(next, min, Number.isFinite(max) ? max : next) + rememberExpandedSize(clamped, index, item) + return clamped + }) + }, [containerSize, getItemMax, getItemMin, items, rememberExpandedSize]) + + useLayoutEffect(() => { + const nextSizes = deriveSizes() + if (!nextSizes.length) return + + const drag = dragRef.current + const prev = pxSizesRef.current + const sameLength = prev.length === nextSizes.length + const changed = + !sameLength || nextSizes.some((size, index) => size !== prev[index]) + + if (drag && sameLength && changed && drag.lastPointerPos != null) { + remapDragOrigin(drag.lastPointerPos, getBarEdge(nextSizes, drag.barIndex)) + skipNextTransitionRef.current = true + } + + const shouldAnimate = + changed && + !drag && + sameLength && + !skipNextTransitionRef.current && + prev.length + + skipNextTransitionRef.current = false + + if (shouldAnimate) { + scheduleAnimatedSizes(nextSizes) + return + } + + if (changed) { + cancelPendingAnimate() + setPxSizes(nextSizes) + } + }, [ + cancelPendingAnimate, + deriveSizes, + remapDragOrigin, + scheduleAnimatedSizes + ]) + + useEffect( + () => () => { + if (sizeTransitionTimeoutRef.current) { + clearTimeout(sizeTransitionTimeoutRef.current) + } + animateRafRef.current.forEach((id) => cancelAnimationFrame(id)) + }, + [] + ) + + const getPointerPos = useCallback( + (event) => { + const node = containerRef.current + if (!node) return 0 + const rect = node.getBoundingClientRect() + return isVertical ? event.clientY - rect.top : event.clientX - rect.left + }, + [isVertical] + ) + + const commitSizes = useCallback( + (nextSizes, { notify = true, end = false } = {}) => { + cancelPendingAnimate() + setPxSizes(nextSizes) + nextSizes.forEach((size, index) => + rememberExpandedSize(size, index, itemsRef.current[index]) + ) + if (!notify) return + if (end) onResizeEnd?.(nextSizes) + else onResize?.(nextSizes) + }, + [cancelPendingAnimate, onResize, onResizeEnd, rememberExpandedSize] + ) + + const resolveDragSize = useCallback( + (barIndex, pointerPos, originOffset, currentSizes) => { + const item = itemsRef.current[barIndex] + const drag = dragRef.current + const collapsed = + drag?.barIndex === barIndex ? drag.collapsed : !!item?.collapsed + const rawEdge = pointerPos - originOffset + const startSize = rawEdge - getBarEdge(currentSizes, barIndex - 1) + + const collapsedSize = item?.collapsedSize ?? getItemMin(item) + const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX + const collapseThreshold = item?.collapseThreshold + + if (collapsed) { + const preview = Math.max(collapsedSize, startSize) + if (preview - collapsedSize > expandDragPx) { + return { + expand: true, + collapse: false, + startSize: preview + } + } + return { + expand: false, + collapse: false, + startSize: preview + } + } + + if (drag?.blockCollapse) { + if (startSize <= collapsedSize + expandDragPx) { + return { + expand: false, + collapse: true, + startSize: collapsedSize + } + } + if (collapseThreshold != null && startSize >= collapseThreshold) { + drag.blockCollapse = false + } + return { + expand: false, + collapse: false, + startSize: Math.max(startSize, collapsedSize) + } + } + + if (collapseThreshold != null && startSize < collapseThreshold) { + return { + expand: false, + collapse: true, + startSize: collapsedSize + } + } + + return { + expand: false, + collapse: false, + startSize + } + }, + [getItemMin] + ) + + const handlePointerMove = useCallback( + (event) => { + const drag = dragRef.current + if (!drag || event.pointerId !== drag.pointerId) return + + const pointerPos = getPointerPos(event) + drag.lastPointerPos = pointerPos + const currentSizes = pxSizesRef.current + const resolved = resolveDragSize( + drag.barIndex, + pointerPos, + drag.originOffset, + currentSizes + ) + + if (lazy) { + const startEdge = getBarEdge(drag.startSizes, drag.barIndex) + const nextOffset = + getBarEdge( + applyBarResize(drag.startSizes, drag.barIndex, resolved.startSize), + drag.barIndex + ) - startEdge + lazyOffsetRef.current = nextOffset + setLazyOffset(nextOffset) + return + } + + const nextSizes = applyBarResize( + currentSizes, + drag.barIndex, + resolved.startSize + ) + + if (resolved.expand) { + drag.collapsed = false + drag.blockCollapse = true + skipNextTransitionRef.current = true + remapDragOrigin(pointerPos, getBarEdge(nextSizes, drag.barIndex)) + emitCollapse(drag.barIndex, false, nextSizes) + commitSizes(nextSizes) + return + } + + if (resolved.collapse) { + const barIndex = drag.barIndex + endDragSession() + emitCollapse(barIndex, true, nextSizes) + onResizeEnd?.(nextSizes) + return + } + + commitSizes(nextSizes) + }, + [ + applyBarResize, + commitSizes, + emitCollapse, + getPointerPos, + lazy, + endDragSession, + onResizeEnd, + remapDragOrigin, + resolveDragSize + ] + ) + + const stopDragging = useCallback( + (event) => { + const drag = dragRef.current + if (!drag || (event && event.pointerId !== drag.pointerId)) return + + let nextSizes = pxSizesRef.current.slice() + + if (lazy) { + const pointerPos = event ? getPointerPos(event) : drag.lastPointerPos + const resolved = resolveDragSize( + drag.barIndex, + pointerPos, + drag.originOffset, + drag.startSizes + ) + nextSizes = applyBarResize( + drag.startSizes, + drag.barIndex, + resolved.startSize + ) + if (resolved.expand) { + drag.collapsed = false + drag.blockCollapse = true + emitCollapse(drag.barIndex, false, nextSizes) + } + if (resolved.collapse) { + drag.collapsed = true + drag.blockCollapse = false + emitCollapse(drag.barIndex, true, nextSizes) + } + } + + const item = itemsRef.current[drag.barIndex] + const collapsed = drag.collapsed ?? item?.collapsed + if (collapsed) { + const collapsedSize = item.collapsedSize ?? getItemMin(item) + if (nextSizes[drag.barIndex] !== collapsedSize) { + nextSizes = applyBarResize(nextSizes, drag.barIndex, collapsedSize) + } + } + + endDragSession() + skipNextTransitionRef.current = true + commitSizes(nextSizes, { end: true }) + }, + [ + applyBarResize, + commitSizes, + emitCollapse, + endDragSession, + getItemMin, + getPointerPos, + lazy, + resolveDragSize + ] + ) + + const startDragging = useCallback( + (barIndex, event) => { + const item = itemsRef.current[barIndex] + const nextItem = itemsRef.current[barIndex + 1] + if ( + !pxSizesRef.current.length || + item?.resizable === false || + nextItem?.resizable === false + ) { + return + } + + event.preventDefault() + event.stopPropagation() + event.currentTarget.setPointerCapture?.(event.pointerId) + + const pointerPos = getPointerPos(event) + const startSizes = pxSizesRef.current.slice() + + dragRef.current = { + barIndex, + pointerId: event.pointerId, + captureTarget: event.currentTarget, + originOffset: pointerPos - getBarEdge(startSizes, barIndex), + lastPointerPos: pointerPos, + startSizes, + collapsed: !!item.collapsed, + blockCollapse: false + } + skipNextTransitionRef.current = true + setSizeTransitioning(false) + setDragging(true) + setActiveBar(barIndex) + onResizeStart?.(startSizes) + }, + [getPointerPos, onResizeStart] + ) + + useEffect(() => { + if (!dragging) return undefined + window.addEventListener('pointermove', handlePointerMove) + window.addEventListener('pointerup', stopDragging) + window.addEventListener('pointercancel', stopDragging) + return () => { + window.removeEventListener('pointermove', handlePointerMove) + window.removeEventListener('pointerup', stopDragging) + window.removeEventListener('pointercancel', stopDragging) + } + }, [dragging, handlePointerMove, stopDragging]) + + const collapseViaControl = useCallback( + (barIndex, side) => { + const itemIndex = side === 'start' ? barIndex : barIndex + 1 + const item = itemsRef.current[itemIndex] + if (!item) return + + const collapsedSize = item.collapsedSize ?? getItemMin(item) + const current = pxSizesRef.current[itemIndex] + const alreadyCollapsed = + item.collapsed || + (item.collapseThreshold != null + ? current < item.collapseThreshold + : current <= collapsedSize + 1) + const restored = resolveExpandedSize(itemIndex, item) + let nextStartSize + + if (side === 'start') { + nextStartSize = alreadyCollapsed ? restored : collapsedSize + } else { + const neighbor = pxSizesRef.current[barIndex] + const nextPanelSize = alreadyCollapsed ? restored : collapsedSize + nextStartSize = neighbor + (current - nextPanelSize) + } + + if (!alreadyCollapsed) { + rememberExpandedSize(current, itemIndex, item) + } + + const nextSizes = applyBarResize( + pxSizesRef.current, + barIndex, + nextStartSize + ) + emitCollapse(itemIndex, !alreadyCollapsed, nextSizes) + }, + [ + applyBarResize, + emitCollapse, + getItemMin, + rememberExpandedSize, + resolveExpandedSize + ] + ) + + const handleBarKeyDown = useCallback( + (barIndex, event) => { + const backward = isVertical + ? event.key === 'ArrowUp' + : event.key === 'ArrowLeft' + const forward = isVertical + ? event.key === 'ArrowDown' + : event.key === 'ArrowRight' + if (!backward && !forward) return + + event.preventDefault() + const delta = forward ? KEYBOARD_STEP_PX : -KEYBOARD_STEP_PX + const item = itemsRef.current[barIndex] + const proposed = pxSizesRef.current[barIndex] + delta + const collapsedSize = item?.collapsedSize ?? getItemMin(item) + const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX + + if ( + item?.collapseThreshold != null && + proposed < item.collapseThreshold && + !item.collapsed + ) { + rememberExpandedSize(pxSizesRef.current[barIndex], barIndex, item) + const collapsedSizes = applyBarResize( + pxSizesRef.current, + barIndex, + collapsedSize + ) + emitCollapse(barIndex, true, collapsedSizes) + commitSizes(collapsedSizes, { end: true }) + return + } + + if (item?.collapsed && proposed - collapsedSize > expandDragPx) { + const expandedSizes = applyBarResize( + pxSizesRef.current, + barIndex, + resolveExpandedSize(barIndex, item) + ) + emitCollapse(barIndex, false, expandedSizes) + commitSizes(expandedSizes, { end: true }) + return + } + + commitSizes(applyBarResize(pxSizesRef.current, barIndex, proposed), { + end: true + }) + }, + [ + applyBarResize, + commitSizes, + emitCollapse, + getItemMin, + isVertical, + rememberExpandedSize, + resolveExpandedSize + ] + ) + + const orientationClass = isVertical ? 'vertical' : 'horizontal' + const rootClassName = [ + 'adaptive-splitter', + 'ant-splitter', + `adaptive-splitter-${orientationClass}`, + `ant-splitter-${orientationClass}`, + ready ? 'is-ready' : '', + sizeTransitioning ? 'is-size-transitioning' : '', + dragging ? 'is-dragging' : '', + className + ] + .filter(Boolean) + .join(' ') + + return ( +
+ {items.flatMap((item, index) => { + const size = pxSizes[index] ?? 0 + const isFill = + item.size == null && + item.defaultSize == null && + !item.collapsed && + index === items.length - 1 + const minPx = getItemMin(item) + const maxPx = getItemMax(item) + const panelStyle = isFill + ? { + flex: '1 1 auto', + minWidth: isVertical ? 0 : minPx, + minHeight: isVertical ? minPx : 0, + overflow: 'auto', + ...item.style + } + : { + flex: `0 0 ${size}px`, + [isVertical ? 'height' : 'width']: size, + minWidth: isVertical ? undefined : minPx, + maxWidth: + isVertical || !Number.isFinite(maxPx) ? undefined : maxPx, + minHeight: isVertical ? minPx : undefined, + maxHeight: + !isVertical || !Number.isFinite(maxPx) ? undefined : maxPx, + overflow: 'auto', + ...item.style + } + + const collapsible = getCollapsibleSides(item.collapsible) + const nextCollapsible = getCollapsibleSides( + items[index + 1]?.collapsible + ) + const showBar = index < items.length - 1 + const barResizable = + item.resizable !== false && items[index + 1]?.resizable !== false + + const nodes = [ +
+ {item.children} +
+ ] + + if (showBar) { + nodes.push( + startDragging(index, event)} + onKeyDown={(event) => handleBarKeyDown(index, event)} + onCollapseStart={() => collapseViaControl(index, 'start')} + onCollapseEnd={() => collapseViaControl(index, 'end')} + /> + ) + } + + return nodes + })} + {dragging && ( +
+ )} +
+ ) +} + +AdaptiveSplitter.Panel = AdaptiveSplitterPanel +AdaptiveSplitter.SIZE_TRANSITION_MS = SIZE_TRANSITION_MS + +AdaptiveSplitter.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + style: PropTypes.object, + layout: PropTypes.oneOf(['horizontal', 'vertical']), + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + vertical: PropTypes.bool, + lazy: PropTypes.bool, + onResizeStart: PropTypes.func, + onResize: PropTypes.func, + onResizeEnd: PropTypes.func, + onCollapse: PropTypes.func, + sizeTransitionMs: PropTypes.number +} + +export default AdaptiveSplitter diff --git a/src/components/Dashboard/common/AddressDisplay.jsx b/src/components/Dashboard/common/AddressDisplay.jsx index 1d4bf526..39f733a1 100644 --- a/src/components/Dashboard/common/AddressDisplay.jsx +++ b/src/components/Dashboard/common/AddressDisplay.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types' import { Flex, Typography, Input } from 'antd' import CountryDisplay from './CountryDisplay' import CountrySelect from './CountrySelect' +import ElipsisText from './ElipsisText' const { Text } = Typography @@ -50,9 +51,9 @@ const AddressDisplay = ({ {isCountry ? ( ) : ( - + {fieldValue} - + )} ) diff --git a/src/components/Dashboard/common/BulkObjectActions.jsx b/src/components/Dashboard/common/BulkObjectActions.jsx new file mode 100644 index 00000000..41821064 --- /dev/null +++ b/src/components/Dashboard/common/BulkObjectActions.jsx @@ -0,0 +1,196 @@ +import { useContext, useEffect, useMemo, useRef, useState } from 'react' +import { Button, Divider, Dropdown, Flex, Modal } from 'antd' +import PropTypes from 'prop-types' +import { createElement } from 'react' +import { getModelByName } from '../../../database/ObjectModels' +import { hasActionPermission } from '../../../database/permissions' +import { AuthContext } from '../context/AuthContext' +import { ApiServerContext } from '../context/ApiServerContext' +import { useObjectListView } from '../context/ObjectListViewContext' +import { useMessageContext } from '../context/MessageContext' +import MessageDialogView from './MessageDialogView' +import BinIcon from '../../Icons/BinIcon' + +const BulkObjectActions = ({ type, onReload }) => { + const model = getModelByName(type) + const { userProfile } = useContext(AuthContext) + const { deleteObjects, fetchObjects } = useContext(ApiServerContext) + const { showSuccess, showError } = useMessageContext() + const { bulkSelection, clearBulkSelection } = useObjectListView() + const [activeAction, setActiveAction] = useState(null) + const [actionLoading, setActionLoading] = useState(false) + const [selectedObjects, setSelectedObjects] = useState([]) + const [selectionLoading, setSelectionLoading] = useState(false) + const fetchObjectsRef = useRef(fetchObjects) + fetchObjectsRef.current = fetchObjects + + const selectionKey = useMemo( + () => + JSON.stringify({ + filter: bulkSelection?.filter || {}, + masterFilter: bulkSelection?.masterFilter || {} + }), + [bulkSelection?.filter, bulkSelection?.masterFilter] + ) + + useEffect(() => { + if (!bulkSelection) { + setSelectedObjects([]) + setSelectionLoading(false) + return + } + + let cancelled = false + + const loadSelectedObjects = async () => { + setSelectionLoading(true) + try { + if (Array.isArray(bulkSelection.selectedObjects)) { + setSelectedObjects(bulkSelection.selectedObjects) + return + } + + const objects = [] + const filter = { + ...(bulkSelection.masterFilter || {}), + ...(bulkSelection.filter || {}) + } + let page = 1 + + while (!cancelled) { + const result = await fetchObjectsRef.current(type, { + page, + limit: 100, + filter + }) + objects.push(...(result?.data || [])) + if (!result?.hasMore) break + page += 1 + } + + if (!cancelled) setSelectedObjects(objects) + } catch (error) { + console.error(`Error loading selected ${model.labelPlural}:`, error) + if (!cancelled) setSelectedObjects([]) + } finally { + if (!cancelled) setSelectionLoading(false) + } + } + + loadSelectedObjects() + return () => { + cancelled = true + } + // selectionKey intentionally represents both selection filters. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectionKey, type]) + + const actions = useMemo( + () => + (model.actions || []).filter( + (action) => + action.bulk === true && + hasActionPermission(userProfile, model, action.name) + ), + [model, userProfile] + ) + + if (!bulkSelection || actions.length === 0) return null + + const isActionDisabled = (action) => { + if (selectionLoading || selectedObjects.length === 0) return true + if (typeof action.disabled === 'function') { + return selectedObjects + .map((objectData) => + action.disabled({ ...objectData, _user: userProfile }) + ) + .every(Boolean) + } + return action.disabled === true + } + + const menu = { + items: actions.map((action) => ({ + key: action.name, + label: action.label, + danger: action.danger === true, + icon: action.icon ? createElement(action.icon) : undefined, + disabled: isActionDisabled(action) + })), + onClick: ({ key }) => { + const action = actions.find((item) => item.name === key) + if (action) setActiveAction(action) + } + } + + const closeModal = () => { + if (!actionLoading) setActiveAction(null) + } + + const runBulkDelete = async () => { + setActionLoading(true) + try { + const result = await deleteObjects(type, { + filter: bulkSelection.filter || {}, + masterFilter: bulkSelection.masterFilter || {} + }) + const deletedCount = result?.deletedCount || 0 + const failedCount = result?.failedCount || 0 + + showSuccess( + `${deletedCount} ${deletedCount === 1 ? model.label.toLowerCase() : model.labelPlural.toLowerCase()} deleted successfully!` + ) + if (failedCount > 0) { + showError( + `${failedCount} ${failedCount === 1 ? model.label.toLowerCase() : model.labelPlural.toLowerCase()} could not be deleted.` + ) + } + setActiveAction(null) + clearBulkSelection() + onReload?.() + } catch (error) { + console.error(`Error deleting ${model.labelPlural.toLowerCase()}:`, error) + showError(`Failed to delete selected ${model.labelPlural.toLowerCase()}.`) + } finally { + setActionLoading(false) + } + } + + return ( + <> + + + + + + + + {activeAction?.name === 'delete' && ( + } + title={`Are you sure you want to delete the selected ${model.labelPlural.toLowerCase()}?`} + description='This will permanently delete every object matching the selection.' + onOk={runBulkDelete} + okText='Delete' + okLoading={actionLoading} + danger + /> + )} + + + ) +} + +BulkObjectActions.propTypes = { + type: PropTypes.string.isRequired, + onReload: PropTypes.func +} + +export default BulkObjectActions diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx index e4a71631..0bb14715 100644 --- a/src/components/Dashboard/common/CodeBlockEditor.jsx +++ b/src/components/Dashboard/common/CodeBlockEditor.jsx @@ -1,28 +1,161 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, useRef, useEffect } from 'react' import PropTypes from 'prop-types' import CodeMirror from '@uiw/react-codemirror' -import { javascript } from '@codemirror/lang-javascript' -import { python } from '@codemirror/lang-python' -import { json } from '@codemirror/lang-json' -import { css } from '@codemirror/lang-css' -import { html } from '@codemirror/lang-html' -import { markdown } from '@codemirror/lang-markdown' -import { sql } from '@codemirror/lang-sql' -import { java } from '@codemirror/lang-java' -import { cpp } from '@codemirror/lang-cpp' -import { rust } from '@codemirror/lang-rust' -import { go } from '@codemirror/lang-go' -import { php } from '@codemirror/lang-php' -import { yaml } from '@codemirror/lang-yaml' -import { xml } from '@codemirror/lang-xml' -import { oneDark } from '@codemirror/theme-one-dark' +import { EditorView, Decoration, ViewPlugin } from '@codemirror/view' +import { StateField } from '@codemirror/state' +import { + farmControlDark, + farmControlLight +} from '../../../codemirror/farmControlTheme' +import { + getCodeLanguageExtension, + toEditorCode +} from './codeBlockEditorUtils' import { useThemeContext } from '../context/ThemeContext' -import { Button, Modal, Typography } from 'antd' +import ScrollBox from './ScrollBox' +import { Button, Modal, Typography, Card } from 'antd' const { Link } = Typography +const codeBlockEditorLayoutTheme = EditorView.theme({ + '&': { + height: 'auto !important' + }, + '.cm-scroller': { + overflow: 'visible !important' + }, + '.cm-gutters': { + position: 'sticky', + left: '0', + zIndex: 2 + } +}) + +function createScrollBoxSyncExtension() { + return ViewPlugin.fromClass( + class { + constructor(view) { + this.view = view + this.scrollEl = view.dom.closest('.simplebar-content-wrapper') + this.syncing = false + this.onExternalScroll = this.onExternalScroll.bind(this) + this.onEditorScroll = this.onEditorScroll.bind(this) + + if (this.scrollEl) { + this.scrollEl.addEventListener('scroll', this.onExternalScroll, { + passive: true + }) + view.scrollDOM.addEventListener('scroll', this.onEditorScroll, { + passive: true + }) + } + } + + syncFromExternal() { + const { view, scrollEl } = this + if (this.syncing || !scrollEl) return + this.syncing = true + view.scrollDOM.scrollTop = scrollEl.scrollTop + view.scrollDOM.scrollLeft = scrollEl.scrollLeft + view.scrollDOM.dispatchEvent(new Event('scroll')) + this.syncing = false + } + + syncFromEditor() { + const { view, scrollEl } = this + if (this.syncing || !scrollEl) return + this.syncing = true + scrollEl.scrollTop = view.scrollDOM.scrollTop + scrollEl.scrollLeft = view.scrollDOM.scrollLeft + this.syncing = false + } + + onExternalScroll() { + this.syncFromExternal() + } + + onEditorScroll() { + this.syncFromEditor() + } + + destroy() { + if (this.scrollEl) { + this.scrollEl.removeEventListener('scroll', this.onExternalScroll) + this.view.scrollDOM.removeEventListener('scroll', this.onEditorScroll) + } + } + } + ) +} + +function buildErrorLineDecorations(state, errorLineSet) { + if (!errorLineSet.size) { + return Decoration.none + } + + const decorations = [] + for (let i = 1; i <= state.doc.lines; i++) { + if (errorLineSet.has(i)) { + const line = state.doc.line(i) + decorations.push( + Decoration.line({ class: 'cm-errorLine' }).range(line.from) + ) + } + } + return Decoration.set(decorations, true) +} + +function createCursorChangeExtension(onCursorChangeRef) { + return ViewPlugin.fromClass( + class { + constructor(view) { + this.report(view) + } + + update(update) { + if (update.selectionSet || update.docChanged) { + this.report(update.view) + } + } + + report(view) { + const onCursorChange = onCursorChangeRef.current + if (typeof onCursorChange !== 'function') { + return + } + const head = view.state.selection.main.head + const line = view.state.doc.lineAt(head) + onCursorChange({ + offset: head, + line: line.number, + column: head - line.from, + content: view.state.doc.toString() + }) + } + } + ) +} + +function createErrorLineExtension(errorLines) { + const errorLineSet = new Set(errorLines) + + return StateField.define({ + create(state) { + return buildErrorLineDecorations(state, errorLineSet) + }, + update(decorations, tr) { + if (tr.docChanged) { + return buildErrorLineDecorations(tr.state, errorLineSet) + } + return decorations.map(tr.changes) + }, + provide: (f) => EditorView.decorations.from(f) + }) +} + export default function CodeBlockEditor({ code = '', + value, language = 'javascript', style = {}, className = '', @@ -31,60 +164,56 @@ export default function CodeBlockEditor({ height = 'auto', showLineNumbers = true, disabled = false, - minimal = false + minimal = false, + autoCompleteObject = null, + resourceNames = [], + errorLines = [], + templateType, + onCursorChange = null }) { const { isDarkMode } = useThemeContext() const [codeMirrorOpen, setCodeMirrorOpen] = useState(false) - var editorCode = code + const sourceCode = value !== undefined ? value : code + const editorCode = toEditorCode(sourceCode, language) + const onCursorChangeRef = useRef(onCursorChange) - if (typeof code == 'object' && language == 'json') { - editorCode = JSON.stringify(code, null, 2) - } + useEffect(() => { + onCursorChangeRef.current = onCursorChange + }, [onCursorChange]) - // Map language to CodeMirror extension - const languageExtension = useMemo(() => { - switch (language.toLowerCase()) { - case 'javascript': - case 'js': - return javascript() - case 'python': - case 'py': - return python() - case 'json': - return json() - case 'xml': - return xml() - case 'ejs': - return xml() - case 'html': - return html() - case 'css': - return css() - case 'markdown': - case 'md': - return markdown() - case 'sql': - return sql() - case 'java': - return java() - case 'cpp': - case 'c++': - case 'c': - return cpp() - case 'rust': - case 'rs': - return rust() - case 'go': - return go() - case 'php': - return php() - case 'yaml': - case 'yml': - return yaml() - default: - return javascript() // Default fallback + const languageExtension = useMemo( + () => + getCodeLanguageExtension( + language, + autoCompleteObject, + templateType, + resourceNames + ), + [language, autoCompleteObject, templateType, resourceNames] + ) + + const errorLineExtension = useMemo(() => { + if (!errorLines?.length) { + return [] } - }, [language]) + return [createErrorLineExtension(errorLines)] + }, [errorLines]) + + const cursorChangeExtension = useMemo( + () => createCursorChangeExtension(onCursorChangeRef), + [] + ) + + const editorExtensions = useMemo( + () => [ + languageExtension, + ...errorLineExtension, + cursorChangeExtension, + codeBlockEditorLayoutTheme, + createScrollBoxSyncExtension() + ], + [languageExtension, errorLineExtension, cursorChangeExtension] + ) const handleOnChange = (value) => { if (typeof code == 'object' && language == 'json') { @@ -97,43 +226,56 @@ export default function CodeBlockEditor({ const codeMirror = (
- + styles={{ body: { padding: 0, height: '100%', overflow: 'visible' } }} + > + + + +
) @@ -176,7 +318,8 @@ export default function CodeBlockEditor({ } CodeBlockEditor.propTypes = { - code: PropTypes.string.isRequired, + code: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), language: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf(PropTypes.string) @@ -188,5 +331,10 @@ CodeBlockEditor.propTypes = { height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), showLineNumbers: PropTypes.bool, disabled: PropTypes.bool, - minimal: PropTypes.bool + minimal: PropTypes.bool, + autoCompleteObject: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + resourceNames: PropTypes.arrayOf(PropTypes.string), + errorLines: PropTypes.arrayOf(PropTypes.number), + templateType: PropTypes.string, + onCursorChange: PropTypes.func } diff --git a/src/components/Dashboard/common/CodeDiffViewer.jsx b/src/components/Dashboard/common/CodeDiffViewer.jsx new file mode 100644 index 00000000..26273674 --- /dev/null +++ b/src/components/Dashboard/common/CodeDiffViewer.jsx @@ -0,0 +1,80 @@ +import { useLayoutEffect, useMemo, useRef } from 'react' +import PropTypes from 'prop-types' +import { MergeView } from '@codemirror/merge' +import { EditorState } from '@codemirror/state' +import { EditorView } from '@codemirror/view' +import { + farmControlDark, + farmControlLight +} from '../../../codemirror/farmControlTheme' +import { basicSetup } from 'codemirror' +import { useThemeContext } from '../context/ThemeContext' +import { + getCodeLanguageExtension, + toEditorCode +} from './codeBlockEditorUtils' + +export default function CodeDiffViewer({ + oldCode = '', + newCode = '', + language = 'javascript' +}) { + const { isDarkMode } = useThemeContext() + const parentRef = useRef(null) + const oldDoc = toEditorCode(oldCode, language) + const newDoc = toEditorCode(newCode, language) + + const languageExtension = useMemo( + () => getCodeLanguageExtension(language), + [language] + ) + + useLayoutEffect(() => { + if (!parentRef.current) return + + const sharedExtensions = [ + basicSetup, + languageExtension, + EditorView.editable.of(false), + EditorState.readOnly.of(true), + ...(isDarkMode ? farmControlDark : farmControlLight) + ] + + const view = new MergeView({ + a: { + doc: oldDoc, + extensions: sharedExtensions + }, + b: { + doc: newDoc, + extensions: sharedExtensions + }, + parent: parentRef.current, + highlightChanges: true, + gutter: true, + collapseUnchanged: { margin: 3, minSize: 6 } + }) + + return () => { + view.destroy() + } + }, [oldDoc, newDoc, languageExtension, isDarkMode]) + + return ( +
+ ) +} + +CodeDiffViewer.propTypes = { + oldCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + newCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + language: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string) + ]) +} diff --git a/src/components/Dashboard/common/ColorSelector.jsx b/src/components/Dashboard/common/ColorSelector.jsx index fab7d860..6f87111f 100644 --- a/src/components/Dashboard/common/ColorSelector.jsx +++ b/src/components/Dashboard/common/ColorSelector.jsx @@ -2,7 +2,16 @@ import { useState, useEffect } from 'react' import { ColorPicker, Checkbox, Flex, Row, Col, Divider } from 'antd' import PropTypes from 'prop-types' -const ColorSelector = ({ value, onChange, disabled, required = false }) => { +const ColorSelector = ({ + value, + onChange, + disabled, + required = false, + size, + showText = true, + className, + onClick +}) => { // Determine initial enabled state based on value const [colorEnabled, setColorEnabled] = useState(!!value) @@ -97,18 +106,24 @@ const ColorSelector = ({ value, onChange, disabled, required = false }) => { ] return ( - + {!required && ( - +
+ ) +} + +DashboardOverviewFlexColumns.propTypes = { + dragHandle: PropTypes.node, + onRemove: PropTypes.func, + isLayoutEditing: PropTypes.bool, + isDragOver: PropTypes.bool, + onDragOver: PropTypes.func, + onDrop: PropTypes.func, + onDragLeave: PropTypes.func, + proportions: PropTypes.arrayOf(PropTypes.number), + onProportionsChange: PropTypes.func, + children: PropTypes.node +} + +export default DashboardOverviewFlexColumns diff --git a/src/components/Dashboard/common/DashboardOverviewPage.jsx b/src/components/Dashboard/common/DashboardOverviewPage.jsx new file mode 100644 index 00000000..2d1cb7dc --- /dev/null +++ b/src/components/Dashboard/common/DashboardOverviewPage.jsx @@ -0,0 +1,1011 @@ +import { + cloneElement, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react' +import { Flex, Space } from 'antd' +import PropTypes from 'prop-types' +import { HolderOutlined } from '@ant-design/icons' +import ActionsButton from './ActionsButton' +import ViewButton from './ViewButton' +import EditButtons from './EditButtons' +import ScrollBox from './ScrollBox' +import InfoCollapse from './InfoCollapse' +import StatsDisplay, { StatsViewButton } from './StatsDisplay' +import ModelHistoryDisplay from './ModelHistoryDisplay' +import LazyWhenVisible from './LazyWhenVisible' +import ObjectTable from './ObjectTable' +import DashboardOverviewFlexColumns from './DashboardOverviewFlexColumns' +import useCollapseState from '../hooks/useCollapseState' +import usePageLayout, { + mergeOrder, + normalizeColumnProportions +} from '../hooks/usePageLayout' +import { OverviewLayoutContext } from '../context/OverviewLayoutContext' + +const flattenOverviewSections = (sectionList = []) => { + const leaves = {} + const defaultFlexColumns = {} + const defaultSectionOrder = [] + + for (const section of sectionList) { + if (section?.type === 'row') { + const childKeys = [] + for (const column of section.columns || []) { + for (const item of column) { + if (item?.key) { + leaves[item.key] = item + childKeys.push(item.key) + } + } + } + defaultFlexColumns[section.key] = childKeys + defaultSectionOrder.push(section.key) + } else if (section?.key) { + leaves[section.key] = section + defaultSectionOrder.push(section.key) + } + } + + return { leaves, defaultSectionOrder, defaultFlexColumns } +} + +const collectViewItems = (sectionList = []) => { + const items = [] + const visit = (section) => { + if (section?.type === 'row') { + section.columns?.forEach((column) => column.forEach(visit)) + return + } + if (section?.key && section?.title) { + items.push({ key: section.key, label: section.title }) + } + } + sectionList.forEach(visit) + return items +} + +const copyLayout = (layout) => ({ + sectionOrder: [...(layout?.sectionOrder || [])], + flexColumns: Object.fromEntries( + Object.entries(layout?.flexColumns || {}).map(([key, value]) => [ + key, + Array.isArray(value) ? [...value] : [] + ]) + ), + statsOrder: Object.fromEntries( + Object.entries(layout?.statsOrder || {}).map(([key, value]) => [ + key, + Array.isArray(value) ? [...value] : value + ]) + ), + statsVisibility: Object.fromEntries( + Object.entries(layout?.statsVisibility || {}).map(([key, value]) => [ + key, + value && typeof value === 'object' && !Array.isArray(value) + ? { ...value } + : {} + ]) + ), + sectionHeights: { ...(layout?.sectionHeights || {}) }, + flexColumnProportions: Object.fromEntries( + Object.entries(layout?.flexColumnProportions || {}).map(([key, value]) => [ + key, + Array.isArray(value) ? [...value] : [] + ]) + ) +}) + +const createFlexColumnsId = () => + `flexColumns-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + +const DEFAULT_HISTORY_HEIGHT = 400 +const DEFAULT_TABLE_HEIGHT = 360 +const EMPTY_ORDER = [] +const EMPTY_COLUMNS = {} + +const getVerticalEdgeInsert = (element, clientY) => { + const rect = element.getBoundingClientRect() + const edge = Math.min(48, rect.height / 3) + const y = clientY - rect.top + if (y <= edge) return { insertBefore: true } + if (y >= rect.height - edge) return { insertBefore: false } + return null +} + +const DashboardOverviewPage = ({ + pageName, + collapseDefaults = {}, + viewItems, + actionMenu, + onOpenActionsModal, + sections = [] +}) => { + const [savedCollapseState, updateCollapseState] = useCollapseState( + pageName, + collapseDefaults + ) + const collapseState = useMemo( + () => ({ ...collapseDefaults, ...savedCollapseState }), + [collapseDefaults, savedCollapseState] + ) + + const { leaves, defaultSectionOrder, defaultFlexColumns } = useMemo( + () => flattenOverviewSections(sections), + [sections] + ) + const [savedLayout, updatePageLayout] = usePageLayout(pageName, { + sectionOrder: defaultSectionOrder, + flexColumns: defaultFlexColumns, + statsOrder: {}, + statsVisibility: {} + }) + + const [isLayoutEditing, setIsLayoutEditing] = useState(false) + const [draftLayout, setDraftLayout] = useState(null) + const [draggedItem, setDraggedItem] = useState(null) + const [dropTarget, setDropTarget] = useState(null) + const [liveHeights, setLiveHeights] = useState({}) + const scrollRef = useRef(null) + const dragYRef = useRef(null) + const scrollRafRef = useRef(0) + + const displayLayout = + isLayoutEditing && draftLayout ? draftLayout : savedLayout + const resolvedViewItems = viewItems || collectViewItems(sections) + const sectionsByKey = leaves + const orderedSectionKeys = useMemo( + () => displayLayout.sectionOrder || EMPTY_ORDER, + [displayLayout.sectionOrder] + ) + const flexColumns = useMemo( + () => displayLayout.flexColumns || EMPTY_COLUMNS, + [displayLayout.flexColumns] + ) + const isGroupKey = (key) => + Object.prototype.hasOwnProperty.call(flexColumns, key) + + const startEditing = useCallback(() => { + setDraftLayout(copyLayout(savedLayout)) + setIsLayoutEditing(true) + }, [savedLayout]) + + const clearDragState = useCallback(() => { + setDraggedItem(null) + setDropTarget(null) + }, []) + + const getSectionHeight = useCallback( + (key, fallback) => { + if (liveHeights[key] != null) return liveHeights[key] + const saved = savedLayout.sectionHeights?.[key] + return typeof saved === 'number' ? saved : fallback + }, + [liveHeights, savedLayout.sectionHeights] + ) + + const handleSectionHeightChange = useCallback( + (key, height, persist) => { + setLiveHeights((current) => ({ ...current, [key]: height })) + if (!persist) return + + const nextHeights = { + ...(savedLayout.sectionHeights || {}), + [key]: height + } + setDraftLayout((current) => { + if (!current) return current + return { ...current, sectionHeights: nextHeights } + }) + updatePageLayout({ + ...savedLayout, + sectionHeights: nextHeights + }) + }, + [savedLayout, updatePageLayout] + ) + + useEffect(() => { + if (!draggedItem) return undefined + + const edgePx = 72 + const maxSpeed = 28 + + const stopScroll = () => { + if (scrollRafRef.current) { + cancelAnimationFrame(scrollRafRef.current) + scrollRafRef.current = 0 + } + } + + const tick = () => { + scrollRafRef.current = 0 + const y = dragYRef.current + const scrollEl = scrollRef.current + if (y == null || !scrollEl) return + + const rect = scrollEl.getBoundingClientRect() + let dy = 0 + if (y >= rect.bottom - edgePx) { + dy = Math.min(maxSpeed, Math.ceil((y - (rect.bottom - edgePx)) / 2) + 6) + } else if (y <= rect.top + edgePx) { + dy = -Math.min(maxSpeed, Math.ceil((rect.top + edgePx - y) / 2) + 6) + } + + if (dy === 0) return + + const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight + const next = Math.max(0, Math.min(maxScroll, scrollEl.scrollTop + dy)) + if (next !== scrollEl.scrollTop) { + scrollEl.scrollTop = next + scrollRafRef.current = requestAnimationFrame(tick) + } + } + + const onDragOver = (event) => { + dragYRef.current = event.clientY + if (!scrollRafRef.current) { + scrollRafRef.current = requestAnimationFrame(tick) + } + } + + const onDragEnd = () => { + dragYRef.current = null + stopScroll() + } + + document.addEventListener('dragover', onDragOver) + document.addEventListener('dragend', onDragEnd) + document.addEventListener('drop', onDragEnd) + + return () => { + document.removeEventListener('dragover', onDragOver) + document.removeEventListener('dragend', onDragEnd) + document.removeEventListener('drop', onDragEnd) + onDragEnd() + } + }, [draggedItem]) + + const cancelEditing = useCallback(() => { + setDraftLayout(null) + setIsLayoutEditing(false) + clearDragState() + }, [clearDragState]) + + const handleUpdate = useCallback(() => { + if (draftLayout) { + updatePageLayout(draftLayout) + } + setDraftLayout(null) + setIsLayoutEditing(false) + clearDragState() + }, [draftLayout, updatePageLayout, clearDragState]) + + const reorderStats = useCallback( + (objectType, newOrder) => { + if (!isLayoutEditing) return + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + return { + ...base, + statsOrder: { + ...base.statsOrder, + [objectType]: newOrder + } + } + }) + }, + [isLayoutEditing, savedLayout] + ) + + const updateStatsVisibility = useCallback( + (objectType, key, value) => { + if (!isLayoutEditing) return + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + return { + ...base, + statsVisibility: { + ...base.statsVisibility, + [objectType]: { + ...(base.statsVisibility?.[objectType] || {}), + [key]: value + } + } + } + }) + }, + [isLayoutEditing, savedLayout] + ) + + const layoutContextValue = useMemo( + () => ({ + isLayoutEditing, + getStatsOrder: (objectType, defaultOrder = []) => + mergeOrder(displayLayout.statsOrder?.[objectType] || [], defaultOrder), + reorderStats, + getStatsVisibility: (objectType, defaultNames = []) => { + const saved = displayLayout.statsVisibility?.[objectType] || {} + return Object.fromEntries( + defaultNames.map((name) => [name, saved[name] !== false]) + ) + }, + updateStatsVisibility + }), + [isLayoutEditing, displayLayout, reorderStats, updateStatsVisibility] + ) + + const applyMove = useCallback( + (dest) => { + if (!draggedItem || !dest) return + if (draggedItem.kind === 'flexGroup' && dest.parent !== 'root') return + + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + const originalSourceList = + draggedItem.sourceParent === 'root' + ? base.sectionOrder + : base.flexColumns?.[draggedItem.sourceParent] || [] + const sourceIndex = originalSourceList.indexOf(draggedItem.key) + if (sourceIndex === -1) return current || base + + const originalDestList = + dest.parent === 'root' + ? base.sectionOrder + : base.flexColumns?.[dest.parent] || [] + + let destIndex = dest.insertBefore ? dest.index : dest.index + 1 + if ( + draggedItem.sourceParent === dest.parent && + sourceIndex < destIndex + ) { + destIndex -= 1 + } + destIndex = Math.max(0, Math.min(destIndex, originalDestList.length)) + if ( + draggedItem.sourceParent === dest.parent && + destIndex === sourceIndex + ) { + return current || base + } + + const sectionOrder = [...(base.sectionOrder || [])] + const nextFlexColumns = Object.fromEntries( + Object.entries(base.flexColumns || {}).map(([id, items]) => [ + id, + [...items] + ]) + ) + + if (draggedItem.sourceParent === 'root') { + const from = sectionOrder.indexOf(draggedItem.key) + if (from !== -1) sectionOrder.splice(from, 1) + } else { + nextFlexColumns[draggedItem.sourceParent] = ( + nextFlexColumns[draggedItem.sourceParent] || [] + ).filter((key) => key !== draggedItem.key) + } + + if (dest.parent === 'root') { + const insertAt = Math.max(0, Math.min(destIndex, sectionOrder.length)) + sectionOrder.splice(insertAt, 0, draggedItem.key) + } else { + const destList = [...(nextFlexColumns[dest.parent] || [])] + const insertAt = Math.max(0, Math.min(destIndex, destList.length)) + destList.splice(insertAt, 0, draggedItem.key) + nextFlexColumns[dest.parent] = destList + } + + return { + ...base, + sectionOrder, + flexColumns: nextFlexColumns, + flexColumnProportions: Object.fromEntries( + Object.entries(nextFlexColumns).map(([id, items]) => [ + id, + normalizeColumnProportions( + items.length, + base.flexColumnProportions?.[id] + ) + ]) + ) + } + }) + }, + [draggedItem, savedLayout] + ) + + const handleDragStart = useCallback( + (e, item) => { + if (!isLayoutEditing) return + e.stopPropagation() + const selector = + item.sourceParent === 'root' + ? '.overview-section-wrapper' + : '.overview-flex-columns-item' + const wrapper = e.currentTarget.closest(selector) + if (wrapper) { + const rect = wrapper.getBoundingClientRect() + e.dataTransfer.setDragImage( + wrapper, + e.clientX - rect.left, + e.clientY - rect.top + ) + } + setDraggedItem(item) + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/html', '') + }, + [isLayoutEditing] + ) + + const setRootDropFromGroupEdge = useCallback( + (e, groupId) => { + const wrapper = e.currentTarget.closest('.overview-section-wrapper') + if (!wrapper) return false + const edge = getVerticalEdgeInsert(wrapper, e.clientY) + if (!edge) return false + const rootIndex = orderedSectionKeys.indexOf(groupId) + if (rootIndex === -1) return false + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = 'move' + setDropTarget({ + parent: 'root', + index: rootIndex, + insertBefore: edge.insertBefore + }) + return true + }, + [orderedSectionKeys] + ) + + const handleRootDragOver = useCallback( + (e, index, key) => { + if (!isLayoutEditing || !draggedItem) return + const isGroup = Object.prototype.hasOwnProperty.call(flexColumns, key) + if (draggedItem.kind === 'section' && isGroup) { + const edge = getVerticalEdgeInsert(e.currentTarget, e.clientY) + if (edge) { + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + setDropTarget({ + parent: 'root', + index, + insertBefore: edge.insertBefore + }) + } + return + } + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + const rect = e.currentTarget.getBoundingClientRect() + setDropTarget({ + parent: 'root', + index, + insertBefore: e.clientY < rect.top + rect.height / 2 + }) + }, + [isLayoutEditing, draggedItem, flexColumns] + ) + + const handleRootListDragOver = useCallback( + (e) => { + if (!isLayoutEditing || !draggedItem) return + if ( + e.target instanceof Element && + e.target.closest('.overview-section-wrapper') + ) { + return + } + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + const wrappers = [...e.currentTarget.children].filter((el) => + el.classList.contains('overview-section-wrapper') + ) + if (wrappers.length === 0) { + setDropTarget({ parent: 'root', index: 0, insertBefore: true }) + return + } + const y = e.clientY + for (let i = 0; i < wrappers.length; i++) { + const rect = wrappers[i].getBoundingClientRect() + if (y < rect.top + rect.height / 2) { + const key = wrappers[i].dataset.overviewKey + const nextIndex = orderedSectionKeys.indexOf(key) + setDropTarget({ + parent: 'root', + index: nextIndex === -1 ? i : nextIndex, + insertBefore: true + }) + return + } + } + const lastKey = wrappers[wrappers.length - 1].dataset.overviewKey + const lastIndex = orderedSectionKeys.indexOf(lastKey) + setDropTarget({ + parent: 'root', + index: lastIndex === -1 ? wrappers.length - 1 : lastIndex, + insertBefore: false + }) + }, + [isLayoutEditing, draggedItem, orderedSectionKeys] + ) + + const handleGroupItemDragOver = useCallback( + (e, groupId, index) => { + if (!isLayoutEditing || !draggedItem || draggedItem.kind !== 'section') { + return + } + if (setRootDropFromGroupEdge(e, groupId)) return + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = 'move' + setDropTarget({ + parent: groupId, + index, + insertBefore: true + }) + }, + [isLayoutEditing, draggedItem, setRootDropFromGroupEdge] + ) + + const handleGroupContainerDragOver = useCallback( + (e, groupId) => { + if (!isLayoutEditing || !draggedItem || draggedItem.kind !== 'section') { + return + } + if (setRootDropFromGroupEdge(e, groupId)) return + if ( + e.target instanceof Element && + e.target.closest('.overview-flex-columns-item') + ) { + return + } + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = 'move' + const items = flexColumns[groupId] || [] + setDropTarget({ + parent: groupId, + index: items.length, + insertBefore: true + }) + }, + [isLayoutEditing, draggedItem, flexColumns, setRootDropFromGroupEdge] + ) + + const handleDragLeave = useCallback((e) => { + if ( + e.relatedTarget instanceof Node && + e.currentTarget.contains(e.relatedTarget) + ) { + return + } + setDropTarget(null) + }, []) + + const handleDrop = useCallback( + (e, fallbackParent, fallbackIndex) => { + if (!isLayoutEditing || !draggedItem) return + e.preventDefault() + e.stopPropagation() + const dest = dropTarget || { + parent: fallbackParent, + index: fallbackIndex, + insertBefore: true + } + applyMove(dest) + clearDragState() + }, + [isLayoutEditing, draggedItem, dropTarget, applyMove, clearDragState] + ) + + const handleDragEnd = useCallback(() => { + clearDragState() + }, [clearDragState]) + + const addColumns = useCallback(() => { + setIsLayoutEditing(true) + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + const id = createFlexColumnsId() + return { + ...base, + sectionOrder: [...(base.sectionOrder || []), id], + flexColumns: { + ...(base.flexColumns || {}), + [id]: [] + } + } + }) + }, [savedLayout]) + + const removeGroup = useCallback( + (groupId) => { + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + const children = base.flexColumns?.[groupId] || [] + const sectionOrder = [...(base.sectionOrder || [])] + const at = sectionOrder.indexOf(groupId) + if (at === -1) return current || base + sectionOrder.splice(at, 1, ...children) + const nextFlexColumns = { ...(base.flexColumns || {}) } + delete nextFlexColumns[groupId] + const nextFlexColumnProportions = { + ...(base.flexColumnProportions || {}) + } + delete nextFlexColumnProportions[groupId] + return { + ...base, + sectionOrder, + flexColumns: nextFlexColumns, + flexColumnProportions: nextFlexColumnProportions + } + }) + }, + [savedLayout] + ) + + const updateFlexColumnProportions = useCallback( + (groupId, nextProportions) => { + if (!isLayoutEditing) return + setDraftLayout((current) => { + const base = current || copyLayout(savedLayout) + return { + ...base, + flexColumnProportions: { + ...(base.flexColumnProportions || {}), + [groupId]: nextProportions + } + } + }) + }, + [isLayoutEditing, savedLayout] + ) + + const renderDragHandle = (item) => ( + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onDragStart={(e) => handleDragStart(e, item)} + onDragEnd={handleDragEnd} + > + + + ) + + const isItemVisible = (key) => isLayoutEditing || collapseState[key] !== false + + const wouldMove = (item, dest) => { + if (!item || !dest) return false + if (item.key === orderedSectionKeys[dest.index] && dest.parent === 'root') { + const insertIndex = dest.insertBefore ? dest.index : dest.index + 1 + let targetIndex = insertIndex + if (item.sourceParent === 'root' && item.sourceIndex < targetIndex) { + targetIndex -= 1 + } + return targetIndex !== item.sourceIndex + } + if (item.sourceParent === dest.parent) { + const insertIndex = dest.insertBefore ? dest.index : dest.index + 1 + let targetIndex = insertIndex + if (item.sourceIndex < targetIndex) { + targetIndex -= 1 + } + return targetIndex !== item.sourceIndex + } + return true + } + + const getRootWrapperClass = (index, key) => { + const isDragging = + draggedItem?.sourceParent === 'root' && draggedItem.key === key + let insertClass = '' + if ( + dropTarget?.parent === 'root' && + dropTarget.index === index && + draggedItem && + draggedItem.key !== key && + wouldMove(draggedItem, dropTarget) + ) { + insertClass = dropTarget.insertBefore + ? 'overview-insert-before' + : 'overview-insert-after' + } + return `overview-section-wrapper ${isDragging ? 'overview-dragging' : ''} ${insertClass}` + } + + const getGroupItemClass = (groupId, index, key) => { + const isDragging = + draggedItem?.sourceParent === groupId && draggedItem.key === key + const isDragOver = + dropTarget?.parent === groupId && + dropTarget.index === index && + draggedItem?.kind === 'section' && + draggedItem.key !== key + return `overview-flex-columns-item ${isDragging ? 'overview-dragging' : ''} ${ + isDragOver ? 'overview-drag-over' : '' + }` + } + + const renderCollapse = (section, dragHandle = null) => { + const isStats = section.content?.type === StatsDisplay + const statsObjectType = isStats ? section.content.props?.objectType : null + const isHistory = section.content?.type === ModelHistoryDisplay + const isTable = section.content?.type === ObjectTable + const isLazyContent = isStats || isHistory || isTable + const resizable = isHistory || isTable + const defaultHeight = isHistory + ? DEFAULT_HISTORY_HEIGHT + : DEFAULT_TABLE_HEIGHT + const contentHeight = getSectionHeight(section.key, defaultHeight) + const extra = + isLayoutEditing && statsObjectType ? ( + + ) : null + + let content = section.content + if (resizable && content) { + content = isHistory + ? cloneElement(content, { height: contentHeight }) + : cloneElement(content, { scrollHeight: `${contentHeight}px` }) + } + + if (isLazyContent && content) { + content = ( + + {content} + + ) + } + + return ( + updateCollapseState(section.key, isActive)} + className={section.className || ''} + collapseKey={section.key} + dragHandle={dragHandle} + extra={extra} + resizable={resizable} + contentHeight={contentHeight} + onContentHeightChange={ + resizable + ? (height, persist) => + handleSectionHeightChange(section.key, height, persist) + : undefined + } + > + {content} + + ) + } + + const actionMenuItems = useMemo(() => { + const layoutItems = [ + { + key: 'add-columns', + label: 'Add columns', + onClick: addColumns + } + ] + const extraItems = actionMenu?.items || [] + if (extraItems.length === 0) { + return layoutItems + } + return [...layoutItems, { type: 'divider' }, ...extraItems] + }, [actionMenu, addColumns]) + + return ( + + + + + + + + + + + + + handleDrop(e, 'root', 0)} + > + {orderedSectionKeys.map((key, index) => { + if (isGroupKey(key)) { + const groupItems = flexColumns[key] || [] + const itemsToRender = isLayoutEditing + ? groupItems + : groupItems.filter((childKey) => isItemVisible(childKey)) + if (itemsToRender.length === 0 && !isLayoutEditing) { + return null + } + const groupProportions = normalizeColumnProportions( + groupItems.length, + displayLayout.flexColumnProportions?.[key] + ) + const renderProportions = itemsToRender.map((childKey) => { + const itemIndex = groupItems.indexOf(childKey) + return itemIndex === -1 + ? undefined + : groupProportions[itemIndex] + }) + + return ( +
handleRootDragOver(e, index, key)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, 'root', index)} + className={getRootWrapperClass(index, key)} + > + = groupItems.length + } + dragHandle={ + isLayoutEditing + ? renderDragHandle({ + kind: 'flexGroup', + key, + sourceParent: 'root', + sourceIndex: index + }) + : null + } + onRemove={ + isLayoutEditing ? () => removeGroup(key) : undefined + } + onDragOver={(e) => handleGroupContainerDragOver(e, key)} + onDrop={(e) => handleDrop(e, key, groupItems.length)} + onDragLeave={handleDragLeave} + proportions={renderProportions} + onProportionsChange={ + isLayoutEditing + ? (next) => updateFlexColumnProportions(key, next) + : undefined + } + > + {itemsToRender.map((childKey, childIndex) => { + const section = sectionsByKey[childKey] + if (!section) return null + return ( +
+ handleGroupItemDragOver(e, key, childIndex) + } + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, key, childIndex)} + className={getGroupItemClass( + key, + childIndex, + childKey + )} + > + {renderCollapse( + section, + isLayoutEditing + ? renderDragHandle({ + kind: 'section', + key: childKey, + sourceParent: key, + sourceIndex: childIndex + }) + : null + )} +
+ ) + })} +
+
+ ) + } + + const section = sectionsByKey[key] + if (!section) return null + if (!isItemVisible(section.key)) { + return null + } + + return ( +
handleRootDragOver(e, index, key)} + onDragLeave={handleDragLeave} + onDrop={(e) => handleDrop(e, 'root', index)} + className={getRootWrapperClass(index, key)} + > + {renderCollapse( + section, + isLayoutEditing + ? renderDragHandle({ + kind: 'section', + key, + sourceParent: 'root', + sourceIndex: index + }) + : null + )} +
+ ) + })} +
+
+
+
+ ) +} + +const sectionShape = PropTypes.shape({ + key: PropTypes.string.isRequired, + title: PropTypes.string, + icon: PropTypes.node, + className: PropTypes.string, + content: PropTypes.node, + type: PropTypes.oneOf(['row']), + columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)) +}) + +DashboardOverviewPage.propTypes = { + pageName: PropTypes.string.isRequired, + collapseDefaults: PropTypes.object, + viewItems: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ), + actionMenu: PropTypes.object, + onOpenActionsModal: PropTypes.func, + sections: PropTypes.arrayOf(sectionShape) +} + +export default DashboardOverviewPage diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx index 072e3c6f..5d564afe 100644 --- a/src/components/Dashboard/common/DashboardSidebar.jsx +++ b/src/components/Dashboard/common/DashboardSidebar.jsx @@ -1,22 +1,52 @@ -import { useState, useEffect, useContext } from 'react' -import { Layout, Menu, Flex, Button } from 'antd' -import { CaretDownFilled } from '@ant-design/icons' +import { + cloneElement, + isValidElement, + useState, + useEffect, + useContext +} from 'react' +import { Layout, Menu, Flex, Button, Divider } from 'antd' import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon' import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon' import { useMediaQuery } from 'react-responsive' import { useNavigate } from 'react-router-dom' import PropTypes from 'prop-types' import { ElectronContext } from '../context/ElectronContext' +import { AuthContext } from '../context/AuthContext' import { getSidebarIconNode } from '../../Icons/sidebarIconMap' import SimpleBar from 'simplebar-react' import { useThemeContext } from '../context/ThemeContext' +import { filterSidebarItemsByListPermission } from '../../../database/Sidebars' +import Tooltip from './Tooltip' +import KeyboardShortcut from './KeyboardShortcut' const { Sider } = Layout +const CollapsedItemIcon = ({ className, icon, title }) => { + const iconNode = isValidElement(icon) + ? cloneElement(icon, { + className: [icon.props.className, className].filter(Boolean).join(' ') + }) + : icon + + return ( + + {iconNode ?? } + + ) +} + +CollapsedItemIcon.propTypes = { + className: PropTypes.string, + icon: PropTypes.node, + title: PropTypes.node +} + const DashboardSidebar = ({ items = [], selectedKey = '', onCollapse, collapsed: collapsedProp, + collapsedWidth: collapsedWidthProp, key = 'DashboardSidebar_collapseState' }) => { const [collapsed, setCollapsed] = useState(() => { @@ -28,6 +58,9 @@ const DashboardSidebar = ({ const navigate = useNavigate() const { isDarkMode } = useThemeContext() const { isElectron } = useContext(ElectronContext) + const collapsedWidth = collapsedWidthProp ?? (isElectron ? 55 : 80) + const { userProfile } = useContext(AuthContext) + const allowedItems = filterSidebarItemsByListPermission(items, userProfile) useEffect(() => { if (typeof collapsedProp === 'boolean') { @@ -42,15 +75,21 @@ const DashboardSidebar = ({ } // Recursive function to map items and their children - const mapItemsRecursively = (items) => { + const mapItemsRecursively = (items, showCollapsedTooltip = true) => { return items.map((item) => { if (item?.type === 'divider') { return item } + const icon = item.icon || getSidebarIconNode(item.iconKey) const mappedItem = { key: item.key, - icon: item.icon || getSidebarIconNode(item.iconKey), + icon: + collapsed && !isMobile && showCollapsedTooltip && !item.children ? ( + + ) : ( + icon + ), label: item.label } @@ -61,7 +100,7 @@ const DashboardSidebar = ({ // Recursively map children if they exist if (item?.children && Array.isArray(item.children)) { - mappedItem.children = mapItemsRecursively(item.children) + mappedItem.children = mapItemsRecursively(item.children, false) } return mappedItem @@ -69,27 +108,22 @@ const DashboardSidebar = ({ } // Map items recursively - const _items = mapItemsRecursively(items) + const _items = mapItemsRecursively(allowedItems) if (isMobile) { - return ( - } />} - /> - ) + return null } return ( - - , - - ]} - > - - Are you sure you want to delete this {model.label.toLowerCase()}? - - - ) -} - -DeleteObjectModal.propTypes = { - open: PropTypes.bool.isRequired, - onOk: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - loading: PropTypes.bool, - objectType: PropTypes.string.isRequired -} - -export default DeleteObjectModal diff --git a/src/components/Dashboard/common/DiffLabel.jsx b/src/components/Dashboard/common/DiffLabel.jsx new file mode 100644 index 00000000..7573fbdc --- /dev/null +++ b/src/components/Dashboard/common/DiffLabel.jsx @@ -0,0 +1,116 @@ +import { useMemo, useState } from 'react' +import PropTypes from 'prop-types' +import { Button, Modal, Typography, theme, Flex } from 'antd' +import { presentableDiff } from '@codemirror/merge' +import CodeDiffViewer from './CodeDiffViewer' +import { toEditorCode } from './codeBlockEditorUtils' + +const { Link, Text } = Typography + +const SQUARE_COUNT = 10 + +const countLines = (str) => { + if (!str) return 0 + const parts = str.split('\n') + if (parts[parts.length - 1] === '') parts.pop() + return Math.max(parts.length, 1) +} + +export function getCodeDiffStats(oldCode, newCode, language = 'javascript') { + const oldText = toEditorCode(oldCode, language) + const newText = toEditorCode(newCode, language) + const changes = presentableDiff(oldText, newText) + let additions = 0 + let deletions = 0 + + for (const change of changes) { + const deleted = oldText.slice(change.fromA, change.toA) + const added = newText.slice(change.fromB, change.toB) + if (deleted) deletions += countLines(deleted) + if (added) additions += countLines(added) + } + + return { additions, deletions } +} + +export default function DiffLabel({ + oldCode = '', + newCode = '', + language = 'javascript' +}) { + const { token } = theme.useToken() + const [open, setOpen] = useState(false) + const [viewerReady, setViewerReady] = useState(false) + const { additions, deletions } = useMemo( + () => getCodeDiffStats(oldCode, newCode, language), + [oldCode, newCode, language] + ) + + const greenCount = Math.round( + (additions / (additions + deletions)) * SQUARE_COUNT + ) + + return ( + <> + { + setOpen(true) + }} + > + + +{additions} + + {Array.from({ length: SQUARE_COUNT }).map((_, i) => ( +
+ ))} + + -{deletions} + + + { + setOpen(false) + }} + footer={ + + } + > + {viewerReady ? ( + + ) : null} + + + ) +} + +DiffLabel.propTypes = { + oldCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + newCode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + language: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string) + ]) +} diff --git a/src/components/Dashboard/common/DocumentPrintButton.jsx b/src/components/Dashboard/common/DocumentPrintButton.jsx index d26cb4a4..cc718433 100644 --- a/src/components/Dashboard/common/DocumentPrintButton.jsx +++ b/src/components/Dashboard/common/DocumentPrintButton.jsx @@ -7,86 +7,297 @@ import { useMemo, useRef } from 'react' -// import { getModelByName } from '../../../database/ObjectModels' import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon' -import { Button, Dropdown, Modal } from 'antd' +import { Button, Dropdown, Modal, message } from 'antd' import { ApiServerContext } from '../context/ApiServerContext' +import { TableStateContext } from '../context/TableStateContext' import DocumentTemplateIcon from '../../Icons/DocumentTemplateIcon' import NewDocumentJob from '../Management/DocumentJobs/NewDocumentJob' import { AuthContext } from '../context/AuthContext' +import LoadingPleaseWaitModal from './LoadingPleaseWaitModal' +import { fetchAllObjectPages } from '../utils/fetchAllObjectPages' +import { resolveObjectsList } from '../utils/templateObjectData' const DocumentPrintButton = ({ type, objectData, + objects, + listMode = false, disabled = false, ...buttonProps }) => { - const { fetchObjects } = useContext(ApiServerContext) + const { fetchObjects, connected, subscribeToObjectUpdates, subscribeToObjectTypeUpdates } = + useContext(ApiServerContext) + const tableState = useContext(TableStateContext) const fetchObjectsRef = useRef(fetchObjects) fetchObjectsRef.current = fetchObjects const [documentTemplates, setDocumentTemplates] = useState([]) const [currentDocumentTemplate, setCurrentDocumentTemplate] = useState(null) const [loading, setLoading] = useState(false) + const [listLoading, setListLoading] = useState(false) const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false) + const [selectedObjects, setSelectedObjects] = useState(() => + resolveObjectsList(objects, objectData) + ) + + const subscribedIdsRef = useRef([]) + const unsubscribesRef = useRef([]) + const subscribeToObjectTypeUpdatesRef = useRef(null) + const updateEventHandlerRef = useRef() const { token } = useContext(AuthContext) - // Get the model by name - //const model = getModelByName(type) + const resolvedObjects = useMemo( + () => resolveObjectsList(objects, objectData), + [objects, objectData] + ) - const loadDocumentTemplates = useCallback(async () => { - if (!type || token == null) return + const subscriptionFilter = useMemo( + () => ({ + objectType: type, + global: false, + active: true + }), + [type] + ) - setLoading(true) - try { - const result = await fetchObjectsRef.current('documentTemplate', { - filter: { - objectType: type, - global: false, - active: true - }, - limit: 100 // Get more templates to show in dropdown - }) + const templateMatchesFilter = useCallback( + (template) => + template?.objectType === type && + template?.global === false && + template?.active === true, + [type] + ) - if (result && result.data) { - setDocumentTemplates(result.data) + const loadDocumentTemplates = useCallback( + async (silent = false) => { + if (!type || token == null) return + + if (!silent) { + setLoading(true) } - } catch (error) { - console.error('Error fetching document templates:', error) - } finally { - setLoading(false) - } - }, [type, token]) + try { + const result = await fetchObjectsRef.current('documentTemplate', { + filter: subscriptionFilter, + limit: 100 + }) + + if (result && result.data) { + setDocumentTemplates(result.data) + } + } catch (error) { + console.error('Error fetching document templates:', error) + } finally { + if (!silent) { + setLoading(false) + } + } + }, + [type, token, subscriptionFilter] + ) + + const loadDocumentTemplatesRef = useRef(loadDocumentTemplates) + loadDocumentTemplatesRef.current = loadDocumentTemplates - // Stable key from objectData._id (excludes _isEditing so toggling edit mode doesn't trigger template reload) const objectKey = useMemo(() => { - if (!objectData) return null - return objectData._id ?? objectData.id ?? null - }, [objectData]) + if (listMode) return 'list' + const first = resolvedObjects[0] + if (!first) return null + return first._id ?? first.id ?? null + }, [listMode, resolvedObjects]) - // Fetch document templates when component mounts or objectData, type or token changes useEffect(() => { loadDocumentTemplates() }, [objectKey, token, type, loadDocumentTemplates]) - // Handle template selection - const handleTemplateSelect = (template) => { - setCurrentDocumentTemplate(template) - setNewDocumentJobOpen(true) - // TODO: Implement the actual printing logic here - // This could open a print dialog, navigate to a print page, etc. - } + const updateEventHandler = useCallback( + (id, updatedData) => { + setDocumentTemplates((prev) => { + const existing = prev.find((template) => template._id === id) + const merged = existing + ? { ...existing, ...updatedData } + : { _id: id, ...updatedData } - // Create dropdown menu items - const menuItems = documentTemplates.map((template) => ({ - key: template._id, - label: template.name, - icon: , - onClick: () => handleTemplateSelect(template) - })) + if (!templateMatchesFilter(merged)) { + return prev.filter((template) => template._id !== id) + } + + if (existing) { + return prev.map((template) => + template._id === id ? merged : template + ) + } + + return [...prev, merged] + }) + }, + [templateMatchesFilter] + ) + + updateEventHandlerRef.current = updateEventHandler + + useEffect(() => { + if (connected !== true) return + + const unsubscribe = subscribeToObjectTypeUpdates( + 'documentTemplate', + subscriptionFilter, + () => loadDocumentTemplatesRef.current(true) + ) + subscribeToObjectTypeUpdatesRef.current = unsubscribe + + return () => { + if (unsubscribe) unsubscribe() + if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) { + subscribeToObjectTypeUpdatesRef.current = null + } + } + }, [connected, subscriptionFilter, subscribeToObjectTypeUpdates]) + + useEffect(() => { + if (connected !== true) return + + const templateIds = documentTemplates + .map((template) => template._id) + .filter(Boolean) + + const newTemplateIds = templateIds.filter( + (id) => !subscribedIdsRef.current.includes(id) + ) + + newTemplateIds.forEach((itemId) => { + const unsubscribe = subscribeToObjectUpdates( + itemId?.toLowerCase(), + 'documentTemplate', + (updateData) => updateEventHandlerRef.current(itemId, updateData) + ) + subscribedIdsRef.current.push(itemId) + if (unsubscribe) { + unsubscribesRef.current.push(unsubscribe) + } + }) + + const templateIdsToUnsubscribe = subscribedIdsRef.current.filter( + (id) => !templateIds.includes(id) + ) + + templateIdsToUnsubscribe.forEach((itemId) => { + const index = subscribedIdsRef.current.indexOf(itemId) + if (index > -1) { + subscribedIdsRef.current.splice(index, 1) + const unsubscribe = unsubscribesRef.current[index] + if (unsubscribe) { + unsubscribe() + } + unsubscribesRef.current.splice(index, 1) + } + }) + }, [documentTemplates, connected, subscribeToObjectUpdates]) + + useEffect(() => { + return () => { + unsubscribesRef.current.forEach((unsubscribe) => { + if (unsubscribe) unsubscribe() + }) + unsubscribesRef.current = [] + subscribedIdsRef.current = [] + + if (subscribeToObjectTypeUpdatesRef.current) { + subscribeToObjectTypeUpdatesRef.current() + subscribeToObjectTypeUpdatesRef.current = null + } + } + }, []) + + const handleTemplateSelect = useCallback( + async (template) => { + setCurrentDocumentTemplate(template) + + if (listMode) { + setListLoading(true) + try { + const list = await fetchAllObjectPages({ + fetchObjects: fetchObjectsRef.current, + type, + filter: tableState?.getObjectListFilter?.(type) || {}, + sorter: tableState?.getObjectListSorter?.(type) || {} + }) + if (!list.length) { + message.warning('No objects in this list to print') + return + } + setSelectedObjects(list) + setNewDocumentJobOpen(true) + } catch (error) { + console.error('Error fetching objects for document job:', error) + message.error('Failed to load objects for printing') + } finally { + setListLoading(false) + } + return + } + + setSelectedObjects(resolvedObjects) + setNewDocumentJobOpen(true) + }, + [listMode, resolvedObjects, tableState, type] + ) + + const menuItems = useMemo(() => { + const templatesByTag = new Map() + const untaggedTemplates = [] + + const toTemplateMenuItem = (template) => ({ + key: template._id, + label: template.name, + icon: , + onClick: () => handleTemplateSelect(template) + }) + + for (const template of documentTemplates) { + const templateTags = Array.isArray(template.tags) + ? template.tags.filter(Boolean) + : [] + + if (templateTags.length === 0) { + untaggedTemplates.push(template) + continue + } + + for (const tag of templateTags) { + const tagged = templatesByTag.get(tag) ?? [] + tagged.push(template) + templatesByTag.set(tag, tagged) + } + } + + const taggedMenuItems = [...templatesByTag.keys()] + .sort((a, b) => a.localeCompare(b)) + .map((tag) => ({ + key: tag, + label: tag, + children: templatesByTag + .get(tag) + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(toTemplateMenuItem) + })) + + const rootUntaggedItems = untaggedTemplates + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(toTemplateMenuItem) + + return [...rootUntaggedItems, ...taggedMenuItems] + }, [documentTemplates, handleTemplateSelect]) + + const unavailable = + disabled || + loading || + listLoading || + (!listMode && resolvedObjects.length === 0) - // If no templates available, show disabled state if (documentTemplates.length === 0 && !loading) { return (
+ ) : null} +
+ {filesToRender.length > 0 ? ( + + ) : null} + + {filesToRender.map((file, index) => ( +
scrollToIndex(index)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + scrollToIndex(index) + } + }} + role='button' + tabIndex={0} + aria-label={`Show image ${index + 1}`} + aria-current={index === activeIndex ? 'true' : undefined} + > + + {editing ? ( + +
+ ))} + {editing && uploadSlot ? ( +
{uploadSlot}
+ ) : null} +
+
+ + ) +} + +FileGalleryList.propTypes = { + files: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + editing: PropTypes.bool, + onRemove: PropTypes.func, + uploadSlot: PropTypes.node +} + +export default FileGalleryList diff --git a/src/components/Dashboard/common/FileList.jsx b/src/components/Dashboard/common/FileList.jsx index d382d42f..136b5c29 100644 --- a/src/components/Dashboard/common/FileList.jsx +++ b/src/components/Dashboard/common/FileList.jsx @@ -1,4 +1,4 @@ -import { Card, Flex, Typography, Button, Tag, Divider } from 'antd' +import { Card, Flex, Button, Tag, Divider } from 'antd' import PropTypes from 'prop-types' import FileIcon from '../../Icons/FileIcon' import BinIcon from '../../Icons/BinIcon' @@ -7,12 +7,13 @@ import DownloadIcon from '../../Icons/DownloadIcon' import { useContext, useState } from 'react' import { ApiServerContext } from '../context/ApiServerContext' import FilePreview from './FilePreview' +import FileGalleryList from './FileGalleryList' import EyeSlashIcon from '../../Icons/EyeSlashIcon' import { getModelByName } from '../../../database/ObjectModels' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import { useNavigate } from 'react-router-dom' - -const { Text } = Typography +import ElipsisText from './ElipsisText' +import MissingPlaceholder from './MissingPlaceholder' const FileList = ({ files, @@ -25,6 +26,8 @@ const FileList = ({ defaultPreviewOpen = false, minimal = false, card = true, + gallery = false, + uploadSlot = null, maxWidth = '100%' }) => { const { fetchFileContent, flushFile } = useContext(ApiServerContext) @@ -38,10 +41,6 @@ const FileList = ({ ? !files || !Array.isArray(files) || files.length === 0 : !files - if (hasNoItems) { - return null - } - const handleRemove = (fileToRemove) => { flushFile(fileToRemove._id) if (multiple) { @@ -61,7 +60,41 @@ const FileList = ({ await fetchFileContent(file, true) } - const filesToRender = multiple ? files : [files] + const filesToRender = multiple + ? Array.isArray(files) + ? files + : [] + : files + ? [files] + : [] + + const rootStyle = { + minWidth: 0, + maxWidth, + ...(card + ? { width: '100%' } + : { width: 'fit-content', alignSelf: 'flex-start' }) + } + + if (gallery && !minimal) { + if (hasNoItems && !(gallery && (editing || uploadSlot))) { + return + } + return ( +
+ +
+ ) + } + + if (hasNoItems && !(gallery && (editing || uploadSlot))) { + return null + } const renderFileContent = (file) => ( - + {file.name || file.filename || 'Unknown file'} - + {file.extension && {file.extension}} {!minimal && ( @@ -152,14 +185,6 @@ const FileList = ({
) - const rootStyle = { - minWidth: 0, - maxWidth, - ...(card - ? { width: '100%' } - : { width: 'fit-content', alignSelf: 'flex-start' }) - } - return (
{filesToRender.map((file, index) => { @@ -206,6 +231,8 @@ FileList.propTypes = { showDownload: PropTypes.bool, defaultPreviewOpen: PropTypes.bool, card: PropTypes.bool, + gallery: PropTypes.bool, + uploadSlot: PropTypes.node, minimal: PropTypes.bool, maxWidth: PropTypes.string } diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx index bc33a53f..39ca131d 100644 --- a/src/components/Dashboard/common/FilePreview.jsx +++ b/src/components/Dashboard/common/FilePreview.jsx @@ -1,11 +1,24 @@ import PropTypes from 'prop-types' +import { Card, Flex } from 'antd' import { ApiServerContext } from '../context/ApiServerContext' -import { useCallback, useContext, useEffect, useState, memo } from 'react' -import LoadingPlaceholder from './LoadingPlaceholder' +import { + useCallback, + useContext, + useEffect, + useState, + memo, + useRef +} from 'react' import GCodePreview from './GCodePreview' import ThreeDPreview from './ThreeDPreview' +import PDFPreview from './PDFPreview' +import ProgressDisplay from './ProgressDisplay' import { AuthContext } from '../context/AuthContext' +const hasExplicitPreviewHeight = (height) => + typeof height === 'number' || + (typeof height === 'string' && height !== '100%' && height !== 'auto') + const FilePreview = ({ file, style = {} }) => { const { token } = useContext(AuthContext) const { fetchFileContent } = useContext(ApiServerContext) @@ -13,73 +26,165 @@ const FilePreview = ({ file, style = {} }) => { const [fileObjectUrl, setFileObjectUrl] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [downloadProgress, setDownloadProgress] = useState({ + progress: 0, + message: 'Loading file preview...' + }) + + const currentId = useRef(null) + + const extension = (file?.extension || '').toLowerCase() + const isGcode = ['.g', '.gcode'].includes(extension) + const is3DModel = ['.stl', '.3mf'].includes(extension) + const isImage = file?.type?.startsWith('image/') || false + const isPdf = + file?.type?.startsWith('application/pdf') || extension === '.pdf' const fetchPreview = useCallback(async () => { if (error != null) { return } + const requestId = file._id setLoading(true) - const objectUrl = await fetchFileContent(file, false) + setDownloadProgress({ progress: 0, message: 'Starting download...' }) + const objectUrl = await fetchFileContent(file, false, (progress) => { + if (currentId.current !== requestId) { + return + } + setDownloadProgress(progress) + }) + if (currentId.current !== requestId) { + return + } if (objectUrl == null) { setLoading(false) + setDownloadProgress(null) console.error('Failed to fetch file content', file) setError('Failed to fetch file content') return } setFileObjectUrl(objectUrl) + setDownloadProgress(null) setLoading(false) }, [file, fetchFileContent, error]) useEffect(() => { - if (file?.type && token != null) { + if (file._id !== currentId.current) { + setFileObjectUrl(null) + setError(null) + setLoading(true) + setDownloadProgress({ + progress: 0, + message: 'Loading file preview...' + }) + } + if (file?.type && token != null && file._id !== currentId.current) { + currentId.current = file._id fetchPreview() } }, [file._id, file?.type, fetchPreview, token]) - if (loading == true || !file?.type) { - return + const showProgressOverlay = downloadProgress != null + const wrapperHeight = hasExplicitPreviewHeight(style.height) + ? style.height + : isPdf + ? '72vh' + : style.height + + const renderPreview = () => { + if (isPdf) { + return ( + + ) + } + + if (loading == true || !file?.type) { + return null + } + + if (isGcode && fileObjectUrl) { + return ( + + ) + } + + if (is3DModel && fileObjectUrl) { + return ( + + ) + } + + if (isImage && fileObjectUrl) { + return + } + + return null } if (error != null) { return
{error}
} - const isGcode = - ['.g', '.gcode'].includes((file?.extension || '').toLowerCase()) || false - - const is3DModel = - ['.stl', '.3mf'].includes((file?.extension || '').toLowerCase()) || false - - const isImage = file?.type.startsWith('image/') - - if (isGcode && fileObjectUrl) { - return ( - - ) - } - - if (is3DModel && fileObjectUrl) { - return ( - - ) - } - - if (isImage && fileObjectUrl) { - return - } - return null + return ( +
+
+ {showProgressOverlay ? ( + + + + {downloadProgress.message || 'Downloading...'} + + + + ) : null} +
+ {renderPreview()} +
+ ) } FilePreview.propTypes = { @@ -91,6 +196,8 @@ FilePreview.propTypes = { const areEqual = (prevProps, nextProps) => { return ( prevProps.file?._id === nextProps.file?._id && + prevProps.file?.type === nextProps.file?.type && + prevProps.file?.size === nextProps.file?.size && JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style) ) } diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx index 6fb07185..94dc1c4b 100644 --- a/src/components/Dashboard/common/FileUpload.jsx +++ b/src/components/Dashboard/common/FileUpload.jsx @@ -28,6 +28,62 @@ const getFileIdentity = (value, multiple) => { return value?._id || value || '' } +const getMasterFilterAccept = (masterFilter) => + Array.isArray(masterFilter) && masterFilter.length > 0 + ? masterFilter.join(',') + : undefined + +const FileUploadDropzone = ({ onUploaded, multiple = true, masterFilter = [] }) => { + const { uploadFile } = useContext(ApiServerContext) + const [uploading, setUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const handleFileUpload = async (file) => { + try { + setUploading(true) + const uploadedFile = await uploadFile(file, {}, (progress) => { + setUploadProgress(progress) + }) + if (uploadedFile) { + onUploaded?.(uploadedFile) + } + } catch (error) { + console.error('File upload failed:', error) + } finally { + setUploading(false) + setUploadProgress(0) + } + return false + } + + return ( + + {uploading ? ( + uploadProgress > 0 && uploadProgress < 100 ? ( + {uploadProgress}% + ) : ( + + ) + ) : ( + + )} + + ) +} + +FileUploadDropzone.propTypes = { + onUploaded: PropTypes.func, + multiple: PropTypes.bool, + masterFilter: PropTypes.array +} + const FileUpload = ({ value, onChange, @@ -36,6 +92,7 @@ const FileUpload = ({ defaultPreviewOpen = false, showPreview = true, showInfo, + gallery = false, masterFilter = [] }) => { const { uploadFile } = useContext(ApiServerContext) @@ -135,6 +192,37 @@ const FileUpload = ({ ) } + if (gallery) { + return ( + { + if (multiple) { + const existing = Array.isArray(currentFiles) ? currentFiles : [] + updateCurrentFiles([...existing, uploadedFile]) + } else { + updateCurrentFiles(uploadedFile) + } + }} + /> + } + onChange={(updatedFiles) => { + updateCurrentFiles(updatedFiles) + }} + /> + ) + } + return ( {hasNoItems && uploading == false ? ( @@ -220,6 +308,7 @@ FileUpload.propTypes = { showInfo: PropTypes.bool, defaultPreviewOpen: PropTypes.bool, minimal: PropTypes.bool, + gallery: PropTypes.bool, masterFilter: PropTypes.array } diff --git a/src/components/Dashboard/common/FilterInput.jsx b/src/components/Dashboard/common/FilterInput.jsx new file mode 100644 index 00000000..4a3c2892 --- /dev/null +++ b/src/components/Dashboard/common/FilterInput.jsx @@ -0,0 +1,1245 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react' +import { ConfigProvider, Popover, Tag } from 'antd' +import { CloseCircleFilled } from '@ant-design/icons' +import PropTypes from 'prop-types' +import { createRoot } from 'react-dom/client' +import classNames from 'classnames' +import useCSSVarCls from 'antd/es/config-provider/hooks/useCSSVarCls' +import useSize from 'antd/es/config-provider/hooks/useSize' +import { useCompactItemContext } from 'antd/es/space/Compact' +import useStyle, { useSharedStyle } from 'antd/es/input/style' +import { useThemeContext } from '../context/ThemeContext' +import SimplePropertyFilter from './SimplePropertyFilter' +import QuickPropertyFilters from './QuickPropertyFilters' +import ScrollBox from './ScrollBox' +import ChevronRightIcon from '../../Icons/ChevronRightIcon' +import GreaterThanIcon from '../../Icons/GreaterThanIcon' +import GreaterThanOrEqualToIcon from '../../Icons/GreaterThanOrEqualToIcon' +import LessThanIcon from '../../Icons/LessThanIcon' +import LessThanOrEqualToIcon from '../../Icons/LessThanOrEqualToIcon' +import NotEqualIcon from '../../Icons/NotEqualIcon' +import EqualIcon from '../../Icons/EqualIcon' +import { Divider } from 'antd' + +const PROPERTY_FILTER_PANEL_MAX_HEIGHT = 220 + +// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">". +// Wildcards (* ?) are highlighted in place; @ stays as plain text — none are operands. +const WILDCARDS = new Set(['*', '?']) +const QUOTE = "'" + +const OPERANDS = [ + { + symbol: '..', + label: ( + + ), + color: 'purple' + }, + { + symbol: '<>', + label: , + color: 'magenta' + }, + { + symbol: '>=', + label: , + color: 'volcano' + }, + { + symbol: '<=', + label: , + color: 'volcano' + }, + { symbol: '|', label: 'OR', color: 'cyan' }, + { symbol: '&', label: 'AND', color: 'cyan' }, + { + symbol: '>', + label: ( + + ), + color: 'orange' + }, + { + symbol: '<', + label: ( + + ), + color: 'orange' + }, + { + symbol: '=', + label: , + color: 'green' + } +] + +const OPERAND_BY_SYMBOL = Object.fromEntries( + OPERANDS.map((operand) => [operand.symbol, operand]) +) + +const DATE_HIGHLIGHT_RE = /TODAY|[+-]?(?:\d+|C)[DdWwYyHhMm]/gi + +const expandDateHighlightTokens = (text) => { + const tokens = [] + const pattern = new RegExp(DATE_HIGHLIGHT_RE.source, DATE_HIGHLIGHT_RE.flags) + let lastIndex = 0 + let match = pattern.exec(text) + + while (match) { + if (match.index > lastIndex) { + tokens.push({ type: 'text', value: text.slice(lastIndex, match.index) }) + } + const raw = match[0] + if (/^TODAY$/i.test(raw)) { + tokens.push({ type: 'dateUnit', value: raw }) + } else { + tokens.push({ type: 'text', value: raw.slice(0, -1) }) + tokens.push({ type: 'dateUnit', value: raw.slice(-1) }) + } + lastIndex = match.index + raw.length + match = pattern.exec(text) + } + + if (lastIndex < text.length) { + tokens.push({ type: 'text', value: text.slice(lastIndex) }) + } + return tokens +} + +const tokenize = (raw = '') => { + const value = String(raw) + const tokens = [] + let text = '' + let i = 0 + + const pushText = (value) => { + if (!value) return + tokens.push(...expandDateHighlightTokens(value)) + } + + while (i < value.length) { + const match = OPERANDS.find((operand) => + value.startsWith(operand.symbol, i) + ) + if (match) { + pushText(text) + text = '' + tokens.push({ type: 'operand', value: match.symbol }) + i += match.symbol.length + } else if (WILDCARDS.has(value[i])) { + pushText(text) + text = '' + tokens.push({ type: 'wildcard', value: value[i] }) + i += 1 + } else if (value[i] === QUOTE) { + pushText(text) + text = '' + tokens.push({ type: 'quote', value: QUOTE }) + i += 1 + } else { + text += value[i] + i += 1 + } + } + + pushText(text) + return tokens +} + +const renderWildcardNode = (char) => { + const span = document.createElement('span') + span.setAttribute('data-wildcard', char) + span.className = 'filter-input-wildcard' + span.textContent = char + return span +} + +const renderDateUnitNode = (value) => { + const span = document.createElement('span') + span.setAttribute('data-date-unit', value) + span.className = 'filter-input-date-unit' + span.textContent = value + return span +} + +const renderQuoteNode = () => { + const span = document.createElement('span') + span.setAttribute('data-quote', QUOTE) + span.className = 'filter-input-quote' + span.textContent = QUOTE + return span +} + +const normalize = (raw = '') => + tokenize(raw) + .map((token) => token.value) + .join('') + +const nodeLength = (node) => { + if (!node) return 0 + if (node.nodeType === Node.TEXT_NODE) return node.textContent?.length ?? 0 + const operand = node.getAttribute?.('data-operand') + if (operand != null) return operand.length + return Array.from(node.childNodes).reduce( + (sum, child) => sum + nodeLength(child), + 0 + ) +} + +const serializeNode = (root) => { + if (!root) return '' + let result = '' + for (const node of root.childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + result += node.textContent ?? '' + } else if (node.getAttribute?.('data-operand') != null) { + result += node.getAttribute('data-operand') + } else { + result += serializeNode(node) + } + } + return result +} + +/** Character offset in the serialized filter string for a DOM point. */ +const getOffsetAt = (root, targetNode, targetOffset, chipSnap = 'end') => { + if (!root || !targetNode || !root.contains(targetNode)) { + return serializeNode(root).length + } + + let offset = 0 + + const visit = (node) => { + if (node === targetNode) { + if (node.nodeType === Node.TEXT_NODE) { + offset += targetOffset + } else if (node.getAttribute?.('data-operand') != null) { + offset += + targetOffset <= 0 ? 0 : node.getAttribute('data-operand').length + } else { + for (let i = 0; i < targetOffset; i += 1) { + offset += nodeLength(node.childNodes[i]) + } + } + return true + } + + if (node.nodeType === Node.TEXT_NODE) { + offset += node.textContent?.length ?? 0 + return false + } + + if (node.getAttribute?.('data-operand') != null) { + if (node.contains(targetNode)) { + // Chips are atomic: snap range starts to the chip start, ends to the chip end. + if (chipSnap !== 'start') { + offset += node.getAttribute('data-operand').length + } + return true + } + offset += node.getAttribute('data-operand').length + return false + } + + for (const child of node.childNodes) { + if (visit(child)) return true + } + return false + } + + visit(root) + return offset +} + +/** Character offset of the selection focus (caret). */ +const getCaretOffset = (root) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !root.contains(selection.focusNode)) { + return serializeNode(root).length + } + return getOffsetAt(root, selection.focusNode, selection.focusOffset, 'end') +} + +/** Serialized filter text for the current selection (operand symbols, not labels). */ +const getSelectedSerializedText = (root) => { + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return '' + + const range = selection.getRangeAt(0) + if ( + !root.contains(range.startContainer) || + !root.contains(range.endContainer) + ) { + return null + } + + const start = getOffsetAt( + root, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(root, range.endContainer, range.endOffset, 'end') + return serializeNode(root).slice(Math.min(start, end), Math.max(start, end)) +} + +/** Keep the caret / selection focus visible inside a horizontally clipped editor. */ +const scrollPointIntoView = (root, clientX) => { + if (!root || clientX == null || Number.isNaN(clientX)) return + const rect = root.getBoundingClientRect() + const pad = 8 + if (clientX > rect.right - pad) { + root.scrollLeft += clientX - (rect.right - pad) + } else if (clientX < rect.left + pad) { + root.scrollLeft -= rect.left + pad - clientX + } +} + +const scrollSelectionIntoView = (root) => { + if (!root) return + const selection = window.getSelection() + if (!selection?.rangeCount || !root.contains(selection.focusNode)) return + + try { + const range = document.createRange() + range.setStart(selection.focusNode, selection.focusOffset) + range.collapse(true) + const rects = range.getClientRects() + const rect = rects[rects.length - 1] ?? range.getBoundingClientRect() + if (rect && (rect.width > 0 || rect.height > 0)) { + scrollPointIntoView(root, rect.left) + return + } + } catch { + // Fall through to caret-at-end heuristic. + } + + // Empty / collapsed-at-end selections often have a zero rect — scroll to end. + if (getCaretOffset(root) >= serializeNode(root).length) { + root.scrollLeft = root.scrollWidth + } +} + +const caretRangeFromPoint = (clientX, clientY) => { + if (document.caretRangeFromPoint) { + return document.caretRangeFromPoint(clientX, clientY) + } + if (document.caretPositionFromPoint) { + const pos = document.caretPositionFromPoint(clientX, clientY) + if (!pos) return null + const range = document.createRange() + range.setStart(pos.offsetNode, pos.offset) + range.collapse(true) + return range + } + return null +} + +/** Extend the live selection to the DOM point under the pointer (used while edge-scrolling). */ +const extendSelectionToPoint = (root, clientX, clientY) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !root) return + const caret = caretRangeFromPoint(clientX, clientY) + if (!caret || !root.contains(caret.startContainer)) return + try { + selection.setBaseAndExtent( + selection.anchorNode, + selection.anchorOffset, + caret.startContainer, + caret.startOffset + ) + } catch { + // Ignore invalid selection boundaries (e.g. inside non-editable chips). + } +} + +const setCaretOffset = (root, targetOffset) => { + if (!root) return + const selection = window.getSelection() + if (!selection) return + + let remaining = Math.max(0, targetOffset) + + const place = (node, offset) => { + const range = document.createRange() + range.setStart(node, offset) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + scrollSelectionIntoView(root) + } + + const walk = (container) => { + for (let i = 0; i < container.childNodes.length; i += 1) { + const child = container.childNodes[i] + const length = nodeLength(child) + + if (child.nodeType === Node.TEXT_NODE) { + if (remaining <= length) { + place(child, remaining) + return true + } + remaining -= length + continue + } + + if (child.getAttribute?.('data-operand') != null) { + if (remaining === 0) { + place(container, i) + return true + } + if (remaining <= length) { + place(container, i + 1) + return true + } + remaining -= length + continue + } + + // Element wrappers (e.g. wildcard spans): recurse into children. + if (walk(child)) return true + } + return false + } + + if (walk(root)) return + + const range = document.createRange() + range.selectNodeContents(root) + range.collapse(false) + selection.removeAllRanges() + selection.addRange(range) + scrollSelectionIntoView(root) +} + +/** True when the DOM already mirrors tokenize(serialized). */ +const domMatchesTokens = (root, raw) => { + const tokens = tokenize(raw) + const children = Array.from(root.childNodes).filter((node) => { + if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? '') !== '' + return ( + node.getAttribute?.('data-operand') != null || + node.getAttribute?.('data-wildcard') != null || + node.getAttribute?.('data-date-unit') != null || + node.getAttribute?.('data-quote') != null + ) + }) + + // Empty editor may keep a single empty text node. + if (tokens.length === 0) { + return ( + root.childNodes.length === 0 || + (root.childNodes.length === 1 && + root.childNodes[0].nodeType === Node.TEXT_NODE && + (root.childNodes[0].textContent ?? '') === '') + ) + } + + if (children.length !== tokens.length) return false + + return tokens.every((token, index) => { + const child = children[index] + if (token.type === 'text') { + return ( + child.nodeType === Node.TEXT_NODE && child.textContent === token.value + ) + } + if (token.type === 'wildcard') { + return ( + child.getAttribute?.('data-wildcard') === token.value && + (child.textContent ?? '') === token.value + ) + } + if (token.type === 'dateUnit') { + return ( + child.getAttribute?.('data-date-unit') === token.value && + (child.textContent ?? '') === token.value + ) + } + if (token.type === 'quote') { + return ( + child.getAttribute?.('data-quote') === token.value && + (child.textContent ?? '') === token.value + ) + } + return child.getAttribute?.('data-operand') === token.value + }) +} + +/** Operand chips are only "live" when their portal React root is still mounted. */ +const chipsHaveLiveRoots = (root, roots) => { + for (const node of root.childNodes) { + if (node.getAttribute?.('data-operand') == null) continue + if (!roots.has(node)) return false + } + return true +} + +const toFilterExpression = (values) => { + if (!values?.length) return '' + const parts = values.map((value) => { + if (value && typeof value === 'object') { + return String(value._id ?? value.type ?? JSON.stringify(value)) + } + return String(value) + }) + return parts.length === 1 ? parts[0] : parts.join('|') +} + +const fromFilterExpression = (expr) => { + if (expr === undefined || expr === null || expr === '') return [] + if (typeof expr === 'string' && expr.includes('|')) { + return expr.split('|').filter((part) => part !== '') + } + return [expr] +} + +const FilterInput = ({ + value = '', + onChange, + placeholder = 'Filter…', + disabled = false, + allowClear = false, + className = '', + style, + onFocus, + onBlur, + onPressEnter, + size, + propertyFilter = null, + filter = {}, + masterFilter = {} +}) => { + const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext) + const prefixCls = getPrefixCls('input') + const { themeConfig } = useThemeContext() + const themeConfigRef = useRef(themeConfig) + themeConfigRef.current = themeConfig + + const rootCls = useCSSVarCls(prefixCls) + const [wrapSharedCSSVar, hashId, cssVarCls] = useSharedStyle(prefixCls) + const [wrapCSSVar] = useStyle(prefixCls, rootCls) + + const { compactSize, compactItemClassnames } = useCompactItemContext( + prefixCls, + direction + ) + const mergedSize = useSize((ctx) => size ?? compactSize ?? ctx) + + const editorRef = useRef(null) + const tagRootsRef = useRef(new Map()) + const focusedRef = useRef(false) + const composingRef = useRef(false) + const selectingRef = useRef(false) + const valueRef = useRef(value ?? '') + const quickFiltersRef = useRef(null) + const quickFilterModalOpenRef = useRef(false) + const [focused, setFocused] = useState(false) + const [internalValue, setInternalValue] = useState(value ?? '') + const [quickFiltersHeight, setQuickFiltersHeight] = useState(0) + const [quickFilterModalOpen, setQuickFilterModalOpen] = useState(false) + const [hover, setHover] = useState(false) + + const clearTagRoots = useCallback(() => { + tagRootsRef.current.forEach((root) => { + queueMicrotask(() => root.unmount()) + }) + tagRootsRef.current.clear() + }, []) + + const unmountChip = useCallback((wrapper) => { + const root = tagRootsRef.current.get(wrapper) + if (root) { + queueMicrotask(() => root.unmount()) + tagRootsRef.current.delete(wrapper) + } + }, []) + + const renderTagNode = useCallback((symbol, themeValue) => { + const operand = OPERAND_BY_SYMBOL[symbol] + return ( + + + {operand?.label ?? symbol} + + + ) + }, []) + + const renderOperandChip = useCallback( + (symbol) => { + const wrapper = document.createElement('span') + wrapper.setAttribute('data-operand', symbol) + wrapper.setAttribute('contenteditable', 'false') + wrapper.className = 'filter-input-operand-chip' + + const root = createRoot(wrapper) + root.render(renderTagNode(symbol, themeConfigRef.current)) + tagRootsRef.current.set(wrapper, root) + return wrapper + }, + [renderTagNode] + ) + + // Portal Tags sit outside the React tree — re-render them when theme changes. + useEffect(() => { + tagRootsRef.current.forEach((root, wrapper) => { + root.render( + renderTagNode(wrapper.getAttribute('data-operand'), themeConfig) + ) + }) + }, [themeConfig, renderTagNode]) + + const emitChange = useCallback( + (next) => { + valueRef.current = next + setInternalValue(next) + onChange?.(next) + }, + [onChange] + ) + + const paint = useCallback( + (raw, caret = null) => { + const editor = editorRef.current + if (!editor) return + + const normalized = normalize(raw) + + // Already in sync with live chip roots — skip to avoid a tag flash. + if ( + domMatchesTokens(editor, normalized) && + chipsHaveLiveRoots(editor, tagRootsRef.current) + ) { + if (caret != null && focusedRef.current) { + requestAnimationFrame(() => { + setCaretOffset(editor, caret) + scrollSelectionIntoView(editor) + }) + } + return + } + + const scrollLeft = editor.scrollLeft + const tokens = tokenize(normalized) + + // Reuse operand chips that still have a mounted React root. + const chipPool = new Map() + for (const node of Array.from(editor.childNodes)) { + const symbol = node.getAttribute?.('data-operand') + if (symbol == null) continue + if (!tagRootsRef.current.has(node)) continue + const list = chipPool.get(symbol) + if (list) list.push(node) + else chipPool.set(symbol, [node]) + } + + const takeChip = (symbol) => { + const list = chipPool.get(symbol) + while (list?.length) { + const wrapper = list.shift() + if (tagRootsRef.current.has(wrapper)) return wrapper + } + return renderOperandChip(symbol) + } + + const nextChildren = [] + if (tokens.length === 0) { + nextChildren.push(document.createTextNode('')) + } else { + for (const item of tokens) { + if (item.type === 'text') { + nextChildren.push(document.createTextNode(item.value)) + } else if (item.type === 'wildcard') { + nextChildren.push(renderWildcardNode(item.value)) + } else if (item.type === 'dateUnit') { + nextChildren.push(renderDateUnitNode(item.value)) + } else if (item.type === 'quote') { + nextChildren.push(renderQuoteNode()) + } else { + nextChildren.push(takeChip(item.value)) + } + } + } + + chipPool.forEach((list) => { + list.forEach((wrapper) => unmountChip(wrapper)) + }) + + editor.replaceChildren(...nextChildren) + editor.scrollLeft = scrollLeft + + if (caret != null && focusedRef.current) { + requestAnimationFrame(() => { + setCaretOffset(editor, caret) + scrollSelectionIntoView(editor) + }) + } + }, + [renderOperandChip, unmountChip] + ) + + useEffect(() => { + const next = value ?? '' + valueRef.current = next + setInternalValue(next) + if (!focusedRef.current) { + paint(next) + } + }, [value, paint]) + + useEffect(() => () => clearTagRoots(), [clearTagRoots]) + + const syncFromDom = useCallback(() => { + const editor = editorRef.current + if (!editor) return + + const caret = getCaretOffset(editor) + const serialized = serializeNode(editor) + const normalized = normalize(serialized) + + if (!domMatchesTokens(editor, normalized)) { + paint(normalized, caret) + } else { + scrollSelectionIntoView(editor) + } + + if (normalized !== valueRef.current) { + emitChange(normalized) + } + }, [emitChange, paint]) + + const findOperandWrapper = (node) => { + const editor = editorRef.current + let current = node + while (current && current !== editor) { + if (current.getAttribute?.('data-operand') != null) return current + current = current.parentNode + } + return null + } + + const handleInput = () => { + if (composingRef.current || disabled) return + syncFromDom() + } + + const removeChip = (chip) => { + const editor = editorRef.current + if (!editor || !chip) return + const parent = chip.parentNode + const index = Array.from(parent.childNodes).indexOf(chip) + let offset = 0 + for (let i = 0; i < index; i += 1) { + offset += nodeLength(parent.childNodes[i]) + } + const root = tagRootsRef.current.get(chip) + if (root) { + queueMicrotask(() => root.unmount()) + tagRootsRef.current.delete(chip) + } + parent.removeChild(chip) + setCaretOffset(editor, offset) + syncFromDom() + } + + const chipBeforeCaret = () => { + const editor = editorRef.current + const selection = window.getSelection() + if (!editor || !selection?.rangeCount || !selection.isCollapsed) return null + const { startContainer, startOffset } = selection.getRangeAt(0) + + const insideOperand = findOperandWrapper(startContainer) + if (insideOperand) return insideOperand + + if (startContainer === editor && startOffset > 0) { + const prev = editor.childNodes[startOffset - 1] + if (prev?.getAttribute?.('data-operand') != null) return prev + } + if (startContainer.nodeType === Node.TEXT_NODE && startOffset === 0) { + const prev = startContainer.previousSibling + if (prev?.getAttribute?.('data-operand') != null) return prev + } + return null + } + + const chipAfterCaret = () => { + const editor = editorRef.current + const selection = window.getSelection() + if (!editor || !selection?.rangeCount || !selection.isCollapsed) return null + const { startContainer, startOffset } = selection.getRangeAt(0) + + const insideOperand = findOperandWrapper(startContainer) + if (insideOperand) return insideOperand + + if (startContainer === editor) { + const next = editor.childNodes[startOffset] + if (next?.getAttribute?.('data-operand') != null) return next + } + if ( + startContainer.nodeType === Node.TEXT_NODE && + startOffset === (startContainer.textContent?.length ?? 0) + ) { + const next = startContainer.nextSibling + if (next?.getAttribute?.('data-operand') != null) return next + } + return null + } + + const handleKeyDown = (event) => { + if (disabled) return + + if (event.key === 'Enter') { + event.preventDefault() + onPressEnter?.(internalValue) + return + } + + if (event.key === 'Backspace') { + const chip = chipBeforeCaret() + if (chip) { + event.preventDefault() + removeChip(chip) + } + return + } + + if (event.key === 'Delete') { + const chip = chipAfterCaret() + if (chip) { + event.preventDefault() + removeChip(chip) + } + } + } + + const handlePaste = (event) => { + if (disabled) return + event.preventDefault() + const text = (event.clipboardData?.getData('text/plain') ?? '').replace( + /[\r\n]+/g, + '' + ) + const editor = editorRef.current + if (!editor) return + + const selection = window.getSelection() + if (!selection?.rangeCount) return + + const range = selection.getRangeAt(0) + const start = getOffsetAt( + editor, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(editor, range.endContainer, range.endOffset, 'end') + const from = Math.min(start, end) + const to = Math.max(start, end) + const current = serializeNode(editor) + const next = `${current.slice(0, from)}${text}${current.slice(to)}` + const normalized = normalize(next) + paint(normalized, from + text.length) + emitChange(normalized) + } + + const handleCopy = (event) => { + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return + const text = getSelectedSerializedText(editor) + if (text == null) return + event.preventDefault() + event.clipboardData?.setData('text/plain', text) + } + + const handleCut = (event) => { + if (disabled) return + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return + + const text = getSelectedSerializedText(editor) + if (text == null) return + + event.preventDefault() + event.clipboardData?.setData('text/plain', text) + + const range = selection.getRangeAt(0) + const start = getOffsetAt( + editor, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(editor, range.endContainer, range.endOffset, 'end') + const from = Math.min(start, end) + const to = Math.max(start, end) + const current = serializeNode(editor) + const next = `${current.slice(0, from)}${current.slice(to)}` + const normalized = normalize(next) + paint(normalized, from) + emitChange(normalized) + } + + const handleClear = (event) => { + event.preventDefault() + event.stopPropagation() + if (disabled) return + paint('') + emitChange('') + editorRef.current?.focus() + } + + const propertyFilterEnabled = + !disabled && + propertyFilter?.modelType != null && + propertyFilter?.propertyName != null + + const propertyFilterValue = useMemo( + () => fromFilterExpression(internalValue), + [internalValue] + ) + + const handlePropertyFilterChange = useCallback( + (keys) => { + const next = toFilterExpression(keys) + paint(next, focusedRef.current ? next.length : null) + emitChange(next) + }, + [emitChange, paint] + ) + + const handleQuickFilterModalOpenChange = useCallback((open) => { + quickFilterModalOpenRef.current = open + setQuickFilterModalOpen(open) + if (open) { + editorRef.current?.blur() + focusedRef.current = false + setFocused(false) + } else { + requestAnimationFrame(() => { + editorRef.current?.focus() + focusedRef.current = true + setFocused(true) + }) + } + }, []) + + const propertyFilterPopoverOpen = focused || quickFilterModalOpen + + useEffect(() => { + if (!propertyFilterEnabled) { + setQuickFiltersHeight(0) + return + } + + const node = quickFiltersRef.current + if (!node) return + + const update = () => { + setQuickFiltersHeight(node.getBoundingClientRect().height) + } + + update() + const observer = new ResizeObserver(update) + observer.observe(node) + return () => observer.disconnect() + }, [ + propertyFilterEnabled, + propertyFilter?.modelType, + propertyFilter?.propertyName, + focused + ]) + + const propertyFilterContent = propertyFilterEnabled ? ( +
{ + // Keep the input focused while interacting with the popover. + event.preventDefault() + }} + className='filter-input-property-filter' + style={{ + '--filter-input-property-filter-max-height': `${quickFiltersHeight + PROPERTY_FILTER_PANEL_MAX_HEIGHT}px` + }} + > +
+ +
+
+ +
+ +
+
+ +
+
+
+
+ ) : null + + // Auto-scroll while click-dragging a selection past the visible edges. + useEffect(() => { + let raf = 0 + let lastClientX = null + + const stopRaf = () => { + if (raf) { + cancelAnimationFrame(raf) + raf = 0 + } + } + + const tick = () => { + raf = 0 + if (!selectingRef.current || !focusedRef.current || lastClientX == null) { + return + } + const editor = editorRef.current + if (!editor) return + + const rect = editor.getBoundingClientRect() + const edge = 28 + let dx = 0 + if (lastClientX >= rect.right - edge) { + dx = Math.min( + 28, + Math.ceil((lastClientX - (rect.right - edge)) / 2) + 6 + ) + } else if (lastClientX <= rect.left + edge) { + dx = -Math.min(28, Math.ceil((rect.left + edge - lastClientX) / 2) + 6) + } + + if (dx !== 0) { + const maxScroll = editor.scrollWidth - editor.clientWidth + const next = Math.max(0, Math.min(maxScroll, editor.scrollLeft + dx)) + if (next !== editor.scrollLeft) { + editor.scrollLeft = next + extendSelectionToPoint( + editor, + lastClientX, + rect.top + rect.height / 2 + ) + scrollSelectionIntoView(editor) + raf = requestAnimationFrame(tick) + } + } + } + + const onMouseMove = (event) => { + if (!selectingRef.current || !focusedRef.current) return + lastClientX = event.clientX + const editor = editorRef.current + if (!editor) return + scrollPointIntoView(editor, event.clientX) + if (!raf) raf = requestAnimationFrame(tick) + } + + const onMouseUp = () => { + selectingRef.current = false + lastClientX = null + stopRaf() + } + + const onSelectionChange = () => { + if (!focusedRef.current) return + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || !editor.contains(selection.focusNode)) + return + scrollSelectionIntoView(editor) + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + document.addEventListener('selectionchange', onSelectionChange) + return () => { + stopRaf() + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + document.removeEventListener('selectionchange', onSelectionChange) + } + }, []) + + const showPlaceholder = Boolean(placeholder) && !internalValue + const showClear = allowClear && !!internalValue && !disabled + const affixCls = `${prefixCls}-affix-wrapper` + + const input = ( + { + if (!disabled && !quickFilterModalOpenRef.current) { + editorRef.current?.focus() + } + }} + onMouseEnter={() => { + setHover(true) + }} + onMouseLeave={() => { + setHover(false) + }} + > + {showPlaceholder && ( + + {placeholder} + + )} +
{ + selectingRef.current = true + }} + onCompositionStart={() => { + composingRef.current = true + }} + onCompositionEnd={() => { + composingRef.current = false + syncFromDom() + }} + onFocus={(event) => { + focusedRef.current = true + setFocused(true) + onFocus?.(event) + }} + onBlur={(event) => { + if (quickFilterModalOpenRef.current) { + focusedRef.current = false + setFocused(false) + return + } + focusedRef.current = false + selectingRef.current = false + setFocused(false) + const next = normalize(serializeNode(editorRef.current)) + paint(next) + if (next !== valueRef.current) emitChange(next) + onBlur?.(event) + }} + /> + {showClear && ( + + + + + + )} + {propertyFilterEnabled && ( + + {/* Anchor only — keep Popover from wrapping the Compact item (z-index/focus ring). */} + + + )} + + ) + + return wrapSharedCSSVar(wrapCSSVar(input)) +} + +FilterInput.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + allowClear: PropTypes.bool, + className: PropTypes.string, + style: PropTypes.object, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onPressEnter: PropTypes.func, + size: PropTypes.oneOf(['small', 'middle', 'large']), + propertyFilter: PropTypes.shape({ + modelType: PropTypes.string.isRequired, + propertyName: PropTypes.string.isRequired + }), + filter: PropTypes.object, + masterFilter: PropTypes.object +} + +export default FilterInput +export { OPERANDS, tokenize } diff --git a/src/components/Dashboard/common/FilterSidebar.jsx b/src/components/Dashboard/common/FilterSidebar.jsx index 0f3a0929..addefd7e 100644 --- a/src/components/Dashboard/common/FilterSidebar.jsx +++ b/src/components/Dashboard/common/FilterSidebar.jsx @@ -1,5 +1,6 @@ import { useMemo, useState, useEffect, useRef, useCallback } from 'react' -import { Card, Select, Input, Button, Flex, Space, Dropdown } from 'antd' +import { Card, Select, Button, Flex, Space, Dropdown } from 'antd' +import FilterInput from './FilterInput' import { CloseOutlined } from '@ant-design/icons' import PropTypes from 'prop-types' import PlusIcon from '../../Icons/PlusIcon' @@ -10,11 +11,43 @@ import { import MissingPlaceholder from './MissingPlaceholder' import BinIcon from '../../Icons/BinIcon' +const hiddenSizerStyle = { + gridArea: '1 / 1', + visibility: 'hidden', + whiteSpace: 'nowrap' +} + +const FieldSelectLabel = ({ label, options }) => ( + + + Field + + {options.map((option) => ( + + {option.label} + + ))} + {label} + +) + +FieldSelectLabel.propTypes = { + label: PropTypes.node, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.string + }) + ).isRequired +} + const FilterSidebar = ({ type, filter = {}, onFilterChange, - masterFilter = {} + masterFilter = {}, + viewFilter = {}, + isEditing = false }) => { const model = getModelByName(type) const modelProps = getModelProperties(type) @@ -38,7 +71,17 @@ const FilterSidebar = ({ if (initialEmptyFields.current.has(k) && v === '') continue visible[k] = v } - setLocalFilter(visible) + setLocalFilter((prev) => { + const prevKeys = Object.keys(prev) + const nextKeys = Object.keys(visible) + if ( + prevKeys.length === nextKeys.length && + nextKeys.every((key) => prev[key] === visible[key]) + ) { + return prev + } + return visible + }) }, [filter]) const debouncedFilterChange = useCallback( @@ -72,6 +115,11 @@ const FilterSidebar = ({ [filterableProperties] ) + const viewFilterKeys = useMemo( + () => new Set(Object.keys(viewFilter).filter((k) => viewFilter[k] !== '')), + [viewFilter] + ) + const activeFields = Object.keys(localFilter) const usedFields = new Set(activeFields) @@ -136,7 +184,7 @@ const FilterSidebar = ({ } return ( - + @@ -151,29 +199,46 @@ const FilterSidebar = ({ /> - {rows.map((row) => ( - - changeValue(row.field, e.target.value)} - style={{ flex: 1 }} - /> - @@ -34,7 +40,8 @@ MessageDialogView.propTypes = { description: PropTypes.node, onOk: PropTypes.func.isRequired, okText: PropTypes.string, - okLoading: PropTypes.bool + okLoading: PropTypes.bool, + danger: PropTypes.bool } export default MessageDialogView diff --git a/src/components/Dashboard/common/MiscId.jsx b/src/components/Dashboard/common/MiscId.jsx index dad222cf..413d9061 100644 --- a/src/components/Dashboard/common/MiscId.jsx +++ b/src/components/Dashboard/common/MiscId.jsx @@ -1,6 +1,7 @@ import PropTypes from 'prop-types' import { Flex, Typography } from 'antd' import CopyButton from './CopyButton' +import ElipsisText from './ElipsisText' const { Text } = Typography @@ -15,13 +16,12 @@ const MiscId = ({ value, showCopy = true }) => { className='miscid' style={{ minWidth: '0px', width: '100%' }} > - {value} - + {showCopy && ( { + var border = undefined + if (hasBorder == false) { + border = 'none' + } else if (mutedBorder == true && hasBorder == true) { + border = '1px solid var(--color-descriptions-border)' + } return ( { + const isMobile = useMediaQuery({ maxWidth: 768 }) + const { userProfile, authenticated } = useContext(AuthContext) + const { isElectron } = useContext(ElectronContext) + const navigate = useNavigate() + const location = useLocation() + const includeDev = import.meta.env.DEV + + const sectionItems = useMemo( + () => + [ + { + key: 'production', + label: 'Production', + iconKey: 'production' + }, + { + key: 'inventory', + label: 'Inventory', + iconKey: 'inventory' + }, + { + key: 'sales', + label: 'Sales', + iconKey: 'sales' + }, + { + key: 'finance', + label: 'Finance', + iconKey: 'finance' + }, + { + key: 'management', + label: 'Management', + iconKey: 'settings' + } + ].filter((item) => + isSidebarSectionVisible(item.key, { includeDev, userProfile }) + ), + [includeDev, userProfile] + ) + + const selectedKey = useMemo(() => { + const pathParts = location.pathname.split('/').filter(Boolean) + const sectionKey = pathParts[1] + return ( + sectionItems.find((item) => item.key === sectionKey)?.key || + sectionItems[0]?.key + ) + }, [location.pathname, sectionItems]) + + const options = useMemo( + () => + sectionItems.map((item) => ({ + value: item.key, + label: item.label, + icon: getSidebarIconNode(item.iconKey) + })), + [sectionItems] + ) + + const showControls = !isElectron || authenticated + + if (!isMobile || !showControls || options.length === 0) { + return null + } + + return ( +
+ + navigate(getSidebarDefaultPath(key, { includeDev, userProfile })) + } + /> +
+ ) +} + +export default MobileFooterNavigation diff --git a/src/components/Dashboard/common/ModelHistoryDisplay.jsx b/src/components/Dashboard/common/ModelHistoryDisplay.jsx index 3b957161..5dbe6be1 100644 --- a/src/components/Dashboard/common/ModelHistoryDisplay.jsx +++ b/src/components/Dashboard/common/ModelHistoryDisplay.jsx @@ -1,16 +1,14 @@ -import { useEffect, useState, useContext, useMemo } from 'react' -import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd' +import { useEffect, useState, useContext, useMemo, useCallback } from 'react' import { - ResponsiveContainer, - BarChart, - Bar, - CartesianGrid, - XAxis, - YAxis, - Tooltip, - Legend -} from 'recharts' - + Card, + Segmented, + Flex, + Popover, + DatePicker, + Button, + Space, + Switch +} from 'antd' import PropTypes from 'prop-types' import { getModelByName } from '../../../database/ObjectModels' import { ApiServerContext } from '../context/ApiServerContext' @@ -18,22 +16,79 @@ import { AuthContext } from '../context/AuthContext' import dayjs from 'dayjs' import { useThemeContext } from '../context/ThemeContext' import MissingPlaceholder from './MissingPlaceholder' +import HistoryEChart from './HistoryEChart' +import HistoryChartLegend from './HistoryChartLegend' +import { useHistoryLegendOverlay } from '../hooks/useHistoryLegendOverlay' import CheckIcon from '../../Icons/CheckIcon' import { LoadingOutlined } from '@ant-design/icons' +import { round } from '../utils/Utils' + +const legendMeasureStyle = { + position: 'absolute', + visibility: 'hidden', + pointerEvents: 'none', + whiteSpace: 'nowrap', + width: 'max-content', + height: 0, + overflow: 'hidden' +} + +const toCssHeight = (value) => + typeof value === 'number' ? `${value}px` : value + +const isAmountStat = (statDef) => { + if (statDef.prefix === '£') return true + const name = statDef.name || '' + if (name.includes('Amount') || name.includes('Value')) return true + const combineList = statDef.combine || statDef.sum + if ( + combineList?.some( + (statName) => statName.includes('Amount') || statName.endsWith('.sum') + ) + ) { + return true + } + return false +} + +const isValueStat = (statDef) => { + if (isAmountStat(statDef)) return false + if (statDef.name?.endsWith('.sum')) return true + if (statDef.suffix) return true + return false +} + +export const resolveStatHistoryGroup = (statDef) => { + if (statDef.history === false) return null + if (statDef.historyGroup) return statDef.historyGroup + if (isAmountStat(statDef)) return 'amount' + if (isValueStat(statDef)) return 'value' + return 'count' +} + +export const getModelHistoryStats = (model, historyGroup = 'count') => { + return (model?.stats || []).filter( + (statDef) => resolveStatHistoryGroup(statDef) === historyGroup + ) +} const ModelHistoryDisplay = ({ objectType, + historyGroup = 'count', startDate, endDate, styles, - height = 400 + height = 400, + chartType: chartTypeProp }) => { + const chartType = chartTypeProp ?? (historyGroup === 'amount' ? 'line' : 'bar') const { getModelHistory, connected } = useContext(ApiServerContext) const { token } = useContext(AuthContext) const [historyData, setHistoryData] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [timeRange, setTimeRange] = useState('4hrs') + const [historyMode, setHistoryMode] = useState('absolute') const [startCustomDate, setStartCustomDate] = useState(null) const [endCustomDate, setEndCustomDate] = useState(null) const { isDarkMode, getColors } = useThemeContext() @@ -110,18 +165,73 @@ const ModelHistoryDisplay = ({ defaultEndDate ]) + const model = useMemo( + () => (objectType ? getModelByName(objectType) : null), + [objectType] + ) + + const modelStats = useMemo( + () => getModelHistoryStats(model, historyGroup), + [model, historyGroup] + ) + + const themeColors = getColors() + + const seriesLabels = useMemo(() => { + const colors = { + success: themeColors.colorSuccess, + processing: themeColors.colorInfo, + error: themeColors.colorError, + warning: themeColors.colorWarning, + default: '#8c8c8c', + cyan: themeColors.colorCyan, + pink: themeColors.colorPink, + purple: themeColors.colorPurple, + magenta: themeColors.colorMagenta, + volcano: themeColors.colorVolcano + } + + return modelStats.map((statDef) => { + const color = statDef.color + ? colors[statDef.color] || statDef.color + : colors.default + + return { + label: statDef.label || statDef.name, + color, + prefix: statDef.prefix, + suffix: statDef.suffix, + roundNumber: statDef.roundNumber + } + }) + }, [modelStats, themeColors]) + + const formatTooltipValue = useCallback( + (value, seriesLabel) => { + const seriesDef = seriesLabels.find((item) => item.label === seriesLabel) + let numericValue = Number(value ?? 0) + if (seriesDef?.roundNumber) { + numericValue = round(numericValue, seriesDef.roundNumber) + } + return `${seriesDef?.prefix || ''}${numericValue}${seriesDef?.suffix || ''}` + }, + [seriesLabels] + ) + + const { + slotRef, + measureRef, + overlay: legendOverflow + } = useHistoryLegendOverlay(seriesLabels) + if (!objectType) { return null } - const model = getModelByName(objectType) - if (!model) { return null } - const modelStats = model.stats || [] - if (modelStats.length === 0) { return null } @@ -248,7 +358,9 @@ const ModelHistoryDisplay = ({ date: point.date, dateFormatted: dayjs(point.date).format('DD/MM HH:mm'), category: label, - value: statValue || 0 + value: statValue || 0, + prefix: statDef.prefix || '', + suffix: statDef.suffix || '' } }) }) @@ -258,31 +370,7 @@ const ModelHistoryDisplay = ({ return new Date(a.date) - new Date(b.date) }) - const themeColors = getColors() - - // Create color mapping from model stats - const colors = { - success: themeColors.colorSuccess, - processing: themeColors.colorInfo, - error: themeColors.colorError, - warning: themeColors.colorWarning, - default: '#8c8c8c', - cyan: themeColors.colorCyan, - pink: themeColors.colorPink, - purple: themeColors.colorPurple, - magenta: themeColors.colorMagenta, - volcano: themeColors.colorVolcano - } - - // Build color range array based on model stats order - const colorRange = modelStats.map((stat) => { - if (stat.color) { - return colors[stat.color] || stat.color - } - return colors.default - }) - - const chartRows = Array.from( + const absoluteRows = Array.from( chartData .reduce((acc, item) => { const existing = acc.get(item.date) || { @@ -296,6 +384,22 @@ const ModelHistoryDisplay = ({ .values() ).sort((a, b) => new Date(a.date) - new Date(b.date)) + const chartRows = + historyMode === 'relative' && absoluteRows.length > 0 + ? absoluteRows.map((row) => { + const next = { + date: row.date, + dateFormatted: row.dateFormatted + } + for (const statDef of modelStats) { + const category = statDef.label || statDef.name + next[category] = + (row[category] || 0) - (absoluteRows[0][category] || 0) + } + return next + }) + : absoluteRows + const customTimeRangeContent = ( - - {!startDate && !endDate && ( - - + + + + {!startDate && !endDate && ( - + )} + + setHistoryMode(checked ? 'absolute' : 'relative') + } + /> + - - {loading == true && } - - - - )} - {chartRows.length > 0 && ( -
- - - - - - - - {modelStats.map((statDef, index) => { - const category = statDef.label || statDef.name - return ( - - ) - })} - - -
- )} - {loading == false && chartRows.length == 0 && ( - - + {!legendOverflow && ( + + )} + +
+ + + {!startDate && !endDate && ( + <> + {loading == true && chartRows.length != 0 && ( + + )} + + + )} - )} +
+
+ {legendOverflow && ( +
+ +
+ )} +
+ {chartRows.length > 0 && ( + + )} + {loading == true && chartRows.length == 0 && ( + + + + )} + {loading == false && chartRows.length == 0 && ( + + + + )} +
+
) @@ -427,13 +573,15 @@ const ModelHistoryDisplay = ({ ModelHistoryDisplay.propTypes = { objectType: PropTypes.string.isRequired, + historyGroup: PropTypes.oneOf(['count', 'amount', 'value']), startDate: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(Date) ]), styles: PropTypes.object, endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + chartType: PropTypes.oneOf(['line', 'bar']) } export default ModelHistoryDisplay diff --git a/src/components/Dashboard/common/NewObjectButtons.jsx b/src/components/Dashboard/common/NewObjectButtons.jsx index 61ec5043..74e1987f 100644 --- a/src/components/Dashboard/common/NewObjectButtons.jsx +++ b/src/components/Dashboard/common/NewObjectButtons.jsx @@ -12,13 +12,15 @@ const NewObjectButtons = ({ submitText = 'Done', disabled = false }) => { + const controlsDisabled = disabled || submitLoading + return ( {totalSteps > 1 ? ( @@ -27,7 +29,7 @@ const NewObjectButtons = ({ {currentStep < totalSteps - 1 ? ( - - + ) } ObjectActions.propTypes = { type: PropTypes.string.isRequired, - objectData: PropTypes.object.isRequired, - id: PropTypes.string.isRequired, + objectData: PropTypes.object, + id: PropTypes.string, + pageName: PropTypes.string, + onReload: PropTypes.func, disabled: PropTypes.bool, buttonProps: PropTypes.object, buttonLabel: PropTypes.string, diff --git a/src/components/Dashboard/common/ObjectCard.jsx b/src/components/Dashboard/common/ObjectCard.jsx index 1959aab4..53fb31bf 100644 --- a/src/components/Dashboard/common/ObjectCard.jsx +++ b/src/components/Dashboard/common/ObjectCard.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import ObjectProperty from './ObjectProperty' import { createElement } from 'react' import Thumbnail from './Thumbnail' +import { LoadingOutlined } from '@ant-design/icons' const ObjectCard = ({ isSkeleton = false, @@ -13,6 +14,7 @@ const ObjectCard = ({ isEditing = false, rowActions = [], renderActions, + lazyLoading = false, cardStyle = 'borderless' }) => { const descriptionItems = [] @@ -59,7 +61,7 @@ const ObjectCard = ({ var actions = undefined if (rowActions.length > 0) { - actions = renderActions(record) + actions = renderActions(record, lazyLoading) } const primaryDescriptionItems = thumbnailPresent @@ -96,7 +98,7 @@ const ObjectCard = ({ return ( @@ -146,8 +148,20 @@ const ObjectCard = ({ {actions && ( <> - - {actions} + + + + {actions} + + + {lazyLoading && ( + + )} )} @@ -166,7 +180,8 @@ ObjectCard.propTypes = { rowActions: PropTypes.array, renderActions: PropTypes.func.isRequired, cardStyle: PropTypes.string, - isSkeleton: PropTypes.bool + isSkeleton: PropTypes.bool, + lazyLoading: PropTypes.bool } export default ObjectCard diff --git a/src/components/Dashboard/common/ObjectChildCards.jsx b/src/components/Dashboard/common/ObjectChildCards.jsx new file mode 100644 index 00000000..3d6e52cf --- /dev/null +++ b/src/components/Dashboard/common/ObjectChildCards.jsx @@ -0,0 +1,242 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import PropTypes from 'prop-types' +import { Button, Card, Flex, Space, Typography } from 'antd' +import ObjectInfo from './ObjectInfo' +import ChevronLeftIcon from '../../Icons/ChevronLeftIcon' +import ChevronRightIcon from '../../Icons/ChevronRightIcon' +import { getModelProperty } from '../../../database/ObjectModels' + +const { Text } = Typography + +const resolveChangeValue = (val, type) => { + if (type === 'bool') return val + if (val?.target && typeof val.target === 'object') { + return val.target.value + } + return val +} + +const ObjectChildCards = ({ + name, + type, + label, + properties: propertiesOverride, + columns: columnsOverride, + visibleProperties = {}, + objectData = null, + parentData = null, + value, + isEditing = false, + onChange, + selectedIndex, + onSelectedIndexChange, + emptyText = 'No items', + bordered = false, + labelWidth = 115, + column = 1, + ...cardProps +}) => { + const childProperty = useMemo(() => { + if (!type || !name) return null + return getModelProperty(type, name) + }, [name, type]) + + const properties = useMemo(() => { + if (Array.isArray(propertiesOverride) && propertiesOverride.length) { + return propertiesOverride + } + return Array.isArray(childProperty?.properties) + ? childProperty.properties + : [] + }, [childProperty, propertiesOverride]) + + const propertyDefinitions = useMemo(() => { + const columnNames = + Array.isArray(columnsOverride) && columnsOverride.length + ? columnsOverride + : Array.isArray(childProperty?.columns) + ? childProperty.columns + : [] + + if (!columnNames.length) return properties + + const propertyMap = new Map( + properties.map((property) => [property.name, property]) + ) + return columnNames + .map((columnName) => propertyMap.get(columnName)) + .filter(Boolean) + }, [childProperty, columnsOverride, properties]) + + const items = useMemo(() => { + if (Array.isArray(value)) return value + if (name && Array.isArray(objectData?.[name])) return objectData[name] + return [] + }, [name, objectData, value]) + + const [internalIndex, setInternalIndex] = useState(0) + const isControlled = typeof onSelectedIndexChange === 'function' + const currentIndex = isControlled ? (selectedIndex ?? 0) : internalIndex + + const setCurrentIndex = useCallback( + (nextIndex) => { + if (isControlled) { + onSelectedIndexChange(nextIndex) + return + } + setInternalIndex(nextIndex) + }, + [isControlled, onSelectedIndexChange] + ) + + useEffect(() => { + if (!items.length) { + if (currentIndex !== 0) setCurrentIndex(0) + return + } + if (currentIndex > items.length - 1) { + setCurrentIndex(items.length - 1) + } + if (currentIndex < 0) { + setCurrentIndex(0) + } + }, [currentIndex, items.length, setCurrentIndex]) + + const currentRecord = items[currentIndex] || null + + const handleStep = useCallback( + (delta) => { + const nextIndex = currentIndex + delta + if (nextIndex < 0 || nextIndex > items.length - 1) return + setCurrentIndex(nextIndex) + }, + [currentIndex, items.length, setCurrentIndex] + ) + + const propertyMap = useMemo(() => { + const map = new Map() + properties.forEach((property) => { + if (property?.name) map.set(property.name, property) + }) + return map + }, [properties]) + + const handlePropertyChange = useCallback( + (propName, newVal) => { + const property = propertyMap.get(propName) + const resolved = resolveChangeValue(newVal, property?.type) + const parent = parentData || objectData + const nextItems = items.map((item, index) => { + if (index !== currentIndex) return item + const updated = { ...item, [propName]: resolved } + properties.forEach((childProperty) => { + if ( + childProperty?.name === propName || + typeof childProperty?.value !== 'function' + ) { + return + } + const computed = childProperty.value(updated, parent) + if (computed !== undefined) { + updated[childProperty.name] = computed + } + }) + return updated + }) + onChange?.(nextItems) + }, + [ + currentIndex, + items, + objectData, + onChange, + parentData, + properties, + propertyMap + ] + ) + + const title = label || childProperty?.label || name || 'Items' + const countLabel = items.length + ? `${currentIndex + 1}/${items.length}` + : '0/0' + + return ( + + + {title} ({countLabel}) + + + } + extra={ + +
+ ) +} + +ColumnFilterDropdown.propTypes = { + setSelectedKeys: PropTypes.func, + selectedKeys: PropTypes.array, + confirm: PropTypes.func, + clearFilters: PropTypes.func, + visible: PropTypes.bool, + propertyName: PropTypes.string, + propertyLabel: PropTypes.string, + modelType: PropTypes.string, + filter: PropTypes.object, + masterFilter: PropTypes.object +} + const RowForm = ({ record, isEditing, onRegister, children }) => { const [form] = Form.useForm() useEffect(() => { @@ -114,12 +299,14 @@ const ObjectTable = forwardRef( scrollHeight = 'calc(var(--unit-100vh) - 258px)', onDataChange, initialPage = 1, + viewMode: viewModeProp, cards = false, visibleColumns = {}, - masterFilter = {}, + masterFilter, size = 'middle', onStateChange, showFilterSidebar = false, + showSortSidebar = false, expandHeight = false, showActions = true, saveFilterInSession = false, @@ -134,9 +321,29 @@ const ObjectTable = forwardRef( }, ref ) => { + const objectListView = useContext(ObjectListViewContext) + const viewMode = normalizeViewMode( + viewModeProp ?? + objectListView?.viewMode ?? + (cards ? { type: 'cards' } : { type: 'list' }) + ) + const isCards = isCardsView(viewMode) + const isKanban = isKanbanView(viewMode) + const isTimeline = isTimelineView(viewMode) + const kanbanRef = useRef(null) + const prevViewModeKeyRef = useRef(null) const { token, userProfile } = useContext(AuthContext) const { isElectron } = useContext(ElectronContext) const { callAction } = useActions() + const resolvedMasterFilter = masterFilter ?? EMPTY_MASTER_FILTER + const listViewId = objectListView?.listViewId ?? null + const listViewFilter = objectListView?.listViewFilter ?? null + const listViewSort = objectListView?.listViewSort ?? null + const activeObjectView = objectListView?.activeObjectView ?? null + const userOverridesRef = objectListView?.userOverridesRef ?? null + const onObjectViewFilterSortChange = objectListView?.isEditing + ? objectListView.handleFilterSortChange + : null const onStateChangeRef = useRef(onStateChange) useEffect(() => { onStateChangeRef.current = onStateChange @@ -144,7 +351,7 @@ const ObjectTable = forwardRef( const { fetchObjects, connected, - subscribeToObjectUpdates, + subscribeToAllObjectUpdates, subscribeToObjectTypeUpdates, updateMultipleObjects, setObjectActivity, @@ -155,11 +362,19 @@ const ObjectTable = forwardRef( const { getPersistedFilter, getPersistedSorter, + getAllTabPersistedFilter, + getAllTabPersistedSorter, persistFilter, + persistSort, persistTableState, - registerPageFilter + persistAllTabTableState, + registerPageFilter, + registerPageSorter, + registerObjectListFilter, + registerObjectListSorter } = useTableStatePersistence({ scope: type, + viewId: listViewId, pagePath: location.pathname, saveFilterInSession, saveFilterInUrl, @@ -170,11 +385,12 @@ const ObjectTable = forwardRef( useSortInSession, useSortInUrl }) + const { getViewFromUrl } = useTableState() var adjustedScrollHeight = scrollHeight if (isMobile) { - adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)' + adjustedScrollHeight = 'calc(var(--unit-100vh) - 348px)' } - if (cards) { + if (isCards || isKanban || isTimeline) { adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)' } if (isElectron) { @@ -183,24 +399,80 @@ const ObjectTable = forwardRef( if (isMobile && isElectron) { adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)' } - if (cards && isElectron) { + if ((isCards || isKanban || isTimeline) && isElectron) { adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)' } const tableRef = useRef(null) const model = getModelByName(type) + const bulkActions = + model.actions?.filter( + (action) => + action.bulk === true && + hasActionPermission(userProfile, model, action.name) + ) || [] + const canBulkSelect = bulkActions.length > 0 && objectListView != null const activeFilterRef = useRef({}) const tableSorterRef = useRef({}) + const listViewIdRef = useRef(listViewId) + const listViewFilterRef = useRef(listViewFilter) + const sidebarViewIdRef = useRef(listViewId) + const activeObjectViewRef = useRef(activeObjectView) + activeObjectViewRef.current = activeObjectView + const onObjectViewFilterSortChangeRef = useRef(onObjectViewFilterSortChange) + onObjectViewFilterSortChangeRef.current = onObjectViewFilterSortChange const [sidebarFilter, setSidebarFilter] = useState({}) const [tableSorter, setTableSorter] = useState({}) const [initialized, setInitialized] = useState(false) + const [sidebarWidth, setSidebarWidth] = useSidebarWidth(type) + + const [verticalScrollVisible, setVerticalScrollVisible] = useState(false) + const tableContentRef = useRef(null) + + // Keep refs in sync with props. applyViewState may update them earlier in + // the same tick; props remain the source of truth after commit. + useEffect(() => { + listViewIdRef.current = listViewId + listViewFilterRef.current = listViewFilter || {} + }, [listViewId, listViewFilter]) + + const assignSidebarToView = useCallback((viewId) => { + sidebarViewIdRef.current = viewId ?? null + }, []) + + const lastAppliedViewKeyRef = useRef(null) + + const buildEffectiveFilter = useCallback( + (userFilter, viewFilter = listViewFilterRef.current) => { + const active = getActiveFilterValues(userFilter) + const viewId = listViewIdRef.current + // While editing a view, the sidebar IS the view filter. Merging the + // previous definition would put removed keys back. + if (!viewId || activeObjectViewRef.current) return active + return { + ...getActiveFilterValues(viewFilter || {}), + ...active + } + }, + [] + ) // Table state const [pages, setPages] = useState([]) const pagesRef = useRef(pages) - const tableData = useMemo( - () => pages.flatMap((page) => page.items), - [pages] - ) + const tableData = useMemo(() => { + const items = pages.flatMap((page) => page.items) + const seen = new Set() + return items.filter((item) => { + const id = item?._id + if (id == null) return true + if (seen.has(id)) { + logger.warn('Duplicate table row omitted:', id) + return false + } + seen.add(id) + return true + }) + }, [pages]) const [loading, setLoading] = useState(true) const [lazyLoading, setLazyLoading] = useState(false) @@ -219,18 +491,31 @@ const ObjectTable = forwardRef( onStateChangeRef.current?.({ isEditing, editLoading }) }, [isEditing, editLoading]) - const subscribedIdsRef = useRef([]) - // const [typeSubscribed, setTypeSubscribed] = useState(false) - const unsubscribesRef = useRef([]) const updateEventHandlerRef = useRef() const subscribeToObjectTypeUpdatesRef = useRef(null) const subscribedTypeRef = useRef(null) + const subscribeToAllObjectUpdatesRef = useRef(null) const newEventHandlerRef = useRef() const subscriptionFilterRef = useRef() + const effectiveSorterRef = useRef({}) + const reloadRef = useRef(null) + const silentReloadRef = useRef(null) const subscribeToObjectTypeUpdatesFnRef = useRef( subscribeToObjectTypeUpdates ) - const prevValuesRef = useRef({ type, masterFilter }) + const prevValuesRef = useRef({ type, masterFilter: resolvedMasterFilter }) + + const getMasterFilter = useCallback( + () => resolvedMasterFilter || EMPTY_MASTER_FILTER, + [resolvedMasterFilter] + ) + + const resolveSorter = useCallback((userSorter) => { + if (userSorter?.field && userSorter?.order) { + return { field: userSorter.field, order: userSorter.order } + } + return {} + }, []) const rowActions = model.actions?.filter((action) => action.row == true) || [] @@ -258,24 +543,86 @@ const ObjectTable = forwardRef( const loadingPagesRef = useRef(new Set()) const pendingScrollAnchorRef = useRef(null) + const dataLoadGenerationRef = useRef(0) + const [tableListKey, setTableListKey] = useState(0) + // Suppress Ant Design Table onChange while we remount / set filter+sort + // programmatically. Remounting with controlled filteredValue often fires + // onChange with all-null filters (sorter intact) and would wipe the view. + const isInternalTableChangeRef = useRef(false) + const internalTableChangeClearRafRef = useRef(null) - const renderActions = (objectData) => { + const suppressTableChange = useCallback(() => { + isInternalTableChangeRef.current = true + if (internalTableChangeClearRafRef.current != null) { + cancelAnimationFrame(internalTableChangeClearRafRef.current) + } + // Double rAF: wait until after the remounted Table commits and any + // synchronous/mount onChange from Ant Design has run. + internalTableChangeClearRafRef.current = requestAnimationFrame(() => { + internalTableChangeClearRafRef.current = requestAnimationFrame(() => { + internalTableChangeClearRafRef.current = null + isInternalTableChangeRef.current = false + }) + }) + }, []) + + const beginDataReload = useCallback(() => { + dataLoadGenerationRef.current += 1 + loadingPagesRef.current.clear() + pendingScrollAnchorRef.current = null + pagesRef.current = [] + suppressTableChange() + setTableListKey((key) => key + 1) + return dataLoadGenerationRef.current + }, [suppressTableChange]) + + const isStaleDataLoad = useCallback((generation) => { + return generation !== dataLoadGenerationRef.current + }, []) + + const clearTablePages = useCallback(() => { + pagesRef.current = [] + setPages([]) + }, []) + + const setTablePages = useCallback((nextPages) => { + pagesRef.current = nextPages + setPages(nextPages) + }, []) + + const mergeLoadedPage = useCallback((currentPages, loadedPage) => { + const withoutSkeletons = currentPages.filter( + (page) => !page.isSkeletonPage + ) + const withoutDuplicate = withoutSkeletons.filter( + (page) => page.pageNum !== loadedPage.pageNum + ) + return [...withoutDuplicate, loadedPage].sort( + (a, b) => a.pageNum - b.pageNum + ) + }, []) + + const renderActions = (objectData, actionsDisabled = false) => { return ( {rowActions.map((action, index) => { - var disabled = false + const denied = !hasActionPermission(userProfile, model, action.name) + var disabled = denied if (action.disabled) { if (typeof action.disabled === 'function') { - disabled = action.disabled({ - ...objectData, - _user: userProfile - }) + disabled = + denied || + action.disabled({ + ...objectData, + _user: userProfile + }) || + actionsDisabled } else { - disabled = action.disabled + disabled = denied || action.disabled || actionsDisabled } } return ( - +
- ) - } + visible, + propertyName, + propertyLabel + }) => ( + + ) const handleTableChange = (pagination, filters, sorter) => { + if (isInternalTableChangeRef.current) return + const next = { ...sidebarFilter } Object.entries(filters).forEach(([key, value]) => { - if (value && value.length > 0) { - next[key] = value[0] + const expression = toFilterExpression(value) + if (expression !== undefined) { + next[key] = expression } else { delete next[key] } @@ -935,34 +1749,172 @@ const ObjectTable = forwardRef( } setSidebarFilter(next) + assignSidebarToView(listViewIdRef.current) setTableSorter(nextSorter) - persistTableState(next, nextSorter) - setPages([]) - setLoading(true) - loadPage(initialPage, getActiveFilterValues(next), nextSorter) + if (activeObjectViewRef.current) { + listViewFilterRef.current = next + // Editing a view: keep filter/sort on the draft view only + onObjectViewFilterSortChangeRef.current?.(next, nextSorter) + } else if (listViewIdRef.current) { + // On a view but not editing: track as user override for startEditing + if (userOverridesRef) { + userOverridesRef.current = { + ...(userOverridesRef.current || {}), + filter: next, + sort: nextSorter + } + } + } else { + // All tab: persist personal filter/sort to URL/session + persistTableState(next, nextSorter) + } + const effective = buildEffectiveFilter(next) + activeFilterRef.current = effective + loadPage(initialPage, effective, resolveSorter(nextSorter), { + silent: true + }) } const handleSidebarFilterChange = useCallback( (newSidebarFilter) => { setSidebarFilter(newSidebarFilter) - persistFilter(newSidebarFilter) - setPages([]) - setLoading(true) - loadPage(initialPage, getActiveFilterValues(newSidebarFilter)) + assignSidebarToView(listViewIdRef.current) + if (activeObjectViewRef.current) { + listViewFilterRef.current = newSidebarFilter + onObjectViewFilterSortChangeRef.current?.( + newSidebarFilter, + tableSorter + ) + } else if (listViewIdRef.current) { + // On a view but not editing: track as user override for startEditing + if (userOverridesRef) { + userOverridesRef.current = { + ...(userOverridesRef.current || {}), + filter: newSidebarFilter, + sort: + userOverridesRef.current?.sort ?? + (tableSorterRef.current?.field + ? tableSorterRef.current + : undefined) + } + } + } else { + persistFilter(newSidebarFilter) + } + const effective = buildEffectiveFilter(newSidebarFilter) + activeFilterRef.current = effective + loadPage(initialPage, effective, resolveSorter(tableSorter), { + silent: true + }) }, - [initialPage, loadPage, persistFilter] + [ + assignSidebarToView, + buildEffectiveFilter, + initialPage, + loadPage, + persistFilter, + resolveSorter, + tableSorter, + userOverridesRef + ] + ) + + const handleSidebarSortChange = useCallback( + (newSorter) => { + const nextSorter = newSorter?.field + ? { field: newSorter.field, order: newSorter.order } + : {} + suppressTableChange() + setTableSorter(nextSorter) + tableSorterRef.current = nextSorter + if (activeObjectViewRef.current) { + onObjectViewFilterSortChangeRef.current?.(sidebarFilter, nextSorter) + } else if (listViewIdRef.current) { + // On a view but not editing: track as user override for startEditing + if (userOverridesRef) { + userOverridesRef.current = { + ...(userOverridesRef.current || {}), + sort: nextSorter, + filter: userOverridesRef.current?.filter ?? sidebarFilter + } + } + } else { + persistSort(nextSorter) + } + const effective = buildEffectiveFilter(sidebarFilter) + loadPage(initialPage, effective, resolveSorter(nextSorter), { + silent: true + }) + }, + [ + buildEffectiveFilter, + initialPage, + loadPage, + persistSort, + resolveSorter, + sidebarFilter, + suppressTableChange, + userOverridesRef + ] ) const modelProperties = getModelProperties(type) // Table columns from model properties const columnsWithSkeleton = [ { - title: lazyLoading ? : cards ? model.icon : null, + title: + loading || lazyLoading ? ( +
+ +
+ ) : canBulkSelect ? ( +
+ 0 + } + onChange={(event) => toggleSelectAll(event.target.checked)} + aria-label={`Select all ${model.labelPlural || model.label}`} + /> +
+ ) : isCards ? ( + model.icon + ) : null, key: 'icon', width: 45, fixed: 'left', - render: () => { - return {createElement(model.icon)} + render: (_, record) => { + const selected = + bulkSelection?.allMatching === true || + selectedRowIdSet.has(String(record?._id)) + + if (!canBulkSelect || record?.isSkeleton) { + return {createElement(model.icon)} + } + + return ( + +
+ event.stopPropagation()} + onChange={(event) => + toggleRowSelection(record, event.target.checked) + } + className='object-table-row-checkbox' + aria-label={`Select ${record?.name || record?._reference || record?._id}`} + /> +
+ {createElement(model.icon)} +
+
+
+ ) } } ] @@ -972,7 +1924,7 @@ const ObjectTable = forwardRef( }, [pages]) useEffect(() => { - if (!expandHeight || cards) return + if (!expandHeight || isCards || isKanban) return const findAntTableBody = (element) => { if (!element) return null @@ -993,7 +1945,14 @@ const ObjectTable = forwardRef( return () => { tableBody.style.minHeight = '' } - }, [expandHeight, adjustedScrollHeight, cards, loading, tableData]) + }, [ + expandHeight, + adjustedScrollHeight, + isCards, + isKanban, + loading, + tableData + ]) // Add columns in the order specified by model.columns model.columns.forEach((colName) => { @@ -1044,7 +2003,11 @@ const ObjectTable = forwardRef( fixed: isMobile ? undefined : fixed, key: prop.name, filterIcon: () => { - return + return ( + + + + ) }, render: (text, record) => { if (record?.isSkeleton) { @@ -1068,23 +2031,29 @@ const ObjectTable = forwardRef( } } - if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) { + if ( + isFilterable && + !Object.keys(resolvedMasterFilter).includes(prop.name) + ) { columnConfig.filterDropdown = ({ setSelectedKeys, selectedKeys, confirm, - clearFilters + clearFilters, + visible }) => getFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, - propertyName: prop.label + visible, + propertyName: prop.name, + propertyLabel: prop.label }) - const sidebarVal = sidebarFilter[prop.name] - columnConfig.filteredValue = - sidebarVal !== undefined && sidebarVal !== '' ? [sidebarVal] : null + columnConfig.filteredValue = fromFilterExpression( + effectiveFilter[prop.name] + ) } columnsWithSkeleton.push(columnConfig) @@ -1116,7 +2085,11 @@ const ObjectTable = forwardRef( const cardsContainerNodeRef = useRef(null) const cardsResizeObserverRef = useRef(null) - const measureCardColSpan = useCallback(() => { + const measureCardColSpan = useCallback((sizes) => { + if (sizes?.[1]) { + setSidebarWidth(sizes[1]) + } + const node = cardsContainerNodeRef.current if (!node) return const panel = node.closest('.ant-splitter-panel') @@ -1159,9 +2132,9 @@ const ObjectTable = forwardRef( ) useLayoutEffect(() => { - if (!cards) return + if (!isCards) return measureCardColSpan() - }, [cards, showFilterSidebar, measureCardColSpan]) + }, [isCards, showFilterSidebar, showSortSidebar, measureCardColSpan]) useEffect(() => { return () => cardsResizeObserverRef.current?.disconnect() @@ -1169,7 +2142,7 @@ const ObjectTable = forwardRef( // Card view: load when the leading skeleton card in a boundary page enters view useLayoutEffect(() => { - if (!cards) return + if (!isCards) return const container = cardsContainerNodeRef.current if (!container) return const scrollEl = container.querySelector('.simplebar-content-wrapper') @@ -1199,17 +2172,19 @@ const ObjectTable = forwardRef( .forEach((el) => observer.observe(el)) return () => observer.disconnect() - }, [cards, skeletonBoundaryIds, loadNextPage, loadPreviousPage]) + }, [isCards, skeletonBoundaryIds, loadNextPage, loadPreviousPage]) const renderCards = () => { return (
- + {tableData.map((record) => { if (record?._id == undefined) { return null @@ -1240,6 +2215,7 @@ const ObjectTable = forwardRef( modelProperties={modelProperties} visibleColumns={visibleColumns} record={record} + lazyLoading={lazyLoading} isEditing={isEditing} rowActions={rowActions} renderActions={renderActions} @@ -1251,6 +2227,22 @@ const ObjectTable = forwardRef( ) })} + {!lazyLoading && tableData.length <= 0 && ( + + )} + {lazyLoading && tableData.length <= 0 && ( + + )}
) @@ -1274,69 +2266,204 @@ const ObjectTable = forwardRef( [isEditing, registerForm] ) + const getRowClassName = useCallback( + (record) => + classNames({ + 'object-table-row-selected': + canBulkSelect && + !record?.isSkeleton && + (bulkSelection?.allMatching === true || + selectedRowIdSet.has(String(record?._id))) + }), + [bulkSelection?.allMatching, canBulkSelect, selectedRowIdSet] + ) + + useEffect(() => { + const container = tableContentRef.current + + if (!container) return + + const updateVisibility = () => { + const scrollableY = container.querySelector('.simplebar-scrollable-y') + + const scrollableX = container.querySelector('.simplebar-scrollable-x') + + const scrollbar = container.querySelector( + '.simplebar-vertical .simplebar-scrollbar' + ) + + setVerticalScrollVisible( + scrollbar?.classList.contains('simplebar-visible') || + scrollableY?.classList.contains('simplebar-mouse-entered') || + scrollableX?.classList.contains('simplebar-mouse-entered') + ) + } + + // Initial state + updateVisibility() + + const observer = new MutationObserver(updateVisibility) + + observer.observe(container, { + subtree: true, + attributes: true, + attributeFilter: ['class'] + }) + + return () => { + observer.disconnect() + } + }, []) + const tableContent = ( - {cards ? ( + {isKanban ? ( +
+ + { + objectListView?.setViewMode?.({ + ...viewMode, + settings: { + ...viewMode.settings, + columns + } + }) + }} + /> + +
+ ) : isTimeline ? ( +
+ + ( + + {row} + + )} + /> + +
+ ) : isCards ? (
- } - spinning={loading} - style={{ height: '100%' }} - > - {renderCards()} - + {renderCards()}
) : ( - - }} - onScroll={handleScroll} - onChange={handleTableChange} - showSorterTooltip={false} - style={{ height: '100%' }} - size={size} - components={components} - onRow={onRow} - /> + +
+ )} - {showFilterSidebar && ( - - + {(showSortSidebar || showFilterSidebar) && initialized && ( + + + {showSortSidebar && ( + + )} + {showFilterSidebar && ( + + + + )} + )} ) - return tableContent + return ( + + {tableContent} + + ) } ) @@ -1348,6 +2475,7 @@ ObjectTable.propTypes = { scrollHeight: PropTypes.string, onDataChange: PropTypes.func, initialPage: PropTypes.number, + viewMode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), cards: PropTypes.bool, cardRenderer: PropTypes.func, visibleColumns: PropTypes.object, @@ -1355,6 +2483,7 @@ ObjectTable.propTypes = { size: PropTypes.string, onStateChange: PropTypes.func, showFilterSidebar: PropTypes.bool, + showSortSidebar: PropTypes.bool, expandHeight: PropTypes.bool, showActions: PropTypes.bool, saveFilterInSession: PropTypes.bool, diff --git a/src/components/Dashboard/common/ObjectTableFilterContext.jsx b/src/components/Dashboard/common/ObjectTableFilterContext.jsx new file mode 100644 index 00000000..d3d47dae --- /dev/null +++ b/src/components/Dashboard/common/ObjectTableFilterContext.jsx @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export const ObjectTableFilterContext = createContext(null) diff --git a/src/components/Dashboard/common/ObjectTableNavigationButtons.jsx b/src/components/Dashboard/common/ObjectTableNavigationButtons.jsx index e79eea32..f7251fd4 100644 --- a/src/components/Dashboard/common/ObjectTableNavigationButtons.jsx +++ b/src/components/Dashboard/common/ObjectTableNavigationButtons.jsx @@ -1,16 +1,18 @@ import { useContext, useEffect, useState, useCallback, useRef } from 'react' import { Button, Divider, Flex, Space } from 'antd' import { useSearchParams } from 'react-router-dom' -import ArrowLeftIcon from '../../Icons/ArrowLeftIcon.jsx' -import ArrowRightIcon from '../../Icons/ArrowRightIcon.jsx' + import PropTypes from 'prop-types' import { ApiServerContext } from '../context/ApiServerContext' import { AuthContext } from '../context/AuthContext' import { getActiveFilterValues, + useTableState, useTableStatePersistence } from '../context/TableStateContext' import KeyboardShortcut from './KeyboardShortcut.jsx' +import ChevronLeftIcon from '../../Icons/ChevronLeftIcon.jsx' +import ChevronRightIcon from '../../Icons/ChevronRightIcon.jsx' const ObjectTableNavigationButtons = ({ disabled, @@ -24,15 +26,26 @@ const ObjectTableNavigationButtons = ({ const [neighbors, setNeighbors] = useState({ next: null, previous: null }) const [loading, setLoading] = useState(false) + const { getObjectListFilter, getObjectListSorter, getObjectListView } = + useTableState() const { getPersistedFilter, getPersistedSorter } = useTableStatePersistence({ - scope: objectType, - useFilterInSession: true, - useSortInSession: true - }) + scope: objectType, + useFilterInSession: true, + useSortInSession: true + }) const getObjectNeighborsRef = useRef(getObjectNeighbors) getObjectNeighborsRef.current = getObjectNeighbors + const getObjectListFilterRef = useRef(getObjectListFilter) + getObjectListFilterRef.current = getObjectListFilter + + const getObjectListSorterRef = useRef(getObjectListSorter) + getObjectListSorterRef.current = getObjectListSorter + + const getObjectListViewRef = useRef(getObjectListView) + getObjectListViewRef.current = getObjectListView + const getPersistedFilterRef = useRef(getPersistedFilter) getPersistedFilterRef.current = getPersistedFilter @@ -50,8 +63,26 @@ const ObjectTableNavigationButtons = ({ const loadNeighbors = async () => { setLoading(true) - const filter = getActiveFilterValues(getPersistedFilterRef.current()) - const sorter = getPersistedSorterRef.current() + // When a view is active, use its effective filter exclusively — never + // fall back to the All-tab persisted filter which belongs to a different + // context and would produce wrong neighbors. + const hasActiveView = !!getObjectListViewRef.current(objectType) + const listFilter = getActiveFilterValues( + getObjectListFilterRef.current(objectType) + ) + const persistedFilter = hasActiveView + ? {} + : getActiveFilterValues(getPersistedFilterRef.current()) + const filter = + Object.keys(listFilter).length > 0 ? listFilter : persistedFilter + + const listSorter = getObjectListSorterRef.current(objectType) + const persistedSorter = hasActiveView + ? {} + : getPersistedSorterRef.current() + const sorter = + listSorter?.field && listSorter?.order ? listSorter : persistedSorter + const result = await getObjectNeighborsRef.current(objectType, { id: _id, filter, @@ -84,7 +115,7 @@ const ObjectTableNavigationButtons = ({ } } - setSearchParams(nextParams) + setSearchParams(nextParams, { replace: true }) }, [searchParams, setSearchParams, objectType] ) @@ -114,15 +145,21 @@ const ObjectTableNavigationButtons = ({ {showEndingDivider && ( )} - + + + + + + + ) + + if (showEndingDivider) { + return ( + + + {content} + + ) + } + return content +} ObjectTableViewButton.propTypes = { - viewMode: PropTypes.oneOf(['list', 'cards']).isRequired, - setViewMode: PropTypes.func.isRequired + objectType: PropTypes.string.isRequired, + viewMode: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + setViewMode: PropTypes.func, + showEndingDivider: PropTypes.bool } export default ObjectTableViewButton diff --git a/src/components/Dashboard/common/ObjectTimeline.jsx b/src/components/Dashboard/common/ObjectTimeline.jsx new file mode 100644 index 00000000..0461c3c6 --- /dev/null +++ b/src/components/Dashboard/common/ObjectTimeline.jsx @@ -0,0 +1,354 @@ +import { + useContext, + useEffect, + useMemo, + useRef, + useState, + useCallback +} from 'react' +import PropTypes from 'prop-types' +import { Flex, Typography, Card, Divider } from 'antd' +import { ApiServerContext } from '../context/ApiServerContext' +import ScrollBox from './ScrollBox' +import MissingPlaceholder from './MissingPlaceholder' +import LoadingPlaceholder from './LoadingPlaceholder' +import ObjectTimelineRow from './ObjectTimelineRow' +import { getTimelineRangeFromValues, getTimelineTicks } from './timelineUtils' +import cn from 'classnames' +import { getStateTagInfo } from '../utils/Utils' +import { AuthContext } from '../context/AuthContext' + +const { Text } = Typography +const LABEL_WIDTH = 226 +const SHADOW_PADDING = 12 +const MIN_TICK_SECTION_WIDTH = 140 + +const ObjectTimeline = ({ + type, + records, + model, + startDate, + endDate, + colorProperty, + filter = {}, + masterFilter = {}, + isEditing = false, + rowActions = [], + renderActions, + lazyLoading = false, + loading = false, + skeletonBoundaryIds = {}, + onScroll, + rowWrapper +}) => { + const { getModelPropertyValues, connected } = useContext(ApiServerContext) + const { token } = useContext(AuthContext) + const getModelPropertyValuesRef = useRef(getModelPropertyValues) + getModelPropertyValuesRef.current = getModelPropertyValues + + const [startValues, setStartValues] = useState([]) + const [endValues, setEndValues] = useState([]) + + const rangeQueryKey = useMemo( + () => + JSON.stringify({ + type, + startDate, + endDate, + filter, + masterFilter + }), + [type, startDate, endDate, filter, masterFilter] + ) + + useEffect(() => { + if (connected !== true || !token) return + + if (!type || !startDate) { + setStartValues([]) + setEndValues([]) + return undefined + } + + let cancelled = false + const { filter: activeFilter, masterFilter: activeMasterFilter } = + JSON.parse(rangeQueryKey) + + const loadRangeValues = async () => { + try { + const startPromise = getModelPropertyValuesRef.current( + type, + startDate, + { + filter: activeFilter, + masterFilter: activeMasterFilter + } + ) + const endPromise = endDate + ? getModelPropertyValuesRef.current(type, endDate, { + filter: activeFilter, + masterFilter: activeMasterFilter + }) + : Promise.resolve([]) + const [nextStartValues, nextEndValues] = await Promise.all([ + startPromise, + endPromise + ]) + if (cancelled) return + setStartValues(Array.isArray(nextStartValues) ? nextStartValues : []) + setEndValues(Array.isArray(nextEndValues) ? nextEndValues : []) + } catch (error) { + if (cancelled) return + console.error('Error fetching timeline date values:', error) + setStartValues([]) + setEndValues([]) + } + } + + loadRangeValues() + return () => { + cancelled = true + } + }, [endDate, rangeQueryKey, startDate, type, connected, token]) + + const [showShadow, setShowShadow] = useState(false) + + const handleScroll = useCallback( + (event) => { + const { scrollLeft } = event.target + setShowShadow(scrollLeft > 0) + onScroll?.(event) + }, + [onScroll] + ) + + const timelineRange = useMemo( + () => getTimelineRangeFromValues(startValues, endValues), + [endValues, startValues] + ) + const lastTimelineRangeRef = useRef(null) + if (timelineRange) { + lastTimelineRangeRef.current = timelineRange + } + + const range = useMemo( + () => + timelineRange || + lastTimelineRangeRef.current || + getTimelineRangeFromValues([new Date()]), + [timelineRange] + ) + + const calculatedLabelWidth = useMemo(() => { + return LABEL_WIDTH + rowActions.length * 32 + }, [rowActions.length]) + + const ticks = useMemo(() => getTimelineTicks(range.start, range.end), [range]) + const tickCount = Math.max(1, ticks.length - 1) + const minTrackWidth = MIN_TICK_SECTION_WIDTH * tickCount + const minContentWidth = calculatedLabelWidth + minTrackWidth + + const nameProperty = model.properties?.find( + (property) => property.name === 'name' + ) + + const referenceProperty = model.properties?.find( + (property) => property.name === '_reference' + ) + + const colorPropertyObject = model.properties?.find( + (property) => property.name === colorProperty + ) + + if (!startDate) { + return ( + + ) + } + + if (!loading && !lazyLoading && records.length === 0) { + return ( + + ) + } + + if (records.length === 0) { + return ( + + ) + } + + return ( + +
+ +
+ +
+ + + + {nameProperty?.label + ? nameProperty?.label + : referenceProperty?.label} + + +
+
+ {ticks.map((tick, index) => { + return ( + + {tick.label} + + ) + })} +
+
+ {records.map((record) => { + const skeletonBoundary = + record._id === skeletonBoundaryIds.next + ? 'next' + : record._id === skeletonBoundaryIds.previous + ? 'previous' + : undefined + var color = 'var(--color-primary, #1677ff)' + if (colorPropertyObject) { + if ( + colorPropertyObject.type === 'state' && + record[colorPropertyObject.name]?.type + ) { + console.log('GOT STATE') + color = `var(--color-${getStateTagInfo(record[colorPropertyObject.name].type).status}, #1677ff)` + } else { + color = record[colorPropertyObject.name] + } + } + const row = ( + + ) + return rowWrapper ? rowWrapper(record, row) : row + })} +
+
+
+
+ ) +} + +ObjectTimeline.propTypes = { + type: PropTypes.string, + records: PropTypes.array.isRequired, + model: PropTypes.object.isRequired, + startDate: PropTypes.string, + endDate: PropTypes.string, + colorProperty: PropTypes.string, + filter: PropTypes.object, + masterFilter: PropTypes.object, + isEditing: PropTypes.bool, + rowActions: PropTypes.array, + renderActions: PropTypes.func.isRequired, + lazyLoading: PropTypes.bool, + loading: PropTypes.bool, + skeletonBoundaryIds: PropTypes.object, + onScroll: PropTypes.func, + rowWrapper: PropTypes.func +} + +export default ObjectTimeline diff --git a/src/components/Dashboard/common/ObjectTimelineItem.jsx b/src/components/Dashboard/common/ObjectTimelineItem.jsx new file mode 100644 index 00000000..31c004e8 --- /dev/null +++ b/src/components/Dashboard/common/ObjectTimelineItem.jsx @@ -0,0 +1,119 @@ +import PropTypes from 'prop-types' +import dayjs from 'dayjs' +import { Flex, Typography } from 'antd' +import classNames from 'classnames' +import { getTimelineItemPosition } from './timelineUtils' + +const { Text } = Typography + +const ObjectTimelineItem = ({ + label, + startValue, + endValue, + rangeStart, + rangeEnd, + labelIsReference = false, + isSkeleton = false, + color = 'var(--color-primary, #1677ff)' +}) => { + if (isSkeleton) { + return ( +
+ ) + } + + const position = getTimelineItemPosition( + startValue, + endValue, + rangeStart, + rangeEnd + ) + if (!position) { + return No valid start date + } + + const startLabel = dayjs(startValue).format('YYYY-MM-DD HH:mm') + const endLabel = + !position.isPoint && endValue && dayjs(endValue).isValid() + ? dayjs(endValue).format('YYYY-MM-DD HH:mm') + : null + + return ( + +
+ + {label} + +
+
+ ) +} + +ObjectTimelineItem.propTypes = { + label: PropTypes.string, + color: PropTypes.string, + startValue: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Date) + ]), + endValue: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + rangeStart: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date) + ]).isRequired, + rangeEnd: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date) + ]).isRequired, + isSkeleton: PropTypes.bool, + labelIsReference: PropTypes.bool +} + +export default ObjectTimelineItem diff --git a/src/components/Dashboard/common/ObjectTimelineRow.jsx b/src/components/Dashboard/common/ObjectTimelineRow.jsx new file mode 100644 index 00000000..c22a9d0e --- /dev/null +++ b/src/components/Dashboard/common/ObjectTimelineRow.jsx @@ -0,0 +1,160 @@ +import { createElement } from 'react' +import PropTypes from 'prop-types' +import { Flex, Skeleton } from 'antd' +import { getPropertyValue } from '../../../database/ObjectModels' +import ObjectProperty from './ObjectProperty' +import ObjectTimelineItem from './ObjectTimelineItem' + +const ObjectTimelineRow = ({ + model, + record, + startDate, + endDate, + rangeStart, + rangeEnd, + labelWidth, + minTrackWidth, + tickCount, + isEditing = false, + rowActions = [], + renderActions, + lazyLoading = false, + skeletonBoundary, + color = 'var(--color-primary, #1677ff)', + shadowPadding = 12 +}) => { + const isSkeleton = record?.isSkeleton === true + const nameProperty = model.properties?.find( + (property) => property.name === 'name' + ) + const referenceProperty = model.properties?.find( + (property) => property.name === '_reference' + ) + const nameValue = getPropertyValue(record, 'name') + const modelPrefix = model.prefix + const label = + typeof nameValue === 'string' + ? nameValue + : modelPrefix + ':' + String(record?._reference || '') + const startValue = getPropertyValue(record, startDate) + const endValue = endDate ? getPropertyValue(record, endDate) : undefined + const actions = + !isSkeleton && rowActions.length > 0 + ? renderActions(record, lazyLoading) + : null + + return ( + +
+ + {isSkeleton ? ( + + ) : ( + <> + + {createElement(model.icon, { + style: { fontSize: 14, flex: '0 0 auto' } + })} + + + {nameProperty ? ( + + ) : ( + + )} + + {actions} + + )} + +
+ + + +
+ ) +} + +ObjectTimelineRow.propTypes = { + model: PropTypes.object.isRequired, + record: PropTypes.object.isRequired, + color: PropTypes.string, + startDate: PropTypes.string.isRequired, + endDate: PropTypes.string, + rangeStart: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + rangeEnd: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + labelWidth: PropTypes.number.isRequired, + minTrackWidth: PropTypes.number.isRequired, + tickCount: PropTypes.number.isRequired, + isEditing: PropTypes.bool, + rowActions: PropTypes.array, + renderActions: PropTypes.func.isRequired, + lazyLoading: PropTypes.bool, + skeletonBoundary: PropTypes.string, + shadowPadding: PropTypes.number +} + +export default ObjectTimelineRow diff --git a/src/components/Dashboard/common/PDFPreview.jsx b/src/components/Dashboard/common/PDFPreview.jsx new file mode 100644 index 00000000..aa58a94b --- /dev/null +++ b/src/components/Dashboard/common/PDFPreview.jsx @@ -0,0 +1,87 @@ +import { useState } from 'react' +import PropTypes from 'prop-types' +import { Flex, Button } from 'antd' +import PlusIcon from '../../Icons/PlusIcon.jsx' +import MinusIcon from '../../Icons/MinusIcon.jsx' +import PanIcon from '../../Icons/PanIcon.jsx' +import PanFilledIcon from '../../Icons/PanFilledIcon.jsx' +import PDFViewer from './PDFViewer.jsx' +import { clampPreviewScale } from '../hooks/usePinchZoom.js' + +const PDFPreview = ({ file, loading = false, style }) => { + const [previewScale, setPreviewScale] = useState(1) + const [panMode, setPanMode] = useState(false) + + return ( + + + +
+ ) +} + +PDFViewer.propTypes = { + file: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Blob), + PropTypes.instanceOf(ArrayBuffer), + PropTypes.object + ]), + scale: PropTypes.number, + panMode: PropTypes.bool, + loading: PropTypes.bool, + onScaleChange: PropTypes.func, + onLoadSuccess: PropTypes.func, + onLoadError: PropTypes.func +} + +export default PDFViewer diff --git a/src/components/Dashboard/common/PermissionCheckbox.jsx b/src/components/Dashboard/common/PermissionCheckbox.jsx new file mode 100644 index 00000000..d831c72a --- /dev/null +++ b/src/components/Dashboard/common/PermissionCheckbox.jsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types' +import { theme } from 'antd' +import CheckIcon from '../../Icons/CheckIcon' +import XMarkIcon from '../../Icons/XMarkIcon' + +const ICON_STYLE = { + fontSize: 7, + color: '#ffffff' +} + +const PermissionCheckbox = ({ + checked = false, + indeterminate = false, + mixed = false, + disabled = false, + onChange +}) => { + const { token } = theme.useToken() + const state = mixed + ? 'mixed' + : indeterminate + ? 'inherit' + : checked + ? 'allow' + : 'deny' + + return ( + + ) +} + +PermissionCheckbox.propTypes = { + checked: PropTypes.bool, + indeterminate: PropTypes.bool, + mixed: PropTypes.bool, + disabled: PropTypes.bool, + onChange: PropTypes.func +} + +export default PermissionCheckbox diff --git a/src/components/Dashboard/common/PermissionsMatrix.jsx b/src/components/Dashboard/common/PermissionsMatrix.jsx new file mode 100644 index 00000000..713c4de0 --- /dev/null +++ b/src/components/Dashboard/common/PermissionsMatrix.jsx @@ -0,0 +1,750 @@ +import { + useMemo, + useCallback, + useState, + createElement, + useContext, + useRef, + useEffect, + useLayoutEffect +} from 'react' +import PropTypes from 'prop-types' +import { Table, Flex, Button } from 'antd' +import Tooltip from './Tooltip' +import { useMediaQuery } from 'react-responsive' +import { ElectronContext } from '../context/ElectronContext' +import { + getPermissionMatrixModels, + getPermissionMatrixActions, + modelHasPermissionAction +} from '../../../database/ObjectModels' +import { getSidebar, getSidebarMenuSections } from '../../../database/Sidebars' +import { getSidebarIconNode } from '../../Icons/sidebarIconMap' +import PermissionCheckbox from './PermissionCheckbox' +import { CaretRightOutlined } from '@ant-design/icons' + +const DEFAULT_SCROLL_HEIGHT = 'calc(var(--unit-100vh) - 258px)' +const PERMISSIONS_MATRIX_SESSION_KEY = 'permissionsMatrix:ui' + +const readPermissionsMatrixSession = () => { + try { + const parsed = JSON.parse( + sessionStorage.getItem(PERMISSIONS_MATRIX_SESSION_KEY) + ) + if (!parsed || typeof parsed !== 'object') return {} + return parsed + } catch { + return {} + } +} + +const writePermissionsMatrixSession = (patch) => { + try { + sessionStorage.setItem( + PERMISSIONS_MATRIX_SESSION_KEY, + JSON.stringify({ ...readPermissionsMatrixSession(), ...patch }) + ) + } catch { + // sessionStorage may be unavailable + } +} + +const getColumnIndexProps = (columnIndex) => ({ + 'data-col': String(columnIndex), + style: { '--permissions-col': String(columnIndex) } +}) + +const getPermissionState = (permissions, modelName, actionName) => { + const value = permissions?.[modelName]?.[actionName] + if (value === true) return true + if (value === false) return false + return null +} + +const setPermissionState = (permissions, modelName, actionName, next) => { + const current = + permissions && typeof permissions === 'object' ? permissions : {} + const modelPermissions = { + ...(current[modelName] && typeof current[modelName] === 'object' + ? current[modelName] + : {}) + } + + if (next === null) { + delete modelPermissions[actionName] + } else { + modelPermissions[actionName] = next + } + + const nextPermissions = { ...current } + if (Object.keys(modelPermissions).length === 0) { + delete nextPermissions[modelName] + } else { + nextPermissions[modelName] = modelPermissions + } + return nextPermissions +} + +const cyclePermissionState = (current, inherit) => { + if (!inherit) { + return current === true ? false : true + } + if (current === true) return false + if (current === false) return null + return true +} + +const normalizePermissionState = (state, inherit) => { + if (!inherit && state === null) return false + return state +} + +const getAggregatePermissionState = (states) => { + if (states.length === 0) return null + const first = states[0] + for (let i = 1; i < states.length; i++) { + if (states[i] !== first) return 'mixed' + } + return first +} + +const getRowPermissionStates = (permissions, model, actions, inherit) => + actions + .filter((action) => modelHasPermissionAction(model, action.name)) + .map((action) => + normalizePermissionState( + getPermissionState(permissions, model.name, action.name), + inherit + ) + ) + +const getColumnPermissionStates = (permissions, models, action, inherit) => + models + .filter((model) => modelHasPermissionAction(model, action.name)) + .map((model) => + normalizePermissionState( + getPermissionState(permissions, model.name, action.name), + inherit + ) + ) + +const setRowPermissionState = (permissions, model, actions, next) => { + let nextPermissions = permissions + actions.forEach((action) => { + if (modelHasPermissionAction(model, action.name)) { + nextPermissions = setPermissionState( + nextPermissions, + model.name, + action.name, + next + ) + } + }) + return nextPermissions +} + +const setColumnPermissionState = (permissions, models, action, next) => { + let nextPermissions = permissions + models.forEach((model) => { + if (modelHasPermissionAction(model, action.name)) { + nextPermissions = setPermissionState( + nextPermissions, + model.name, + action.name, + next + ) + } + }) + return nextPermissions +} + +const getCheckboxTitle = (state, inherit) => { + if (state === 'mixed') return 'Mixed' + if (inherit && state === null) return 'Inherit' + if (state === true) return 'Allow' + return 'Deny' +} + +const normalizePath = (path) => { + if (!path || typeof path !== 'string') return '' + return path.split('?')[0].replace(/\/+$/, '') +} + +const collectSidebarMeta = (items, acc = { paths: [], iconKeys: [] }) => { + items.forEach((item) => { + if (item?.type === 'divider') return + if (item.path) acc.paths.push(normalizePath(item.path)) + if (item.iconKey) acc.iconKeys.push(item.iconKey) + if (item.children?.length) collectSidebarMeta(item.children, acc) + }) + return acc +} + +const getModelPaths = (model) => { + const paths = [] + const addPath = (url) => { + if (typeof url === 'string') { + paths.push(normalizePath(url)) + return + } + if (typeof url === 'function') { + try { + const sample = url('') + if (typeof sample === 'string') { + paths.push(normalizePath(sample.split('?')[0])) + } + } catch { + // ignore uncallable url builders + } + } + } + + addPath(model?.url) + ;(model?.actions || []).forEach((action) => { + if (action?.type === 'alias' || action?.type === 'callback') return + addPath(action?.url) + }) + return [...new Set(paths.filter(Boolean))] +} + +const pathMatches = (modelPath, sidebarPath) => + modelPath === sidebarPath || modelPath.startsWith(`${sidebarPath}/`) + +const getModelSectionOrder = (model, section) => { + const modelPaths = getModelPaths(model) + const pathIndex = section.paths.findIndex((sidebarPath) => + modelPaths.some((modelPath) => pathMatches(modelPath, sidebarPath)) + ) + if (pathIndex !== -1) return pathIndex + const iconIndex = section.iconKeys.findIndex( + (iconKey) => iconKey === model.name + ) + if (iconIndex !== -1) return iconIndex + return Number.MAX_SAFE_INTEGER +} + +const modelMatchesSection = (model, section) => { + const modelPaths = getModelPaths(model) + if ( + modelPaths.some((modelPath) => + section.paths.some((sidebarPath) => pathMatches(modelPath, sidebarPath)) + ) + ) { + return true + } + if (section.iconKeys.includes(model.name)) return true + const prefix = `/dashboard/${section.key}` + return modelPaths.some((modelPath) => pathMatches(modelPath, prefix)) +} + +const getRecordModels = (record) => { + if (record?.isGroup) { + return (record.children || []).map((child) => child.model).filter(Boolean) + } + return record?.model ? [record.model] : [] +} + +const getModelsRowPermissionStates = (permissions, models, actions, inherit) => + models.flatMap((model) => + getRowPermissionStates(permissions, model, actions, inherit) + ) + +const setModelsRowPermissionState = (permissions, models, actions, next) => { + let nextPermissions = permissions + models.forEach((model) => { + nextPermissions = setRowPermissionState( + nextPermissions, + model, + actions, + next + ) + }) + return nextPermissions +} + +const buildPermissionMatrixDataSource = (models) => { + const assigned = new Set() + const sections = getSidebarMenuSections({ includeDev: false }).map( + (section) => { + const sidebar = getSidebar(section.key) + const meta = collectSidebarMeta(section.items || []) + Object.keys(sidebar?.routeAliases || {}).forEach((path) => { + meta.paths.push(normalizePath(path)) + }) + return { + key: section.key, + label: section.label, + iconKey: section.iconKey, + paths: meta.paths, + iconKeys: meta.iconKeys + } + } + ) + + const dataSource = [] + sections.forEach((section) => { + const sectionModels = models + .filter( + (model) => + !assigned.has(model.name) && modelMatchesSection(model, section) + ) + .sort( + (a, b) => + getModelSectionOrder(a, section) - getModelSectionOrder(b, section) + ) + + sectionModels.forEach((model) => assigned.add(model.name)) + if (sectionModels.length === 0) return + + dataSource.push({ + key: `section-${section.key}`, + isGroup: true, + label: section.label, + iconNode: getSidebarIconNode(section.iconKey), + children: sectionModels.map((model) => ({ + key: model.name, + label: model.label, + icon: model.icon, + model + })) + }) + }) + + const leftover = models.filter((model) => !assigned.has(model.name)) + if (leftover.length > 0) { + dataSource.push({ + key: 'section-other', + isGroup: true, + label: 'Other', + iconNode: null, + children: leftover.map((model) => ({ + key: model.name, + label: model.label, + icon: model.icon, + model + })) + }) + } + + return dataSource +} + +const PermissionsMatrix = ({ + value, + onChange, + disabled = false, + inherit = true, + scrollHeight = DEFAULT_SCROLL_HEIGHT, + size = 'middle' +}) => { + const { isElectron } = useContext(ElectronContext) + const isMobile = useMediaQuery({ maxWidth: 768 }) + const models = useMemo(() => getPermissionMatrixModels(), []) + const actions = useMemo( + () => + getPermissionMatrixActions().filter( + (action) => action.type !== 'alias' && action.type !== 'callback' + ), + [] + ) + const permissions = useMemo( + () => (value && typeof value === 'object' ? value : {}), + [value] + ) + const dataSource = useMemo( + () => buildPermissionMatrixDataSource(models), + [models] + ) + const [expandedRowKeys, setExpandedRowKeys] = useState(() => { + const stored = readPermissionsMatrixSession().expandedRowKeys + return Array.isArray(stored) ? stored : [] + }) + const matrixRef = useRef(null) + const scrollPositionRef = useRef(null) + if (scrollPositionRef.current === null) { + const storedScroll = readPermissionsMatrixSession().scroll + scrollPositionRef.current = { + left: Number(storedScroll?.left) || 0, + top: Number(storedScroll?.top) || 0 + } + } + const hoveredColumnIndexRef = useRef(null) + + const persistExpandedRowKeys = useCallback((keys) => { + setExpandedRowKeys(keys) + writePermissionsMatrixSession({ expandedRowKeys: keys }) + }, []) + + const handleMatrixMouseOver = useCallback((event) => { + const cell = event.target.closest('.ant-table-cell') + if (!cell || !event.currentTarget.contains(cell)) return + const next = cell.getAttribute('data-col') + if (!next || hoveredColumnIndexRef.current === next) return + hoveredColumnIndexRef.current = next + event.currentTarget.style.setProperty('--permissions-hovered-col', next) + }, []) + + const handleMatrixMouseLeave = useCallback((event) => { + hoveredColumnIndexRef.current = null + event.currentTarget.style.removeProperty('--permissions-hovered-col') + }, []) + + const resolvedExpandedRowKeys = useMemo(() => { + const sectionKeys = new Set(dataSource.map((row) => row.key)) + return expandedRowKeys.filter((key) => sectionKeys.has(key)) + }, [dataSource, expandedRowKeys]) + + const getTableBody = useCallback( + () => matrixRef.current?.querySelector('.ant-table-body') || null, + [] + ) + + const restoreTableScroll = useCallback(() => { + const body = getTableBody() + if (!body) return + body.scrollLeft = scrollPositionRef.current.left + body.scrollTop = scrollPositionRef.current.top + }, [getTableBody]) + + useLayoutEffect(() => { + restoreTableScroll() + }, [restoreTableScroll, resolvedExpandedRowKeys]) + + useEffect(() => { + const body = getTableBody() + if (!body) return + + const handleScroll = (event) => { + const next = { + left: event.currentTarget.scrollLeft, + top: event.currentTarget.scrollTop + } + scrollPositionRef.current = next + writePermissionsMatrixSession({ scroll: next }) + } + + body.addEventListener('scroll', handleScroll, { passive: true }) + return () => body.removeEventListener('scroll', handleScroll) + }, [getTableBody, resolvedExpandedRowKeys]) + + const isSectionExpanded = useCallback( + (record) => resolvedExpandedRowKeys.includes(record.key), + [resolvedExpandedRowKeys] + ) + + const handleToggleSection = useCallback( + (record) => { + if (!record.isGroup) return + persistExpandedRowKeys( + expandedRowKeys.includes(record.key) + ? expandedRowKeys.filter((key) => key !== record.key) + : [...expandedRowKeys, record.key] + ) + }, + [expandedRowKeys, persistExpandedRowKeys] + ) + + const handleExpandedRowsChange = useCallback( + (keys) => { + persistExpandedRowKeys(keys) + }, + [persistExpandedRowKeys] + ) + + var adjustedScrollHeight = scrollHeight + if (isMobile) { + adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)' + } + if (isElectron) { + adjustedScrollHeight = 'calc(var(--unit-100vh) - 238px)' + } + if (isMobile && isElectron) { + adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)' + } + + const handleCycle = useCallback( + (modelName, actionName) => { + if (disabled || typeof onChange !== 'function') return + const current = getPermissionState(permissions, modelName, actionName) + const next = cyclePermissionState(current, inherit) + onChange(setPermissionState(permissions, modelName, actionName, next)) + }, + [disabled, inherit, onChange, permissions] + ) + + const handleCycleModels = useCallback( + (recordModels) => { + if ( + disabled || + typeof onChange !== 'function' || + recordModels.length === 0 + ) { + return + } + const aggregate = getAggregatePermissionState( + getModelsRowPermissionStates( + permissions, + recordModels, + actions, + inherit + ) + ) + const next = cyclePermissionState( + aggregate === 'mixed' ? null : aggregate, + inherit + ) + onChange( + setModelsRowPermissionState(permissions, recordModels, actions, next) + ) + }, + [actions, disabled, inherit, onChange, permissions] + ) + + const handleCycleModelsAction = useCallback( + (recordModels, action) => { + if (disabled || typeof onChange !== 'function') return + const applicable = recordModels.filter((model) => + modelHasPermissionAction(model, action.name) + ) + if (applicable.length === 0) return + const aggregate = getAggregatePermissionState( + getColumnPermissionStates(permissions, applicable, action, inherit) + ) + const next = cyclePermissionState( + aggregate === 'mixed' ? null : aggregate, + inherit + ) + onChange(setColumnPermissionState(permissions, applicable, action, next)) + }, + [disabled, inherit, onChange, permissions] + ) + + const handleCycleColumn = useCallback( + (action) => { + if (disabled || typeof onChange !== 'function') return + const aggregate = getAggregatePermissionState( + getColumnPermissionStates(permissions, models, action, inherit) + ) + const next = cyclePermissionState( + aggregate === 'mixed' ? null : aggregate, + inherit + ) + onChange(setColumnPermissionState(permissions, models, action, next)) + }, + [disabled, inherit, models, onChange, permissions] + ) + + const renderCheckbox = useCallback( + (state, onCycle) => { + const mixed = state === 'mixed' + const checked = state === true + const indeterminate = inherit && state === null + + return ( + + + + + + ) + }, + [disabled, inherit] + ) + + const columns = useMemo(() => { + const modelColumn = { + title: '', + key: 'model', + dataIndex: 'label', + fixed: isMobile ? undefined : 'left', + width: 280, + onHeaderCell: () => getColumnIndexProps(1), + onCell: () => getColumnIndexProps(1), + render: (label, record) => { + const recordModels = getRecordModels(record) + const aggregate = getAggregatePermissionState( + getModelsRowPermissionStates( + permissions, + recordModels, + actions, + inherit + ) + ) + + return ( + + + {record.isGroup ? ( +
+ record.isGroup ? 'permissions-matrix-group-row' : '' + } + expandable={{ + expandedRowKeys: resolvedExpandedRowKeys, + onExpandedRowsChange: handleExpandedRowsChange, + indentSize: 0, + expandIcon: () => null + }} + /> + + ) +} + +PermissionsMatrix.propTypes = { + value: PropTypes.object, + onChange: PropTypes.func, + disabled: PropTypes.bool, + inherit: PropTypes.bool, + scrollHeight: PropTypes.string, + size: PropTypes.string +} + +export default PermissionsMatrix diff --git a/src/components/Dashboard/common/PrinterQueue.jsx b/src/components/Dashboard/common/PrinterQueue.jsx new file mode 100644 index 00000000..3d34fff2 --- /dev/null +++ b/src/components/Dashboard/common/PrinterQueue.jsx @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types' +import ObjectDisplay from './ObjectDisplay' +import { Steps } from 'antd' +import MissingPlaceholder from './MissingPlaceholder' + +const getId = (item) => { + if (!item) return null + if (typeof item === 'string') return item + if (typeof item._id === 'string') return item._id + if (typeof item._id === 'object' && item._id?._id) return item._id._id + return null +} + +const toObject = (item) => { + if (!item) return null + if (typeof item === 'string') return { _id: item } + return item +} + +const PrinterQueue = ({ queue, currentSubJob }) => { + const queueItems = Array.isArray(queue) + ? queue.map(toObject).filter(Boolean) + : [] + const current = toObject(currentSubJob) + const currentId = getId(current) + + const items = [] + if (current && !queueItems.some((item) => getId(item) === currentId)) { + items.push(current) + } + items.push(...queueItems) + + if (items.length === 0) { + return ( + + ) + } + + const currentIndex = currentId + ? items.findIndex((item) => getId(item) === currentId) + : -1 + + return ( + ({ + title: + }))} + /> + ) +} + +PrinterQueue.propTypes = { + queue: PropTypes.array, + currentSubJob: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) +} + +export default PrinterQueue diff --git a/src/components/Dashboard/common/ProgressDisplay.jsx b/src/components/Dashboard/common/ProgressDisplay.jsx new file mode 100644 index 00000000..f1832303 --- /dev/null +++ b/src/components/Dashboard/common/ProgressDisplay.jsx @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types' +import { Flex } from 'antd' +import HProgress from './HProgress' +import ElipsisText from './ElipsisText' + +const ProgressDisplay = ({ + children, + labelPosition = 'top', + labelType, + percent, + status, + showInfo, + style +}) => { + const label = children ? ( + + {children} + + ) : null + + const progress = ( + + ) + + return ( + + {labelPosition === 'bottom' ? ( + <> + {progress} + {label} + + ) : ( + <> + {label} + {progress} + + )} + + ) +} + +ProgressDisplay.propTypes = { + children: PropTypes.node, + labelPosition: PropTypes.oneOf(['top', 'bottom']), + labelType: PropTypes.oneOf(['secondary']), + percent: PropTypes.number, + status: PropTypes.oneOf(['normal', 'exception', 'active', 'success']), + showInfo: PropTypes.bool, + style: PropTypes.object +} + +export default ProgressDisplay diff --git a/src/components/Dashboard/common/PropertyChanges.jsx b/src/components/Dashboard/common/PropertyChanges.jsx index eed74f81..3a25ff89 100644 --- a/src/components/Dashboard/common/PropertyChanges.jsx +++ b/src/components/Dashboard/common/PropertyChanges.jsx @@ -1,7 +1,11 @@ import PropTypes from 'prop-types' import { Descriptions, Typography, Flex } from 'antd' -import { getModelProperty } from '../../../database/ObjectModels' +import { + getModelProperty, + getPropertyValue +} from '../../../database/ObjectModels' import ObjectProperty from './ObjectProperty' +import DiffLabel from './DiffLabel' import ArrowRightIcon from '../../Icons/ArrowRightIcon' const { Text } = Typography @@ -51,6 +55,17 @@ const PropertyChanges = ({ type, value }) => { if (!changeProperty) { return null } + + const oldValue = getPropertyValue(value?.old, key) + const newValue = getPropertyValue(value?.new, key) + const isCodeKey = key === 'code' || changeProperty.type === 'codeBlock' + const isCodeEdit = + isCodeKey && + oldValue != null && + oldValue !== '' && + newValue != null && + newValue !== '' + return ( { style={{ minWidth: 0, width: '100%' }} className='property-changes' > - {value?.old ? ( - - ) : null} - {value?.old && value?.new ? ( - - - - ) : null} - {value?.new ? ( - - ) : null} + ) : ( + <> + {value?.old ? ( + + ) : null} + {value?.old && value?.new ? ( + + + + ) : null} + {value?.new ? ( + + ) : null} + + )} ) diff --git a/src/components/Dashboard/common/QuickPropertyFilters.jsx b/src/components/Dashboard/common/QuickPropertyFilters.jsx new file mode 100644 index 00000000..c9452928 --- /dev/null +++ b/src/components/Dashboard/common/QuickPropertyFilters.jsx @@ -0,0 +1,635 @@ +import { useMemo, useRef, useState } from 'react' +import { + Button, + Card, + DatePicker, + Descriptions, + Dropdown, + Flex, + Input, + Modal, + Typography +} from 'antd' +import PropTypes from 'prop-types' +import dayjs from 'dayjs' +import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' +import InputNumberCal from './InputNumberCal' +import { + getModelByName, + getModelProperties +} from '../../../database/ObjectModels' +import { isDateTimePropertyType } from './filterExpression' +import { CaretRightFilled } from '@ant-design/icons' + +const { Text } = Typography + +const TEXT_TYPES = new Set([ + 'text', + 'email', + 'url', + 'codeBlock', + 'markdown', + 'tags', + 'stringList', + 'country', + 'phone', + 'address', + 'secret', + 'miscId' +]) + +const NUMBER_TYPES = new Set(['number', 'density', 'variance', 'numberList']) + +const BOOL_TYPES = new Set(['bool', 'boolean']) + +const OBJECT_TYPES = new Set([ + 'object', + 'objectList', + 'reference', + 'state', + 'id', + 'objectType' +]) + +const formatFilterValue = (value, property) => { + if (value == null || value === '') return '' + if (typeof value !== 'object') return String(value) + + if (property?.type === 'state') { + return String(value.type ?? value) + } + + if (value.objectType) { + const { prefix } = getModelByName(value.objectType) + if (value._reference != null) return `${prefix}:${value._reference}` + if (value._id != null) return `${prefix}:${value._id}` + } + + if (property?.objectType && value._id != null) { + const { prefix } = getModelByName(property.objectType) + return `${prefix}:${value._id}` + } + + return String(value._id ?? value.type ?? JSON.stringify(value)) +} + +const formatDateFilterValue = (value, property) => { + if (value == null || value === '') return '' + const date = dayjs(value) + if (!date.isValid()) return String(value) + if (property?.type === 'date') { + return `${date.date()} ${date.month() + 1} ${date.year()}` + } + return date.toISOString() +} + +const buildExpression = (operator, rawValue, property) => { + const value = isDateTimePropertyType(property) + ? formatDateFilterValue(rawValue, property) + : formatFilterValue(rawValue, property) + + switch (operator) { + case 'startsWith': + return `${value}*` + case 'notStartsWith': + return `<>${value}*` + case 'contains': + return `*${value}*` + case 'notContains': + return `<>*${value}*` + case 'endsWith': + return `*${value}` + case 'notEndsWith': + return `<>*${value}` + case 'equals': + return value + case 'notEquals': + return `<>${value}` + case 'greaterThan': + return `>${value}` + case 'greaterOrEqual': + return `>=${value}` + case 'lessThan': + return `<${value}` + case 'lessOrEqual': + return `<=${value}` + case 'between': + return value + case 'isTrue': + return 'true' + case 'isFalse': + return 'false' + case 'isEmpty': + return "''" + case 'isNotEmpty': + return "<>''" + default: + return value + } +} + +const getPropertyCategory = (property) => { + const type = property?.type + if (!type) return 'text' + if (BOOL_TYPES.has(type)) return 'boolean' + if (isDateTimePropertyType(property)) return 'date' + if (NUMBER_TYPES.has(type)) return 'number' + if (OBJECT_TYPES.has(type)) return 'object' + if (TEXT_TYPES.has(type)) return 'text' + return 'text' +} + +const getCategoryLabel = (category) => { + switch (category) { + case 'boolean': + return 'Boolean' + case 'number': + return 'Number' + case 'date': + return 'Date' + case 'object': + return 'Object' + case 'text': + default: + return 'Text' + } +} + +const getFilterOptions = (category) => { + switch (category) { + case 'boolean': + return [ + { key: 'isTrue', label: 'Is true' }, + { key: 'isFalse', label: 'Is false' }, + { type: 'divider' }, + { key: 'isEmpty', label: 'Is empty' }, + { key: 'isNotEmpty', label: 'Is not empty' } + ] + case 'number': + return [ + { key: 'equals', label: 'Equals', needsValue: true }, + { key: 'notEquals', label: 'Does not equal', needsValue: true }, + { type: 'divider' }, + { key: 'greaterThan', label: 'Greater than', needsValue: true }, + { + key: 'greaterOrEqual', + label: 'Greater than or equal', + needsValue: true + }, + { key: 'lessThan', label: 'Less than', needsValue: true }, + { + key: 'lessOrEqual', + label: 'Less than or equal', + needsValue: true + }, + { type: 'divider' }, + { key: 'between', label: 'Between', needsRange: true }, + { type: 'divider' }, + { key: 'isEmpty', label: 'Is empty' }, + { key: 'isNotEmpty', label: 'Is not empty' } + ] + case 'date': + return [ + { key: 'equals', label: 'Equals', needsValue: true }, + { type: 'divider' }, + { key: 'lessThan', label: 'Before', needsValue: true }, + { key: 'greaterThan', label: 'After', needsValue: true }, + { key: 'between', label: 'Between', needsRange: true }, + { type: 'divider' }, + { key: 'isEmpty', label: 'Is empty' }, + { key: 'isNotEmpty', label: 'Is not empty' }, + { type: 'divider' }, + { key: 'yesterday', label: 'Yesterday', expression: '-1D' }, + { key: 'today', label: 'Today', expression: 'TODAY' }, + { key: 'tomorrow', label: 'Tomorrow', expression: '+1D' }, + { type: 'divider' }, + { + key: 'this', + label: 'This', + children: [ + { key: 'thisMinute', label: 'This minute', expression: 'Cm' }, + { key: 'thisHour', label: 'This hour', expression: 'CH' }, + { key: 'thisDay', label: 'This day', expression: 'CD' }, + { key: 'thisWeek', label: 'This week', expression: 'CW' }, + { key: 'thisMonth', label: 'This month', expression: 'CM' }, + { key: 'thisYear', label: 'This year', expression: 'CY' } + ] + }, + + { + key: 'last', + label: 'Last', + children: [ + { key: 'last5Minutes', label: '5 mins', expression: '-5m..TODAY' }, + { + key: 'last15Minutes', + label: '15 mins', + expression: '-15m..TODAY' + }, + { key: 'lastMinute', label: '30 mins', expression: '-30m..TODAY' }, + { key: 'lastHour', label: 'Hour', expression: '-1H..TODAY' }, + { + key: 'last12Hours', + label: '12 hours', + expression: '-12H..TODAY' + }, + { type: 'divider' }, + { key: 'lastDay', label: 'Day', expression: '-1D..TODAY' }, + { key: 'last3Days', label: '3 days', expression: '-3D..TODAY' }, + { key: 'lastWeek', label: 'Week', expression: '-1W' }, + { key: 'last7Days', label: '7 days', expression: '-7D..TODAY' }, + { type: 'divider' }, + { key: 'lastMonth', label: 'Month', expression: '-1M' }, + { key: 'last30Days', label: '30 days', expression: '-30D..TODAY' }, + { key: 'last3Months', label: '3 months', expression: '-3M' }, + { key: 'last90Days', label: '90 days', expression: '-90D' }, + { key: 'last6Months', label: '6 months', expression: '-6M' }, + { key: 'last180Days', label: '180 days', expression: '-180D' }, + { type: 'divider' }, + { key: 'lastYear', label: 'Year', expression: '-1Y' }, + { key: 'last2Years', label: '2 years', expression: '-2Y' } + ] + }, + { + key: 'next', + label: 'Next', + children: [ + { key: 'next5Minutes', label: '5 mins', expression: '+5m..TODAY' }, + { + key: 'next15Minutes', + label: '15 mins', + expression: '+15m..TODAY' + }, + { key: 'nextMinute', label: '30 mins', expression: '+30m..TODAY' }, + { key: 'nextHour', label: 'Hour', expression: '+1H..TODAY' }, + { + key: 'next12Hours', + label: '12 hours', + expression: '+12H..TODAY' + }, + { type: 'divider' }, + { key: 'nextDay', label: 'Day', expression: 'Today..+1D' }, + { key: 'next3Days', label: '3 days', expression: 'Today..+3D' }, + { key: 'nextWeek', label: 'Week', expression: '+1W' }, + { key: 'next7Days', label: '7 days', expression: 'Today..+7D' }, + { type: 'divider' }, + { key: 'nextMonth', label: 'Month', expression: '+1M' }, + { key: 'next30Days', label: '30 days', expression: 'Today..+30D' }, + { key: 'next3Months', label: '3 months', expression: '+3M' }, + { key: 'next90Days', label: '90 days', expression: 'Today..+90D' }, + { key: 'next6Months', label: '6 months', expression: '+6M' }, + { + key: 'next180Days', + label: '180 days', + expression: 'Today..+180D' + }, + { type: 'divider' }, + { key: 'nextYear', label: 'Year', expression: '+1Y' }, + { key: 'next2Years', label: '2 years', expression: '+2Y' } + ] + }, + { type: 'divider' }, + { + key: 'fromToday', + label: 'From today', + expression: 'TODAY..' + }, + { + key: 'untilToday', + label: 'Until today', + expression: '..TODAY' + }, + { type: 'divider' }, + { + key: 'yearToDate', + label: 'Year to Date', + expression: 'CY..TODAY' + } + ] + case 'object': + return [ + { key: 'equals', label: 'Equals', needsValue: true }, + { key: 'notEquals', label: 'Does not equal', needsValue: true }, + { type: 'divider' }, + { key: 'isEmpty', label: 'Is empty' }, + { key: 'isNotEmpty', label: 'Is not empty' } + ] + case 'text': + default: + return [ + { key: 'startsWith', label: 'Starts with', needsValue: true }, + { + key: 'notStartsWith', + label: 'Does not start with', + needsValue: true + }, + { type: 'divider' }, + { key: 'contains', label: 'Contains', needsValue: true }, + { key: 'notContains', label: 'Does not contain', needsValue: true }, + { type: 'divider' }, + { key: 'endsWith', label: 'Ends with', needsValue: true }, + { key: 'notEndsWith', label: 'Does not end with', needsValue: true }, + { type: 'divider' }, + { key: 'equals', label: 'Equals', needsValue: true }, + { key: 'notEquals', label: 'Does not equal', needsValue: true }, + { type: 'divider' }, + { key: 'isEmpty', label: 'Is empty' }, + { key: 'isNotEmpty', label: 'Is not empty' } + ] + } +} + +const buildFilterMenuItems = (options, onOptionClick) => + options.map((item) => + item.type === 'divider' + ? item + : { + key: item.key, + label: item.label, + onClick: () => + !item.children || item.children.length === 0 + ? onOptionClick(item) + : undefined, + children: + item.children && item.children.length > 0 + ? buildFilterMenuItems(item.children, onOptionClick) + : undefined + } + ) + +const QuickPropertyFilters = ({ + modelType, + propertyName, + value = [], + onChange, + onModalOpenChange, + useCard = true +}) => { + const [activeOption, setActiveOption] = useState(null) + const [inputValue, setInputValue] = useState(null) + const [rangeFrom, setRangeFrom] = useState(null) + const [rangeTo, setRangeTo] = useState(null) + const modalContentRef = useRef(null) + + const property = useMemo( + () => + getModelProperties(modelType).find((prop) => prop.name === propertyName), + [modelType, propertyName] + ) + + const category = useMemo(() => getPropertyCategory(property), [property]) + + const options = useMemo(() => getFilterOptions(category), [category]) + + const propertyLabel = property?.label || propertyName + const categoryLabel = getCategoryLabel(category) + + const handleClear = () => { + onChange?.([]) + } + + const closeModal = () => { + onModalOpenChange?.(false) + setActiveOption(null) + setInputValue(null) + setRangeFrom(null) + setRangeTo(null) + } + + const applyFilter = (operator, rawValue) => { + const expression = buildExpression(operator, rawValue, property) + onChange?.([expression]) + closeModal() + } + + const handleOptionClick = (option) => { + if (option.expression != null) { + onChange?.([option.expression]) + return + } + if (option.key === 'isTrue' || option.key === 'isFalse') { + applyFilter(option.key) + return + } + if (option.key === 'isEmpty' || option.key === 'isNotEmpty') { + applyFilter(option.key) + return + } + setInputValue(null) + setRangeFrom(null) + setRangeTo(null) + onModalOpenChange?.(true) + setActiveOption(option) + } + + const handleModalOk = () => { + if (!activeOption) return + + if (activeOption.needsRange) { + const from = isDateTimePropertyType(property) + ? formatDateFilterValue(rangeFrom, property) + : formatFilterValue(rangeFrom, property) + const to = isDateTimePropertyType(property) + ? formatDateFilterValue(rangeTo, property) + : formatFilterValue(rangeTo, property) + + if (!from && !to) return + applyFilter('between', `${from}..${to}`) + return + } + + if ( + inputValue == null || + (typeof inputValue === 'string' && inputValue.trim() === '') + ) { + return + } + + applyFilter(activeOption.key, inputValue) + } + + const focusFirstModalInput = () => { + requestAnimationFrame(() => { + modalContentRef.current + ?.querySelector('input, textarea, .ant-picker-input input') + ?.focus() + }) + } + + const renderValueInput = (value, onValueChange) => { + if (isDateTimePropertyType(property)) { + const pickerValue = + value == null ? null : dayjs.isDayjs(value) ? value : dayjs(value) + const validValue = + pickerValue && pickerValue.isValid() ? pickerValue : null + + return ( + + ) + } + + if (NUMBER_TYPES.has(property?.type)) { + return ( + + ) + } + + return ( + onValueChange(event.target.value)} + /> + ) + } + + const filterMenuItems = buildFilterMenuItems(options, handleOptionClick) + + if (!property) return null + + const content = ( + + + + + + + ) + + return ( + <> + {useCard ? ( + + {content} + + ) : ( + content + )} + { + if (open) focusFirstModalInput() + }} + destroyOnHidden + focusTriggerAfterClose={false} + footer={null} + centered + closeIcon={null} + getContainer={() => document.body} + width={520} + > + {activeOption && ( + event.stopPropagation()} + > + + + Filter by {propertyLabel} + + + {activeOption.needsRange + ? `Enter a range to continue:` + : `Enter a ${categoryLabel.toLowerCase()} value where the ${propertyLabel.toLowerCase()} ${activeOption.label.toLowerCase()}:`} + + {activeOption.needsRange ? ( + + + From + + ), + children: renderValueInput(rangeFrom, setRangeFrom) + }, + { + key: 'to', + label: ( + + To + + ), + children: renderValueInput(rangeTo, setRangeTo) + } + ]} + /> + + ) : activeOption.needsValue ? ( + renderValueInput(inputValue, setInputValue) + ) : null} + + + + + + )} + + + ) +} + +QuickPropertyFilters.propTypes = { + modelType: PropTypes.string, + propertyName: PropTypes.string, + value: PropTypes.array, + onChange: PropTypes.func, + onModalOpenChange: PropTypes.func, + useCard: PropTypes.bool +} + +export default QuickPropertyFilters diff --git a/src/components/Dashboard/common/ScrollBox.jsx b/src/components/Dashboard/common/ScrollBox.jsx index 3c660967..5db69c74 100644 --- a/src/components/Dashboard/common/ScrollBox.jsx +++ b/src/components/Dashboard/common/ScrollBox.jsx @@ -7,18 +7,31 @@ const ScrollBox = ({ style, horizontalBottomPadding = 16, verticalRightPadding = 16, + inner = false, + horizontal = false, + smallPadding = false, + className = '', ...rest }) => { + const outerStyle = { + minHeight: '0', + minWidth: horizontal ? 0 : undefined, + '--scrollbox-vertical-right-padding': `${verticalRightPadding}px`, + '--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px`, + ...(horizontal ? { width: '100%', height: 'auto' } : { height: '100%' }) + } + + const simpleBarStyle = { + ...(horizontal ? { width: '100%', height: 'auto' } : { height: '100%' }), + ...style + } + return (
- + {children}
@@ -29,7 +42,11 @@ ScrollBox.propTypes = { children: PropTypes.node, style: PropTypes.object, horizontalBottomPadding: PropTypes.number, - verticalRightPadding: PropTypes.number + verticalRightPadding: PropTypes.number, + inner: PropTypes.bool, + horizontal: PropTypes.bool, + smallPadding: PropTypes.bool, + className: PropTypes.string } export default ScrollBox diff --git a/src/components/Dashboard/common/SecretDisplay.jsx b/src/components/Dashboard/common/SecretDisplay.jsx index 7d0937c8..9596f0ee 100644 --- a/src/components/Dashboard/common/SecretDisplay.jsx +++ b/src/components/Dashboard/common/SecretDisplay.jsx @@ -1,6 +1,7 @@ import { useState } from 'react' import PropTypes from 'prop-types' -import { Typography, Tooltip, Button } from 'antd' +import { Typography, Button } from 'antd' +import Tooltip from './Tooltip' import CopyButton from './CopyButton' import EyeIcon from '../../Icons/EyeIcon' import EyeSlashIcon from '../../Icons/EyeSlashIcon' @@ -20,7 +21,7 @@ const SecretDisplay = ({ value, reveal = false }) => { {reveal && visible ? value : masked} {reveal && ( - + + ) + })} + + + + ) +} + +SegmentedNav.propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + label: PropTypes.node, + disabled: PropTypes.bool, + reorderable: PropTypes.bool, + className: PropTypes.string + }) + ), + onChange: PropTypes.func, + onReorder: PropTypes.func, + className: PropTypes.string, + disabled: PropTypes.bool, + animated: PropTypes.bool +} + +export default SegmentedNav diff --git a/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx b/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx new file mode 100644 index 00000000..c3489007 --- /dev/null +++ b/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx @@ -0,0 +1,702 @@ +import { useState, useEffect, useContext, useMemo, useCallback, useRef } from 'react' +import { Spin, Tree, Flex, Checkbox, Typography } from 'antd' +import PropTypes from 'prop-types' +import dayjs from 'dayjs' +import { ApiServerContext } from '../context/ApiServerContext' +import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons' +import MissingPlaceholder from './MissingPlaceholder' +import { + matchesFilterExpression, + valuesToExpression +} from './filterExpression' + +const { Text } = Typography + +const MONTH_NAMES = Array.from({ length: 12 }, (_, i) => + dayjs().month(i).format('MMMM') +) + +const pad2 = (n) => String(n).padStart(2, '0') + +const nodeKey = { + year: (y) => `${y}`, + month: (y, m) => `${y}-${pad2(m)}`, + day: (y, m, d) => `${y}-${pad2(m)}-${pad2(d)}`, + hour: (y, m, d, h) => `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}`, + minute: (y, m, d, h, min) => + `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}:${pad2(min)}`, + second: (y, m, d, h, min, s) => + `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}:${pad2(min)}:${pad2(s)}` +} + +const lastDayOfMonth = (year, month) => new Date(year, month, 0).getDate() + +const toYearExpression = (year) => `1 1 ${year}..31 12 ${year}` + +const toMonthExpression = (year, month) => { + const last = lastDayOfMonth(year, month) + return `1 ${month} ${year}..${last} ${month} ${year}` +} + +const toDayExpression = (year, month, day) => `${day} ${month} ${year}` + +const toHourExpression = (year, month, day, hour) => + `${day} ${month} ${year} ${hour}` + +const toMinuteExpression = (year, month, day, hour, minute) => + `${day} ${month} ${year} ${hour} ${minute}` + +const toSecondExpression = (year, month, day, hour, minute, second) => + `${day} ${month} ${year} ${hour} ${minute} ${second}` + +const parseDateValue = (value) => { + if (value == null || value === '') return null + const date = value instanceof Date ? value : new Date(value) + return Number.isNaN(date.getTime()) ? null : date +} + +const hasNonMidnightTime = (date) => + date.getHours() !== 0 || + date.getMinutes() !== 0 || + date.getSeconds() !== 0 || + date.getMilliseconds() !== 0 + +/** Nested map: year → month → day → hour → minute → Set(second) */ +const buildDateHierarchy = (dates) => { + const root = new Map() + + for (const date of dates) { + const y = date.getFullYear() + const m = date.getMonth() + 1 + const d = date.getDate() + const h = date.getHours() + const min = date.getMinutes() + const s = date.getSeconds() + + if (!root.has(y)) root.set(y, new Map()) + const months = root.get(y) + if (!months.has(m)) months.set(m, new Map()) + const days = months.get(m) + if (!days.has(d)) days.set(d, { times: new Map(), dates: [] }) + const dayNode = days.get(d) + dayNode.dates.push(date) + + if (!dayNode.times.has(h)) dayNode.times.set(h, new Map()) + const minutes = dayNode.times.get(h) + if (!minutes.has(min)) minutes.set(min, new Set()) + minutes.get(min).add(s) + } + + return root +} + +const dayNeedsTimeLevels = (dayNode) => { + const { dates, times } = dayNode + if (dates.some(hasNonMidnightTime)) return true + let distinct = 0 + for (const minutes of times.values()) { + for (const seconds of minutes.values()) { + distinct += seconds.size + if (distinct > 1) return true + } + } + return false +} + +const sortNumericKeys = (map) => [...map.keys()].sort((a, b) => a - b) + +const buildTreeData = (hierarchy) => { + const years = sortNumericKeys(hierarchy) + + return years.map((year) => { + const monthsMap = hierarchy.get(year) + const monthNodes = sortNumericKeys(monthsMap).map((month) => { + const daysMap = monthsMap.get(month) + const dayNodes = sortNumericKeys(daysMap).map((day) => { + const dayNode = daysMap.get(day) + const includeTime = dayNeedsTimeLevels(dayNode) + let children + + if (includeTime) { + children = sortNumericKeys(dayNode.times).map((hour) => { + const minutesMap = dayNode.times.get(hour) + const minuteNodes = sortNumericKeys(minutesMap).map((minute) => { + const seconds = [...minutesMap.get(minute)].sort((a, b) => a - b) + const secondNodes = seconds.map((second) => ({ + key: nodeKey.second(year, month, day, hour, minute, second), + title: pad2(second), + level: 'second', + year, + month, + day, + hour, + minute, + second, + expression: toSecondExpression( + year, + month, + day, + hour, + minute, + second + ), + isLeaf: true + })) + + return { + key: nodeKey.minute(year, month, day, hour, minute), + title: pad2(minute), + level: 'minute', + year, + month, + day, + hour, + minute, + expression: toMinuteExpression(year, month, day, hour, minute), + children: secondNodes + } + }) + + return { + key: nodeKey.hour(year, month, day, hour), + title: `${pad2(hour)}:00`, + level: 'hour', + year, + month, + day, + hour, + expression: toHourExpression(year, month, day, hour), + children: minuteNodes + } + }) + } + + return { + key: nodeKey.day(year, month, day), + title: String(day), + level: 'day', + year, + month, + day, + expression: toDayExpression(year, month, day), + ...(children ? { children } : { isLeaf: true }) + } + }) + + return { + key: nodeKey.month(year, month), + title: MONTH_NAMES[month - 1], + level: 'month', + year, + month, + expression: toMonthExpression(year, month), + children: dayNodes + } + }) + + return { + key: nodeKey.year(year), + title: String(year), + level: 'year', + year, + expression: toYearExpression(year), + children: monthNodes + } + }) +} + +const collectAllKeys = (nodes, keys = []) => { + for (const node of nodes) { + keys.push(node.key) + if (node.children?.length) collectAllKeys(node.children, keys) + } + return keys +} + +const collectLeafKeys = (nodes, keys = []) => { + for (const node of nodes) { + if (!node.children?.length) keys.push(node.key) + else collectLeafKeys(node.children, keys) + } + return keys +} + +const indexNodesByKey = (nodes, map = new Map()) => { + for (const node of nodes) { + map.set(node.key, node) + if (node.children?.length) indexNodesByKey(node.children, map) + } + return map +} + +/** Prefer fully-selected ancestors over listing every leaf (Excel-style). */ +const collectCollapsedExpressions = (nodes, checkedSet) => { + const expressions = [] + + const walk = (nodeList) => { + for (const node of nodeList) { + const leafKeys = collectLeafKeys([node]) + const allChecked = + leafKeys.length > 0 && leafKeys.every((key) => checkedSet.has(key)) + + if (allChecked) { + expressions.push(node.expression) + continue + } + + if (node.children?.length) walk(node.children) + } + } + + walk(nodes) + return expressions +} + +const filterTreeBySearch = (nodes, query) => { + if (!query) return nodes + + const filterNodes = (list) => { + const result = [] + for (const node of list) { + const titleMatch = String(node.title).toLowerCase().includes(query) + if (titleMatch) { + result.push(node) + continue + } + const filteredChildren = node.children?.length + ? filterNodes(node.children) + : [] + if (filteredChildren.length > 0) { + result.push({ ...node, children: filteredChildren }) + } + } + return result + } + + return filterNodes(nodes) +} + +const getNodeDate = (node) => { + const { year, month, day, hour, minute, second } = node + if (second != null) { + return new Date(year, month - 1, day, hour, minute, second) + } + if (minute != null) { + return new Date(year, month - 1, day, hour, minute, 0) + } + if (hour != null) { + return new Date(year, month - 1, day, hour, 0, 0) + } + return new Date(year, month - 1, day, 0, 0, 0) +} + +const parseExpressionToKeys = (expression, nodeByKey) => { + const text = String(expression).trim() + if (!text) return [] + + // Year: 1 1 Y..31 12 Y + let match = text.match(/^1 1 (\d+)\.\.31 12 \1$/) + if (match) { + const key = nodeKey.year(Number(match[1])) + return nodeByKey.has(key) ? [key] : [] + } + + // Month: 1 M Y..last M Y + match = text.match(/^1 (\d+) (\d+)\.\.(\d+) \1 \2$/) + if (match) { + const month = Number(match[1]) + const year = Number(match[2]) + const last = Number(match[3]) + if (last === lastDayOfMonth(year, month)) { + const key = nodeKey.month(year, month) + return nodeByKey.has(key) ? [key] : [] + } + } + + // Day-first numeric: D M Y [H [Min [S]]] + match = text.match( + /^(\d+)\s+(\d+)\s+(\d+)(?:\s+(\d+)(?:\s+(\d+)(?:\s+(\d+))?)?)?$/ + ) + if (match) { + const day = Number(match[1]) + const month = Number(match[2]) + const year = Number(match[3]) + const hour = match[4] != null ? Number(match[4]) : null + const minute = match[5] != null ? Number(match[5]) : null + const second = match[6] != null ? Number(match[6]) : null + + let key + if (second != null) { + key = nodeKey.second(year, month, day, hour, minute, second) + } else if (minute != null) { + key = nodeKey.minute(year, month, day, hour, minute) + } else if (hour != null) { + key = nodeKey.hour(year, month, day, hour) + } else { + key = nodeKey.day(year, month, day) + } + return nodeByKey.has(key) ? [key] : [] + } + + // Interval spanning a single calendar day already handled; try ISO / Date parse + // for a specific instant → check the deepest matching node. + if (/[-/T]/.test(text) || /\d:\d/.test(text)) { + const date = parseDateValue(text) + if (!date) return [] + const y = date.getFullYear() + const m = date.getMonth() + 1 + const d = date.getDate() + const h = date.getHours() + const min = date.getMinutes() + const s = date.getSeconds() + + const candidates = [ + nodeKey.second(y, m, d, h, min, s), + nodeKey.minute(y, m, d, h, min), + nodeKey.hour(y, m, d, h), + nodeKey.day(y, m, d) + ] + for (const key of candidates) { + if (nodeByKey.has(key)) return [key] + } + } + + return [] +} + +const matchTreeToFilterValues = (value, nodeByKey, allLeafKeys) => { + if (!value?.length) return [] + + const matched = new Set() + const fieldOptions = { isDateField: true } + const expression = valuesToExpression(value) + + for (const expr of value) { + for (const key of parseExpressionToKeys(expr, nodeByKey)) { + matched.add(key) + } + } + + for (const leafKey of allLeafKeys) { + const node = nodeByKey.get(leafKey) + if (!node) continue + const date = getNodeDate(node) + if (matchesFilterExpression(expression, date, fieldOptions)) { + matched.add(leafKey) + } + } + + return [...matched] +} + +const expandKeysForChecked = (keys, nodeByKey) => { + const checked = new Set() + + for (const key of keys) { + const node = nodeByKey.get(key) + if (!node) { + checked.add(key) + continue + } + for (const leaf of collectLeafKeys([node])) { + checked.add(leaf) + } + // Also mark intermediate keys so Tree shows parent check state correctly + checked.add(key) + if (node.children?.length) { + for (const childKey of collectAllKeys(node.children)) { + checked.add(childKey) + } + } + } + + return [...checked] +} + +const EMPTY_OBJECT = {} + +const omitUndefinedValues = (obj) => + Object.fromEntries( + Object.entries(obj || {}).filter( + ([, value]) => value !== undefined && value !== '' + ) + ) + +const stableStringify = (value) => { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(',')}]` + } + if (value && typeof value === 'object') { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(',')}}` + } + return JSON.stringify(value) +} + +const getFilterHash = (filter, masterFilter) => + `${stableStringify(filter || {})}:${stableStringify(masterFilter || {})}` + +const SimpleDateTimePropertyFilter = ({ + modelType, + propertyName, + value = [], + onChange, + search = '', + filter = EMPTY_OBJECT, + masterFilter = EMPTY_OBJECT, + visible = true +}) => { + const { getModelPropertyValues } = useContext(ApiServerContext) + const getModelPropertyValuesRef = useRef(getModelPropertyValues) + getModelPropertyValuesRef.current = getModelPropertyValues + + const [dates, setDates] = useState([]) + const [loading, setLoading] = useState(() => visible) + const [localChecked, setLocalChecked] = useState(null) + + const filterForValues = useMemo(() => { + const next = omitUndefinedValues(filter) + if (propertyName) delete next[propertyName] + return next + }, [filter, propertyName]) + + const masterFilterForValues = useMemo( + () => omitUndefinedValues(masterFilter), + [masterFilter] + ) + + const filterHash = useMemo( + () => getFilterHash(filterForValues, masterFilterForValues), + [filterForValues, masterFilterForValues] + ) + const loadedFilterHashRef = useRef(null) + const filterHashRef = useRef(filterHash) + const filterForValuesRef = useRef(filterForValues) + const masterFilterForValuesRef = useRef(masterFilterForValues) + const wasVisibleRef = useRef(false) + filterHashRef.current = filterHash + filterForValuesRef.current = filterForValues + masterFilterForValuesRef.current = masterFilterForValues + + useEffect(() => { + const becameVisible = visible && !wasVisibleRef.current + wasVisibleRef.current = visible + if (!visible) return + if (!becameVisible && loadedFilterHashRef.current != null) return + + let cancelled = false + + const loadOptions = async () => { + if (!modelType || !propertyName) return + + const currentHash = filterHashRef.current + if (loadedFilterHashRef.current === currentHash) return + + setDates([]) + setLoading(true) + try { + const values = await getModelPropertyValuesRef.current( + modelType, + propertyName, + { + filter: filterForValuesRef.current, + masterFilter: masterFilterForValuesRef.current + } + ) + if (cancelled) return + const parsed = (values || []) + .map(parseDateValue) + .filter(Boolean) + .sort((a, b) => a.getTime() - b.getTime()) + loadedFilterHashRef.current = currentHash + setDates(parsed) + } finally { + if (!cancelled) setLoading(false) + } + } + + loadOptions() + return () => { + cancelled = true + } + }, [visible, modelType, propertyName]) + + const treeData = useMemo( + () => buildTreeData(buildDateHierarchy(dates)), + [dates] + ) + + const nodeByKey = useMemo(() => indexNodesByKey(treeData), [treeData]) + const allKeys = useMemo(() => collectAllKeys(treeData), [treeData]) + const allLeafKeys = useMemo(() => collectLeafKeys(treeData), [treeData]) + + const valueKey = useMemo(() => JSON.stringify(value || []), [value]) + + useEffect(() => { + if (treeData.length === 0) return + + if (value?.length > 0) { + const matched = matchTreeToFilterValues(value, nodeByKey, allLeafKeys) + if (matched.length > 0) { + setLocalChecked(expandKeysForChecked(matched, nodeByKey)) + } else { + setLocalChecked([]) + } + } else { + setLocalChecked(allKeys) + } + // valueKey captures value contents + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [treeData, valueKey, allKeys, allLeafKeys, nodeByKey]) + + const filteredTreeData = useMemo(() => { + const query = search.trim().toLowerCase() + return filterTreeBySearch(treeData, query) + }, [treeData, search]) + + const checkedKeys = localChecked ?? allKeys + + const emitChange = useCallback( + (nextKeys) => { + setLocalChecked(nextKeys) + const checkedSet = new Set(nextKeys) + + if ( + allLeafKeys.length > 0 && + allLeafKeys.every((key) => checkedSet.has(key)) + ) { + onChange?.([]) + return + } + + const expressions = collectCollapsedExpressions(treeData, checkedSet) + onChange?.(expressions) + }, + [allLeafKeys, onChange, treeData] + ) + + const checkedSet = useMemo(() => new Set(checkedKeys), [checkedKeys]) + + const rebuildCheckedKeys = useCallback( + (selectedLeaves) => { + const leafSet = new Set(selectedLeaves) + const next = [] + + const walk = (nodes) => { + for (const node of nodes) { + const leaves = collectLeafKeys([node]) + if (leaves.length > 0 && leaves.every((key) => leafSet.has(key))) { + next.push(...collectAllKeys([node])) + } else if (node.children?.length) { + walk(node.children) + } else if (leafSet.has(node.key)) { + next.push(node.key) + } + } + } + + walk(treeData) + return [...new Set(next)] + }, + [treeData] + ) + + const handleNodeCheck = (node, wantChecked) => { + const leavesUnderNode = new Set(collectLeafKeys([node])) + const currentLeaves = checkedKeys.filter((key) => { + const n = nodeByKey.get(key) + return n && !n.children?.length + }) + + let nextLeaves + if (wantChecked) { + nextLeaves = [...new Set([...currentLeaves, ...leavesUnderNode])] + } else { + nextLeaves = currentLeaves.filter((key) => !leavesUnderNode.has(key)) + } + + const query = search.trim().toLowerCase() + if (!query) { + emitChange(rebuildCheckedKeys(nextLeaves)) + return + } + + // Preserve checked leaves that are hidden by the search filter + const visibleLeafKeys = new Set(collectLeafKeys(filteredTreeData)) + const hiddenSelectedLeaves = currentLeaves.filter( + (key) => !visibleLeafKeys.has(key) + ) + const visibleSelectedLeaves = nextLeaves.filter((key) => + visibleLeafKeys.has(key) + ) + + emitChange( + rebuildCheckedKeys([...hiddenSelectedLeaves, ...visibleSelectedLeaves]) + ) + } + + const titleRender = (node) => { + const leafKeys = collectLeafKeys([node]) + const checkedCount = leafKeys.filter((key) => checkedSet.has(key)).length + const checked = leafKeys.length > 0 && checkedCount === leafKeys.length + const indeterminate = checkedCount > 0 && checkedCount < leafKeys.length + + return ( + + handleNodeCheck(node, e.target.checked)} + onClick={(e) => e.stopPropagation()} + style={{ minWidth: 0 }} + /> + + {node.title} + + + ) + } + + if (!loading && treeData.length === 0) { + return ( + + ) + } + + return ( + }> + {treeData.length > 0 ? ( + } + className='simple-date-time-property-filter' + defaultExpandAll={false} + style={{ minWidth: 0 }} + /> + ) : null} + + ) +} + +SimpleDateTimePropertyFilter.propTypes = { + modelType: PropTypes.string, + propertyName: PropTypes.string, + value: PropTypes.array, + onChange: PropTypes.func, + search: PropTypes.string, + filter: PropTypes.object, + masterFilter: PropTypes.object, + visible: PropTypes.bool +} + +export default SimpleDateTimePropertyFilter diff --git a/src/components/Dashboard/common/SimplePropertyFilter.jsx b/src/components/Dashboard/common/SimplePropertyFilter.jsx new file mode 100644 index 00000000..ad9cdcf9 --- /dev/null +++ b/src/components/Dashboard/common/SimplePropertyFilter.jsx @@ -0,0 +1,348 @@ +import { useState, useEffect, useContext, useMemo, useRef } from 'react' +import { Flex, Checkbox, Spin, Typography } from 'antd' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../context/ApiServerContext' +import { ObjectTableFilterContext } from './ObjectTableFilterContext' +import { + getModelByName, + getModelProperties +} from '../../../database/ObjectModels' +import ObjectProperty from './ObjectProperty' +import SimpleDateTimePropertyFilter from './SimpleDateTimePropertyFilter' +import { + getFieldOptions, + matchOptionsToFilterValues +} from './filterExpression' +import { LoadingOutlined } from '@ant-design/icons' +import MissingPlaceholder from './MissingPlaceholder' +const { Text } = Typography + +const isDateTimeProperty = (property) => + property?.type === 'dateTime' || property?.type === 'date' + +const getOptionKey = (option) => { + if (option && typeof option === 'object') { + if (option.objectType) { + const { prefix } = getModelByName(option.objectType) + if (option._reference != null) return `${prefix}:${option._reference}` + if (option._id != null) return `${prefix}:${option._id}` + } + return String(option._id ?? option.type ?? JSON.stringify(option)) + } + return String(option) +} + +const getDisplayValue = (option, property) => { + if (!property) return option + if (property.type === 'objectList') { + if (Array.isArray(option)) return option + if (option && typeof option === 'object' && option._id) return [option] + if (option != null) return [{ _id: option }] + } + if (property.type === 'object') { + if (option && typeof option === 'object' && option._id) return option + if (option != null) return { _id: option } + } + if (property.type === 'state') { + if (option && typeof option === 'object' && option.type) return option + if (option != null) return { type: option } + } + return option +} + +const getCandidateValue = (option, property) => { + if (property?.type === 'number') { + const num = Number(option) + return Number.isNaN(num) ? getOptionKey(option) : num + } + if (property?.type === 'boolean') { + if (typeof option === 'boolean') return option + const lower = String(option).toLowerCase() + if (['true', 'yes', '1', 'on', 'y'].includes(lower)) return true + if (['false', 'no', '0', 'off', 'n'].includes(lower)) return false + return option + } + return getOptionKey(option) +} + +const stableStringify = (value) => { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(',')}]` + } + if (value && typeof value === 'object') { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(',')}}` + } + return JSON.stringify(value) +} + +const getFilterHash = (filter, masterFilter) => + `${stableStringify(filter || {})}:${stableStringify(masterFilter || {})}` + +const omitUndefinedValues = (obj) => + Object.fromEntries( + Object.entries(obj || {}).filter( + ([, value]) => value !== undefined && value !== '' + ) + ) + +// Survive table reloads / popover remounts without refetching the same property. +const optionsCache = new Map() +const lastLoadedFilterHashByProperty = new Map() + +const EMPTY_OBJECT = {} + +const SimplePropertyFilter = ({ + modelType, + propertyName, + value = [], + onChange, + search = '', + filter = EMPTY_OBJECT, + masterFilter = EMPTY_OBJECT, + visible = true, + useTableFilter = true +}) => { + const { getModelPropertyValues } = useContext(ApiServerContext) + const tableFilterContext = useContext(ObjectTableFilterContext) + const getModelPropertyValuesRef = useRef(getModelPropertyValues) + getModelPropertyValuesRef.current = getModelPropertyValues + + const resolvedFilter = useTableFilter + ? (tableFilterContext?.filter ?? filter) + : filter + const resolvedMasterFilter = useTableFilter + ? (tableFilterContext?.masterFilter ?? masterFilter) + : masterFilter || tableFilterContext?.masterFilter || EMPTY_OBJECT + + const filterForValues = useMemo(() => { + const next = omitUndefinedValues(resolvedFilter) + if (propertyName) delete next[propertyName] + return next + }, [resolvedFilter, propertyName]) + + const masterFilterForValues = useMemo( + () => omitUndefinedValues(resolvedMasterFilter), + [resolvedMasterFilter] + ) + + const filterHash = useMemo( + () => getFilterHash(filterForValues, masterFilterForValues), + [filterForValues, masterFilterForValues] + ) + const loadedFilterHashRef = useRef(null) + const filterHashRef = useRef(filterHash) + const filterForValuesRef = useRef(filterForValues) + const masterFilterForValuesRef = useRef(masterFilterForValues) + const wasVisibleRef = useRef(false) + filterHashRef.current = filterHash + filterForValuesRef.current = filterForValues + masterFilterForValuesRef.current = masterFilterForValues + + const cacheKey = + modelType && propertyName + ? `${modelType}:${propertyName}:${filterHash}` + : null + const [options, setOptions] = useState( + () => (cacheKey && optionsCache.get(cacheKey)) || [] + ) + const [loading, setLoading] = useState( + () => visible && !(cacheKey && optionsCache.has(cacheKey)) + ) + const [localChecked, setLocalChecked] = useState(null) + + const property = useMemo( + () => + getModelProperties(modelType).find((prop) => prop.name === propertyName), + [modelType, propertyName] + ) + + useEffect(() => { + const becameVisible = visible && !wasVisibleRef.current + wasVisibleRef.current = visible + if (!visible) return + if (!becameVisible && loadedFilterHashRef.current != null) return + + let cancelled = false + + const loadOptions = async () => { + if (!modelType || !propertyName || isDateTimeProperty(property)) { + setLoading(false) + return + } + + const currentHash = filterHashRef.current + const propertyKey = `${modelType}:${propertyName}` + const lastHash = + loadedFilterHashRef.current ?? + lastLoadedFilterHashByProperty.get(propertyKey) + + if (lastHash === currentHash) { + loadedFilterHashRef.current = currentHash + const cached = optionsCache.get(`${propertyKey}:${currentHash}`) + if (cached) { + setOptions(cached) + setLoading(false) + return + } + } + + setOptions([]) + setLoading(true) + try { + const values = await getModelPropertyValuesRef.current( + modelType, + propertyName, + { + filter: filterForValuesRef.current, + masterFilter: masterFilterForValuesRef.current + } + ) + if (cancelled) return + const unique = [ + ...new Set((values || []).filter((v) => v != null && v !== '')) + ] + optionsCache.set(`${propertyKey}:${currentHash}`, unique) + lastLoadedFilterHashByProperty.set(propertyKey, currentHash) + loadedFilterHashRef.current = currentHash + setOptions(unique) + } finally { + if (!cancelled) setLoading(false) + } + } + + loadOptions() + return () => { + cancelled = true + } + // Reload only when this filter becomes visible, then compare filterHash. + }, [visible, modelType, propertyName, property]) + + const valueKey = useMemo( + () => JSON.stringify((value || []).map(getOptionKey)), + [value] + ) + + // Sync checkbox state from the applied filter (sidebar / column filteredValue). + // No active filter => all options checked by default. + useEffect(() => { + if (options.length === 0) return + if (value?.length > 0) { + const matchedKeys = matchOptionsToFilterValues(options, value, { + getOptionKey, + getCandidate: (option) => getCandidateValue(option, property), + fieldOptions: getFieldOptions(property) + }) + setLocalChecked(matchedKeys) + } else { + setLocalChecked(options.map(getOptionKey)) + } + // valueKey captures value contents; value is read for matching + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options, valueKey, property]) + + const filteredOptions = useMemo(() => { + const query = search.trim().toLowerCase() + if (!query) return options + return options.filter((option) => + getOptionKey(option).toLowerCase().includes(query) + ) + }, [options, search]) + + const optionKeys = useMemo(() => options.map(getOptionKey), [options]) + const checkedKeys = localChecked ?? optionKeys + + const emitChange = (nextKeys) => { + setLocalChecked(nextKeys) + // All selected (or empty options) means no filter applied + if (optionKeys.length > 0 && nextKeys.length === optionKeys.length) { + onChange?.([]) + } else { + onChange?.(nextKeys) + } + } + + const handleChange = (visibleCheckedKeys) => { + const filteredKeys = new Set(filteredOptions.map(getOptionKey)) + const hiddenSelected = checkedKeys.filter((key) => !filteredKeys.has(key)) + emitChange([...hiddenSelected, ...visibleCheckedKeys]) + } + + if (isDateTimeProperty(property)) { + return ( + + ) + } + + if (!loading && options.length === 0) { + return ( + + ) + } + + return ( + }> + + + {filteredOptions.map((option) => ( + + +
+ {property ? ( + + ) : ( + {String(option)} + )} +
+
+ ))} +
+
+
+ ) +} + +SimplePropertyFilter.propTypes = { + modelType: PropTypes.string, + propertyName: PropTypes.string, + value: PropTypes.array, + onChange: PropTypes.func, + search: PropTypes.string, + filter: PropTypes.object, + masterFilter: PropTypes.object, + visible: PropTypes.bool, + useTableFilter: PropTypes.bool +} + +export default SimplePropertyFilter diff --git a/src/components/Dashboard/common/SortSidebar.jsx b/src/components/Dashboard/common/SortSidebar.jsx new file mode 100644 index 00000000..8d2d62fb --- /dev/null +++ b/src/components/Dashboard/common/SortSidebar.jsx @@ -0,0 +1,100 @@ +import { useMemo } from 'react' +import { Card, Select, Button, Flex, Dropdown } from 'antd' +import PropTypes from 'prop-types' +import { + getModelByName, + getModelProperties +} from '../../../database/ObjectModels' +import BinIcon from '../../Icons/BinIcon' + +const DIRECTION_OPTIONS = [ + { label: 'Ascending', value: 'ascend' }, + { label: 'Descending', value: 'descend' } +] + +const SortSidebar = ({ + type, + sorter = {}, + onSortChange, + stretchHeight = false +}) => { + const model = getModelByName(type) + const modelProps = getModelProperties(type) + + const propertyOptions = useMemo(() => { + if (!model.sorters) return [] + return model.sorters + .map((name) => modelProps.find((p) => p.name === name)) + .filter(Boolean) + .map((prop) => ({ + label: prop.label, + value: prop.name + })) + }, [model.sorters, modelProps]) + + const menuItems = { + items: [ + { + key: 'clearSort', + label: 'Clear sort', + danger: true, + icon: , + onClick: () => onSortChange({}) + } + ] + } + + const changeProperty = (field) => { + if (!field) { + onSortChange({}) + return + } + onSortChange({ + field, + order: sorter.order || 'ascend' + }) + } + + const changeDirection = (order) => { + if (!sorter.field) return + onSortChange({ field: sorter.field, order }) + } + + return ( + + + + + + + + + + + ) +} + +SortSidebar.propTypes = { + type: PropTypes.string.isRequired, + sorter: PropTypes.object, + onSortChange: PropTypes.func.isRequired, + stretchHeight: PropTypes.bool +} + +export default SortSidebar diff --git a/src/components/Dashboard/common/SortSidebarButton.jsx b/src/components/Dashboard/common/SortSidebarButton.jsx new file mode 100644 index 00000000..3de33001 --- /dev/null +++ b/src/components/Dashboard/common/SortSidebarButton.jsx @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types' +import { Badge, Button } from 'antd' +import { useLocation } from 'react-router-dom' +import SortCircleFilledIcon from '../../Icons/SortCircleFilledIcon' +import SortCircleIcon from '../../Icons/SortCircleIcon' +import { useTableState } from '../context/TableStateContext' + +const SortSidebarButton = ({ active, onClick, ...buttonProps }) => { + const location = useLocation() + const { pageSorters } = useTableState() + const sorter = pageSorters[location.pathname] + const count = sorter?.field && sorter?.order ? 1 : 0 + + return ( + + +