Compare commits

...

16 Commits

Author SHA1 Message Date
12a4d46da8 Update DeveloperSidebar to replace Print Server Debug with API Debug; adjust routeKeyMap for new API context debugging path. 2025-09-05 23:17:58 +01:00
1f7b7292fb Add destroyOnHidden prop to NewPrinter modal for improved cleanup on close 2025-09-05 23:17:45 +01:00
6c0933b797 Refactor NewPrinter component to utilize NewObjectForm and WizardView for improved structure and user experience; streamline printer setup process and enhance form validation. 2025-09-05 23:17:35 +01:00
5300874b80 Enhance ObjectTable component to support Electron context; adjust scroll height based on Electron environment and improve action button handling with dynamic disabled state. 2025-09-05 23:17:20 +01:00
f0cb4b3b83 Add ApiContextDebug component for API server debugging and testing functionality; removed PrintServerContextDebug component. 2025-09-05 23:17:08 +01:00
fbdb451659 Add DocumentPrintButton to various management components and update objectData handling 2025-09-05 23:16:42 +01:00
204a881dfa Refactor object data merging in NewObjectForm to use lodash's merge for improved state management 2025-09-05 23:15:42 +01:00
c5355c8cf2 Implement DocumentPrintButton component for document template selection and printing functionality 2025-09-05 23:15:24 +01:00
5a656d43f1 Added AlertsDisplay component to ControlPrinter and updated state management to include objectData. 2025-09-05 23:14:32 +01:00
c82954db2a Icon changes 2025-09-05 23:13:53 +01:00
cccabfb063 Fixed scroll bars. 2025-09-05 23:13:45 +01:00
5e915fe8bc Icon changes 2025-09-05 23:13:36 +01:00
6fedb9739f Added DocumentPrintButton to PrinterInfo component and updated objectData handling. 2025-09-05 23:13:19 +01:00
27a7acf68f Added file and document job support. 2025-09-05 23:12:52 +01:00
e4b766fab1 Removed more logging. 2025-08-31 22:02:48 +01:00
4271a7f084 Removed logging. 2025-08-31 22:01:58 +01:00
47 changed files with 2534 additions and 788 deletions

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,12.4182,0)">
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
<path d="M53.485,22.46L53.485,69.739C53.485,76.482 49.963,80.036 43.319,80.036L10.16,80.036C3.517,80.036 0,76.482 0,69.739L0,22.46C0,15.717 3.517,12.164 10.16,12.164L10.52,12.164C10.45,12.571 10.417,12.998 10.417,13.44L10.417,16.721C10.417,17.219 10.46,17.699 10.543,18.157L10.427,18.157C7.509,18.157 5.993,19.749 5.993,22.523L5.993,69.677C5.993,72.476 7.509,74.043 10.427,74.043L43.058,74.043C45.971,74.043 47.492,72.476 47.492,69.677L47.492,22.523C47.492,19.749 45.971,18.157 43.058,18.157L42.937,18.157C43.02,17.699 43.063,17.219 43.063,16.721L43.063,13.44C43.063,12.998 43.029,12.571 42.959,12.164L43.319,12.164C49.963,12.164 53.485,15.717 53.485,22.46Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
<path d="M17.109,19.927L36.371,19.927C38.251,19.927 39.384,18.726 39.384,16.721L39.384,13.44C39.384,11.429 38.251,10.234 36.371,10.234L33.721,10.234C33.485,6.601 30.469,3.659 26.743,3.659C23.016,3.659 20,6.601 19.764,10.234L17.109,10.234C15.234,10.234 14.101,11.429 14.101,13.44L14.101,16.721C14.101,18.726 15.234,19.927 17.109,19.927ZM26.743,13.248C25.22,13.248 23.984,11.981 23.984,10.495C23.984,8.952 25.22,7.717 26.743,7.717C28.265,7.717 29.495,8.952 29.495,10.495C29.495,11.981 28.265,13.248 26.743,13.248Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
<path d="M13.473,60.281L27.067,60.281C28.27,60.281 29.263,59.294 29.263,58.085C29.263,56.882 28.301,55.9 27.067,55.9L13.473,55.9C12.244,55.9 11.251,56.882 11.251,58.085C11.251,59.294 12.27,60.281 13.473,60.281Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
<path d="M13.473,48.622L40.018,48.622C41.252,48.622 42.234,47.64 42.234,46.406C42.234,45.197 41.246,44.241 40.018,44.241L13.473,44.241C12.244,44.241 11.251,45.197 11.251,46.406C11.251,47.64 12.233,48.622 13.473,48.622Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
<path d="M13.473,37.562L40.018,37.562C41.221,37.562 42.234,36.569 42.234,35.366C42.234,34.157 41.221,33.144 40.018,33.144L13.473,33.144C12.27,33.144 11.251,34.157 11.251,35.366C11.251,36.569 12.27,37.562 13.473,37.562Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.75929,0,0,0.75929,7.36648,3.81256)">
<g transform="matrix(0.811652,0,0,0.811652,5.66772,1.86878)">
<g transform="matrix(1.37054,0,0,1.37054,-11.3557,-4.11161)">
<path d="M46.477,44.675L45.352,44.675C44.11,44.675 43.102,45.683 43.102,46.925C43.102,48.167 44.11,49.175 45.352,49.175L46.477,49.175C47.718,49.175 48.727,48.167 48.727,46.925C48.727,45.683 47.718,44.675 46.477,44.675ZM51.032,42.519C50.944,42.707 50.834,42.883 50.707,43.046C49.946,44.027 50.123,45.442 51.104,46.203C52.085,46.965 53.499,46.787 54.261,45.807C54.595,45.377 54.88,44.914 55.111,44.42C55.635,43.294 55.147,41.954 54.021,41.43C52.896,40.905 51.556,41.394 51.032,42.519ZM51.266,35.661C51.266,36.044 51.266,36.421 51.266,36.786C51.267,38.028 52.276,39.035 53.517,39.035C54.759,39.034 55.767,38.025 55.766,36.784C55.766,36.418 55.766,36.042 55.766,35.659C55.765,34.417 54.756,33.409 53.514,33.41C52.273,33.41 51.265,34.419 51.266,35.661ZM51.261,27.786C51.262,28.131 51.262,28.508 51.262,28.911C51.263,30.153 52.271,31.16 53.513,31.16C54.755,31.159 55.763,30.15 55.762,28.909C55.762,28.506 55.762,28.128 55.761,27.784C55.761,26.542 54.752,25.534 53.51,25.535C52.268,25.535 51.261,26.544 51.261,27.786ZM50.859,20.789C50.948,21.015 51.014,21.246 51.064,21.496C51.311,22.713 52.5,23.5 53.717,23.253C54.934,23.006 55.721,21.817 55.474,20.6C55.37,20.087 55.232,19.614 55.05,19.15C54.598,17.993 53.292,17.422 52.135,17.874C50.979,18.326 50.407,19.632 50.859,20.789ZM19.512,15.856L19.512,15.864C19.512,15.914 19.513,15.963 19.517,16.013L19.518,16.042L19.523,16.088L19.523,16.094L19.524,16.101C19.574,16.662 19.796,17.067 20.082,17.36C20.494,17.822 21.094,18.114 21.762,18.114C21.762,18.114 24.012,17.959 24.012,15.85L24.012,14.739C24.012,13.497 23.004,12.489 21.762,12.489C20.52,12.489 19.512,13.497 19.512,14.739L19.512,15.85L19.512,15.856ZM46.568,15.31C46.85,15.603 47.114,15.879 47.347,16.122C48.207,17.018 49.633,17.047 50.529,16.187C51.425,15.327 51.454,13.901 50.594,13.005C50.36,12.762 50.096,12.487 49.815,12.194C48.955,11.298 47.529,11.269 46.633,12.129C45.737,12.989 45.708,14.414 46.568,15.31ZM41.132,9.633C41.392,9.901 41.644,10.166 41.886,10.422C42.738,11.325 44.163,11.366 45.066,10.514C45.969,9.662 46.01,8.236 45.158,7.333C44.901,7.062 44.634,6.78 44.358,6.496C43.492,5.605 42.066,5.586 41.176,6.451C40.286,7.317 40.266,8.743 41.132,9.633ZM24.106,8.619C24.162,8.422 24.241,8.231 24.34,8.049C24.938,6.96 24.539,5.591 23.45,4.994C22.361,4.397 20.992,4.796 20.395,5.885C20.131,6.365 19.924,6.873 19.777,7.392C19.438,8.587 20.133,9.832 21.328,10.17C22.523,10.509 23.768,9.813 24.106,8.619ZM36.899,2.219L36.89,2.219L36.059,2.219C34.817,2.219 33.809,3.227 33.809,4.469C33.809,5.711 34.817,6.719 36.059,6.719L36.899,6.719C36.935,6.719 36.97,6.721 37.006,6.723C38.244,6.822 39.328,5.896 39.426,4.658C39.524,3.42 38.599,2.336 37.361,2.238C37.207,2.225 37.053,2.219 36.899,2.219L36.899,2.219ZM28.184,6.719L29.309,6.719C30.55,6.719 31.559,5.711 31.559,4.469C31.559,3.227 30.55,2.219 29.309,2.219L28.184,2.219C26.942,2.219 25.934,3.227 25.934,4.469C25.934,5.711 26.942,6.719 28.184,6.719Z"/>
<path d="M51.032,42.519C50.944,42.707 50.834,42.883 50.707,43.046C49.946,44.027 50.123,45.442 51.104,46.203C52.085,46.965 53.499,46.787 54.261,45.807C54.595,45.377 54.88,44.914 55.111,44.42C55.635,43.294 55.147,41.954 54.021,41.43C52.896,40.905 51.556,41.394 51.032,42.519ZM51.266,35.661L51.266,36.786C51.267,38.028 52.276,39.035 53.517,39.035C54.759,39.034 55.767,38.025 55.766,36.784L55.766,35.659C55.765,34.417 54.756,33.409 53.514,33.41C52.273,33.41 51.265,34.419 51.266,35.661ZM51.261,27.786C51.262,28.131 51.262,28.508 51.262,28.911C51.263,30.153 52.271,31.16 53.513,31.16C54.755,31.159 55.763,30.15 55.762,28.909C55.762,28.506 55.762,28.128 55.761,27.784C55.761,26.542 54.752,25.534 53.51,25.535C52.268,25.535 51.261,26.544 51.261,27.786ZM50.859,20.789C50.948,21.015 51.014,21.246 51.064,21.496C51.311,22.713 52.5,23.5 53.717,23.253C54.934,23.006 55.721,21.817 55.474,20.6C55.37,20.087 55.232,19.614 55.05,19.15C54.598,17.993 53.292,17.422 52.135,17.874C50.979,18.326 50.407,19.632 50.859,20.789ZM46.568,15.31C46.85,15.603 47.114,15.879 47.347,16.122C48.207,17.018 49.633,17.047 50.529,16.187C51.425,15.327 51.454,13.901 50.594,13.005C50.36,12.762 50.096,12.487 49.815,12.194C48.955,11.298 47.529,11.269 46.633,12.129C45.737,12.989 45.708,14.414 46.568,15.31ZM41.132,9.633C41.392,9.901 41.644,10.166 41.886,10.422C42.738,11.325 44.163,11.366 45.066,10.514C45.969,9.662 46.01,8.236 45.158,7.333C44.901,7.062 44.634,6.78 44.358,6.496C43.492,5.605 42.066,5.586 41.176,6.451C40.286,7.317 40.266,8.743 41.132,9.633ZM24.106,8.619C24.162,8.422 24.241,8.231 24.34,8.049C24.938,6.96 24.539,5.591 23.45,4.994C22.361,4.397 20.992,4.796 20.395,5.885C20.131,6.365 19.924,6.873 19.777,7.392C19.438,8.587 20.133,9.832 21.328,10.17C22.523,10.509 23.768,9.813 24.106,8.619ZM36.899,2.219L36.059,2.219C34.817,2.219 33.809,3.227 33.809,4.469C33.809,5.711 34.817,6.719 36.059,6.719L36.899,6.719C36.935,6.719 36.97,6.721 37.006,6.723C38.244,6.822 39.328,5.896 39.426,4.658C39.524,3.42 38.599,2.336 37.361,2.238C37.207,2.225 37.053,2.219 36.899,2.219ZM28.184,6.719L29.309,6.719C30.55,6.719 31.559,5.711 31.559,4.469C31.559,3.227 30.55,2.219 29.309,2.219L28.184,2.219C26.942,2.219 25.934,3.227 25.934,4.469C25.934,5.711 26.942,6.719 28.184,6.719Z"/>
</g>
<g transform="matrix(1.37054,0,0,1.37054,-11.3557,-4.11161)">
<path d="M47.785,18.431L46.66,18.431C45.418,18.431 44.41,19.439 44.41,20.681C44.41,21.923 45.418,22.931 46.66,22.931L47.785,22.931C49.027,22.931 50.035,21.923 50.035,20.681C50.035,19.439 49.027,18.431 47.785,18.431ZM41.493,18.243C41.35,18.127 41.218,17.99 41.109,17.844C40.368,16.848 38.958,16.64 37.961,17.381C36.965,18.122 36.757,19.533 37.498,20.529C37.83,20.976 38.233,21.395 38.671,21.747C39.638,22.526 41.055,22.373 41.834,21.406C42.613,20.439 42.46,19.022 41.493,18.243ZM40.961,12.576L40.961,11.451C40.961,10.21 39.953,9.201 38.711,9.201C37.47,9.201 36.461,10.21 36.461,11.451L36.461,12.576C36.461,13.818 37.47,14.826 38.711,14.826C39.953,14.826 40.961,13.818 40.961,12.576Z"/>
</g>
<g transform="matrix(1.09263,0,0,1.09263,0.0174335,-6.97689)">
<g transform="matrix(0.937843,0,0,0.937843,-0.188198,11.8804)">
<path d="M10.149,67.641L43.438,67.641C50.137,67.641 53.586,64.135 53.586,57.41L53.586,29.058C53.586,24.717 53.028,22.725 50.308,19.954L33.894,3.284C31.282,0.616 29.111,0 25.212,0L10.149,0C3.481,0 0,3.532 0,10.257L0,57.41C0,64.161 3.455,67.641 10.149,67.641ZM10.637,61.519C7.621,61.519 6.122,59.941 6.122,57.029L6.122,10.637C6.122,7.752 7.621,6.122 10.663,6.122L23.984,6.122L23.984,23.261C23.984,27.733 26.167,29.895 30.619,29.895L47.464,29.895L47.464,57.029C47.464,59.941 45.965,61.519 42.929,61.519L10.637,61.519ZM31.198,24.496C29.903,24.496 29.384,23.945 29.384,22.656L29.384,6.973L46.613,24.496L31.198,24.496Z" style="fill-rule:nonzero;"/>
</g>
@ -26,4 +27,5 @@
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.156042,0,0,0.156042,1.80453,0.721021)">
<rect x="0" y="0" width="53.774" height="67.661" style="fill-opacity:0;"/>
<path d="M10.149,67.641L43.438,67.641C50.137,67.641 53.586,64.135 53.586,57.41L53.586,29.058C53.586,24.717 53.028,22.725 50.308,19.954L33.894,3.284C31.282,0.616 29.111,0 25.212,0L10.149,0C3.481,0 0,3.532 0,10.257L0,57.41C0,64.161 3.455,67.641 10.149,67.641ZM10.637,61.519C7.621,61.519 6.122,59.941 6.122,57.029L6.122,10.637C6.122,7.752 7.621,6.122 10.663,6.122L23.984,6.122L23.984,23.261C23.984,27.733 26.167,29.895 30.619,29.895L47.464,29.895L47.464,57.029C47.464,59.941 45.965,61.519 42.929,61.519L10.637,61.519ZM31.198,24.496C29.903,24.496 29.384,23.945 29.384,22.656L29.384,6.973L46.613,24.496L31.198,24.496Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,21 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(12.418)">
<g transform="matrix(.83795 0 0 .83795 -2.8271 -3.0658)">
<path d="m53.485 22.46v47.279c0 6.743-3.522 10.297-10.166 10.297h-33.159c-6.643 0-10.16-3.554-10.16-10.297v-47.279c0-6.743 3.517-10.296 10.16-10.296h0.36c-0.07 0.407-0.103 0.834-0.103 1.276v3.281c0 0.498 0.043 0.978 0.126 1.436h-0.116c-2.918 0-4.434 1.592-4.434 4.366v47.154c0 2.799 1.516 4.366 4.434 4.366h32.631c2.913 0 4.434-1.567 4.434-4.366v-47.154c0-2.774-1.521-4.366-4.434-4.366h-0.121c0.083-0.458 0.126-0.938 0.126-1.436v-3.281c0-0.442-0.034-0.869-0.104-1.276h0.36c6.644 0 10.166 3.553 10.166 10.296z" fill-rule="nonzero"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,12.418,0)">
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.0658)">
<path d="M53.485,22.46L53.485,69.739C53.485,76.482 49.963,80.036 43.319,80.036L10.16,80.036C3.517,80.036 0,76.482 0,69.739L0,22.46C0,15.717 3.517,12.164 10.16,12.164L10.52,12.164C10.45,12.571 10.417,12.998 10.417,13.44L10.417,16.721C10.417,17.219 10.46,17.699 10.543,18.157L10.427,18.157C7.509,18.157 5.993,19.749 5.993,22.523L5.993,69.677C5.993,72.476 7.509,74.043 10.427,74.043L43.058,74.043C45.971,74.043 47.492,72.476 47.492,69.677L47.492,22.523C47.492,19.749 45.971,18.157 43.058,18.157L42.937,18.157C43.02,17.699 43.063,17.219 43.063,16.721L43.063,13.44C43.063,12.998 43.029,12.571 42.959,12.164L43.319,12.164C49.963,12.164 53.485,15.717 53.485,22.46Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(.83795 0 0 .83795 -2.8271 -3.0658)">
<path d="m17.109 19.927h19.262c1.88 0 3.013-1.201 3.013-3.206v-3.281c0-2.011-1.133-3.206-3.013-3.206h-2.65c-0.236-3.633-3.252-6.575-6.978-6.575-3.727 0-6.743 2.942-6.979 6.575h-2.655c-1.875 0-3.008 1.195-3.008 3.206v3.281c0 2.005 1.133 3.206 3.008 3.206zm9.634-6.679c-1.523 0-2.759-1.267-2.759-2.753 0-1.543 1.236-2.778 2.759-2.778 1.522 0 2.752 1.235 2.752 2.778 0 1.486-1.23 2.753-2.752 2.753z" fill-rule="nonzero"/>
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.0658)">
<path d="M17.109,19.927L36.371,19.927C38.251,19.927 39.384,18.726 39.384,16.721L39.384,13.44C39.384,11.429 38.251,10.234 36.371,10.234L33.721,10.234C33.485,6.601 30.469,3.659 26.743,3.659C23.016,3.659 20,6.601 19.764,10.234L17.109,10.234C15.234,10.234 14.101,11.429 14.101,13.44L14.101,16.721C14.101,18.726 15.234,19.927 17.109,19.927ZM26.743,13.248C25.22,13.248 23.984,11.981 23.984,10.495C23.984,8.952 25.22,7.717 26.743,7.717C28.265,7.717 29.495,8.952 29.495,10.495C29.495,11.981 28.265,13.248 26.743,13.248Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(.83795 0 0 .83795 -2.8271 -3.0658)">
<path d="m13.473 60.281h13.594c1.203 0 2.196-0.987 2.196-2.196 0-1.203-0.962-2.185-2.196-2.185h-13.594c-1.229 0-2.222 0.982-2.222 2.185 0 1.209 1.019 2.196 2.222 2.196z" fill-rule="nonzero"/>
</g>
<g transform="matrix(.83795 0 0 .83795 -2.8271 -3.0658)">
<path d="m13.473 48.622h26.545c1.234 0 2.216-0.982 2.216-2.216 0-1.209-0.988-2.165-2.216-2.165h-26.545c-1.229 0-2.222 0.956-2.222 2.165 0 1.234 0.982 2.216 2.222 2.216z" fill-rule="nonzero"/>
</g>
<g transform="matrix(.83795 0 0 .83795 -2.8271 -3.0658)">
<path d="m13.473 37.562h26.545c1.203 0 2.216-0.993 2.216-2.196 0-1.209-1.013-2.222-2.216-2.222h-26.545c-1.203 0-2.222 1.013-2.222 2.222 0 1.203 1.019 2.196 2.222 2.196z" fill-rule="nonzero"/>
<g transform="matrix(0.411749,0,0,0.411749,6.00393,20.6414)">
<rect x="0" y="0" width="65.953" height="73.445" style="fill-opacity:0;"/>
<path d="M28.938,13.973L36.828,13.973L36.828,4.613L28.938,4.613L28.938,13.973ZM28.938,24.41L36.828,24.41L36.828,16.895L28.938,16.895L28.938,24.41ZM6.594,57.16L14.453,52.613L10.438,45.77L2.875,50.098L6.594,57.16ZM16.984,51.176L23.5,47.441L19.516,40.566L13.016,44.301L16.984,51.176ZM51.125,52.613L58.984,57.16L62.703,50.098L55.141,45.77L51.125,52.613ZM42.078,47.441L48.609,51.176L52.563,44.301L46.063,40.566L42.078,47.441ZM26.047,45.988L32.797,42.098L39.547,45.988L43.531,39.145L36.828,35.301L36.828,27.316L28.938,27.316L28.938,35.191L22.047,39.145L26.047,45.988ZM4.297,58.176L28.578,72.207C31.297,73.801 34.469,73.801 37.203,72.207L61.469,58.176C64.156,56.645 65.766,53.863 65.766,50.723L65.766,22.691C65.766,19.551 64.156,16.77 61.469,15.223L37.203,1.207C34.469,-0.402 31.297,-0.402 28.578,1.207L4.297,15.223C1.609,16.77 0,19.551 0,22.691L0,50.723C0,53.863 1.609,56.645 4.297,58.176ZM28.938,63.285C28.672,63.145 28.625,63.113 28.359,62.957L9.531,52.066C8.5,51.457 7.875,50.426 7.875,49.238L7.875,27.848L28.938,39.91L28.938,63.285ZM32.797,32.973L11.047,20.52C11.219,20.379 11.281,20.316 11.531,20.176L31.25,8.785C32.281,8.16 33.484,8.16 34.516,8.785L54.25,20.176C54.484,20.316 54.516,20.363 54.625,20.457L32.797,32.973ZM36.828,63.285L36.828,39.801L57.875,27.754L57.875,49.238C57.875,50.426 57.281,51.457 56.25,52.066L37.797,62.738C37.391,62.973 37.219,63.082 36.828,63.285Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -251,8 +251,15 @@ body {
::-webkit-scrollbar {
width: 10px;
border-radius: 34px;
width: 8px;
}
::-webkit-scrollbar:vertical {
width: 8px;
}
::-webkit-scrollbar:horizontal {
height: 8px;
}
/* Track */
@ -263,13 +270,17 @@ body {
/* Handle */
::-webkit-scrollbar-thumb {
background: #8888881a;
border: 4px solid rgba(0, 0, 0, 0);
border: 2px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
transition: all 1s;
-moz-transition: all 1s;
-webkit-transition: all 1s;
}
::-webkit-scrollbar-corner {
background: #00000000; /* The scroll corner color */
}
::-webkit-scrollbar-thumb:vertical {
border-left: none;
}
@ -281,7 +292,7 @@ body {
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #5555551f;
border: 4px solid rgba(0, 0, 0, 0);
border: 2px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
}
@ -292,3 +303,7 @@ body {
::-webkit-scrollbar-thumb:horizontal:hover {
border-top: none;
}
.ant-table-body {
scrollbar-color: auto;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,842 @@
import { useContext, useState, useEffect } from 'react'
import {
Descriptions,
Button,
Typography,
Flex,
Space,
Dropdown,
message,
Tag,
Input,
InputNumber,
Select
} from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
import CloudIcon from '../../Icons/CloudIcon.jsx'
import { ApiServerContext } from '../context/ApiServerContext.jsx'
import BoolDisplay from '../common/BoolDisplay.jsx'
import InfoCollapse from '../common/InfoCollapse.jsx'
const { Text, Paragraph } = Typography
const ApiContextDebug = () => {
const {
apiServer,
error,
connecting,
connected,
fetchLoading,
lockObject,
unlockObject,
fetchObjectLock,
updateObject,
createObject,
deleteObject,
subscribeToObjectUpdates,
subscribeToObjectEvent,
subscribeToObjectTypeUpdates,
subscribeToObjectLock,
fetchObject,
fetchObjects,
fetchObjectsByProperty,
fetchSpotlightData,
fetchObjectContent,
fetchTemplatePreview,
fetchNotes,
fetchHostOTP,
sendObjectAction
} = useContext(ApiServerContext)
const [msgApi, contextHolder] = message.useMessage()
const [connectionStatus, setConnectionStatus] = useState('disconnected')
const [socketId, setSocketId] = useState(null)
// Test input states
const [testInputs, setTestInputs] = useState({
objectId: 'test-id',
objectType: 'user',
hostId: 'test-host-id',
parentId: 'test-parent-id',
fileName: 'test.gcode',
query: 'test query',
action: 'start',
eventType: 'status',
properties: ['name', 'email'],
filter: { active: true },
scale: 1.0,
templateContent: 'Test template content',
testObject: { name: 'Test Object' }
})
// Collapse states
const [collapseStates, setCollapseStates] = useState({
fetchTests: false,
crudTests: false,
subscriptionTests: false,
specialTests: false
})
useEffect(() => {
if (apiServer) {
setConnectionStatus(apiServer.connected ? 'connected' : 'disconnected')
setSocketId(apiServer.id)
// Listen for connection status changes
const handleConnect = () => {
setConnectionStatus('connected')
setSocketId(apiServer.id)
}
const handleDisconnect = () => {
setConnectionStatus('disconnected')
setSocketId(null)
}
apiServer.on('connect', handleConnect)
apiServer.on('disconnect', handleDisconnect)
return () => {
apiServer.off('connect', handleConnect)
apiServer.off('disconnect', handleDisconnect)
}
} else {
setConnectionStatus('disconnected')
setSocketId(null)
}
}, [apiServer])
// Helper functions
const updateTestInput = (key, value) => {
setTestInputs((prev) => ({ ...prev, [key]: value }))
}
const toggleCollapse = (key) => {
setCollapseStates((prev) => ({ ...prev, [key]: !prev[key] }))
}
const handleReconnect = () => {
if (apiServer) {
apiServer.connect()
msgApi.info('Attempting to reconnect...')
} else {
msgApi.warning('No API server instance available')
}
}
const handleDisconnect = () => {
if (apiServer) {
apiServer.disconnect()
msgApi.info('Disconnected from API server')
}
}
const testFetchObject = async () => {
try {
msgApi.loading('Testing fetchObject...', 0)
const result = await fetchObject(
testInputs.objectId,
testInputs.objectType
)
msgApi.destroy()
msgApi.success('fetchObject test completed')
console.log('fetchObject result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchObject test failed')
console.error('fetchObject error:', err)
}
}
const testFetchObjects = async () => {
try {
msgApi.loading('Testing fetchObjects...', 0)
const result = await fetchObjects(testInputs.objectType, {
page: 1,
limit: 5
})
msgApi.destroy()
msgApi.success('fetchObjects test completed')
console.log('fetchObjects result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchObjects test failed')
console.error('fetchObjects error:', err)
}
}
const testLockObject = () => {
try {
lockObject(testInputs.objectId, testInputs.objectType)
msgApi.success('Lock command sent')
} catch (err) {
msgApi.error('Lock command failed')
console.error('Lock error:', err)
}
}
const testUnlockObject = () => {
try {
unlockObject(testInputs.objectId, testInputs.objectType)
msgApi.success('Unlock command sent')
} catch (err) {
msgApi.error('Unlock command failed')
console.error('Unlock error:', err)
}
}
const testFetchObjectLock = async () => {
try {
msgApi.loading('Testing fetchObjectLock...', 0)
const result = await fetchObjectLock(
testInputs.objectId,
testInputs.objectType
)
msgApi.destroy()
msgApi.success('fetchObjectLock test completed')
console.log('fetchObjectLock result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchObjectLock test failed')
console.error('fetchObjectLock error:', err)
}
}
const testUpdateObject = async () => {
try {
msgApi.loading('Testing updateObject...', 0)
const testData = {
name: 'Test Update',
updated: new Date().toISOString()
}
const result = await updateObject(
testInputs.objectId,
testInputs.objectType,
testData
)
msgApi.destroy()
msgApi.success('updateObject test completed')
console.log('updateObject result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('updateObject test failed')
console.error('updateObject error:', err)
}
}
const testCreateObject = async () => {
try {
msgApi.loading('Testing createObject...', 0)
const testData = {
name: 'Test Create',
created: new Date().toISOString()
}
const result = await createObject(testInputs.objectType, testData)
msgApi.destroy()
msgApi.success('createObject test completed')
console.log('createObject result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('createObject test failed')
console.error('createObject error:', err)
}
}
const testDeleteObject = async () => {
try {
msgApi.loading('Testing deleteObject...', 0)
const result = await deleteObject(
testInputs.objectId,
testInputs.objectType
)
msgApi.destroy()
msgApi.success('deleteObject test completed')
console.log('deleteObject result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('deleteObject test failed')
console.error('deleteObject error:', err)
}
}
const testFetchObjectsByProperty = async () => {
try {
msgApi.loading('Testing fetchObjectsByProperty...', 0)
const params = {
properties: testInputs.properties,
filter: testInputs.filter,
masterFilter: {}
}
const result = await fetchObjectsByProperty(testInputs.objectType, params)
msgApi.destroy()
msgApi.success('fetchObjectsByProperty test completed')
console.log('fetchObjectsByProperty result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchObjectsByProperty test failed')
console.error('fetchObjectsByProperty error:', err)
}
}
const testFetchSpotlightData = async () => {
try {
msgApi.loading('Testing fetchSpotlightData...', 0)
const result = await fetchSpotlightData(testInputs.query)
msgApi.destroy()
msgApi.success('fetchSpotlightData test completed')
console.log('fetchSpotlightData result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchSpotlightData test failed')
console.error('fetchSpotlightData error:', err)
}
}
const testFetchObjectContent = async () => {
try {
msgApi.loading('Testing fetchObjectContent...', 0)
await fetchObjectContent(
testInputs.objectId,
'gcodefile',
testInputs.fileName
)
msgApi.destroy()
msgApi.success('fetchObjectContent test completed')
} catch (err) {
msgApi.destroy()
msgApi.error('fetchObjectContent test failed')
console.error('fetchObjectContent error:', err)
}
}
const testFetchTemplatePreview = async () => {
try {
msgApi.loading('Testing fetchTemplatePreview...', 0)
fetchTemplatePreview(
testInputs.objectId,
testInputs.templateContent,
testInputs.testObject,
testInputs.scale,
(result) => {
msgApi.destroy()
if (result.success) {
msgApi.success('fetchTemplatePreview test completed')
} else {
msgApi.error('fetchTemplatePreview test failed')
}
console.log('fetchTemplatePreview result:', result)
}
)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchTemplatePreview test failed')
console.error('fetchTemplatePreview error:', err)
}
}
const testFetchNotes = async () => {
try {
msgApi.loading('Testing fetchNotes...', 0)
const result = await fetchNotes(testInputs.parentId)
msgApi.destroy()
msgApi.success('fetchNotes test completed')
console.log('fetchNotes result:', result)
} catch (err) {
msgApi.destroy()
msgApi.error('fetchNotes test failed')
console.error('fetchNotes error:', err)
}
}
const testFetchHostOTP = async () => {
try {
msgApi.loading('Testing fetchHostOTP...', 0)
fetchHostOTP(testInputs.hostId, (result) => {
msgApi.destroy()
if (result.otp) {
msgApi.success('fetchHostOTP test completed: ' + result.otp)
} else {
msgApi.error('fetchHostOTP test failed')
}
console.log('fetchHostOTP result:', result)
})
} catch (err) {
msgApi.destroy()
msgApi.error('fetchHostOTP test failed')
console.error('fetchHostOTP error:', err)
}
}
const testSendObjectAction = async () => {
try {
msgApi.loading('Testing sendObjectAction...', 0)
sendObjectAction(
testInputs.objectId,
'printer',
testInputs.action,
(result) => {
msgApi.destroy()
if (result.success) {
msgApi.success('sendObjectAction test completed')
} else {
msgApi.error('sendObjectAction test failed')
}
console.log('sendObjectAction result:', result)
}
)
} catch (err) {
msgApi.destroy()
msgApi.error('sendObjectAction test failed')
console.error('sendObjectAction error:', err)
}
}
const testSubscribeToObjectUpdates = () => {
try {
const callback = (data) => {
console.log('Object update received:', data)
}
const unsubscribe = subscribeToObjectUpdates(
testInputs.objectId,
testInputs.objectType,
callback
)
msgApi.success('Subscribed to object updates')
console.log('Subscribed to object updates for test-id')
// Store unsubscribe function for cleanup
setTimeout(() => {
if (unsubscribe) {
unsubscribe()
console.log('Unsubscribed from object updates')
}
}, 10000) // Auto-unsubscribe after 10 seconds
} catch (err) {
msgApi.error('Subscribe to object updates failed')
console.error('Subscribe error:', err)
}
}
const testSubscribeToObjectEvent = () => {
try {
const callback = (event) => {
console.log('Object event received:', event)
}
const unsubscribe = subscribeToObjectEvent(
testInputs.objectId,
'printer',
testInputs.eventType,
callback
)
msgApi.success('Subscribed to object events')
console.log('Subscribed to object events for test-id')
// Store unsubscribe function for cleanup
setTimeout(() => {
if (unsubscribe) {
unsubscribe()
console.log('Unsubscribed from object events')
}
}, 10000) // Auto-unsubscribe after 10 seconds
} catch (err) {
msgApi.error('Subscribe to object events failed')
console.error('Subscribe error:', err)
}
}
const testSubscribeToObjectTypeUpdates = () => {
try {
const callback = (data) => {
console.log('Object type update received:', data)
}
const unsubscribe = subscribeToObjectTypeUpdates(
testInputs.objectType,
callback
)
msgApi.success('Subscribed to object type updates')
console.log('Subscribed to object type updates for user')
// Store unsubscribe function for cleanup
setTimeout(() => {
if (unsubscribe) {
unsubscribe()
console.log('Unsubscribed from object type updates')
}
}, 10000) // Auto-unsubscribe after 10 seconds
} catch (err) {
msgApi.error('Subscribe to object type updates failed')
console.error('Subscribe error:', err)
}
}
const testSubscribeToObjectLock = () => {
try {
const callback = (lockData) => {
console.log('Object lock update received:', lockData)
}
const unsubscribe = subscribeToObjectLock(
testInputs.objectId,
testInputs.objectType,
callback
)
msgApi.success('Subscribed to object lock updates')
console.log('Subscribed to object lock updates for test-id')
// Store unsubscribe function for cleanup
setTimeout(() => {
if (unsubscribe) {
unsubscribe()
console.log('Unsubscribed from object lock updates')
}
}, 10000) // Auto-unsubscribe after 10 seconds
} catch (err) {
msgApi.error('Subscribe to object lock updates failed')
console.error('Subscribe error:', err)
}
}
const actionItems = {
items: [
{
label: 'Reconnect',
key: 'reconnect',
disabled: connected
},
{
label: 'Disconnect',
key: 'disconnect',
disabled: !connected
},
{
type: 'divider'
},
{
label: 'Reload',
key: 'reload',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
switch (key) {
case 'reconnect':
handleReconnect()
break
case 'disconnect':
handleDisconnect()
break
case 'reload':
msgApi.info('Reloading API State...')
window.location.reload()
break
default:
break
}
}
}
const getConnectionStatusColor = () => {
switch (connectionStatus) {
case 'connected':
return 'success'
case 'connecting':
return 'processing'
case 'disconnected':
return 'error'
default:
return 'default'
}
}
return (
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
{contextHolder}
{/* Header with Actions */}
<Flex justify={'space-between'} align={'center'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
</Flex>
<Descriptions bordered>
<Descriptions.Item label='Status'>
<Space>
<Tag color={getConnectionStatusColor()} icon={<CloudIcon />}>
{connectionStatus.charAt(0).toUpperCase() +
connectionStatus.slice(1)}
</Tag>
</Space>
</Descriptions.Item>
<Descriptions.Item label='Connecting'>
<BoolDisplay value={connecting} />
</Descriptions.Item>
<Descriptions.Item label='Connected'>
<BoolDisplay value={connected} />
</Descriptions.Item>
<Descriptions.Item label='Socket ID'>
<Text code>{socketId || 'None'}</Text>
</Descriptions.Item>
<Descriptions.Item label='Fetch Loading'>
<BoolDisplay value={fetchLoading} />
</Descriptions.Item>
<Descriptions.Item label='Error'>
<Text type={error ? 'danger' : 'secondary'}>{error || 'None'}</Text>
</Descriptions.Item>
</Descriptions>
<InfoCollapse
title='Params'
icon={<span>🔎</span>}
active={collapseStates.params}
onToggle={() => toggleCollapse('params')}
collapseKey='params'
>
<Descriptions bordered column={2}>
<Descriptions.Item label='Object ID'>
<Input
value={testInputs.objectId}
onChange={(e) => updateTestInput('objectId', e.target.value)}
placeholder='Enter object ID'
size='small'
/>
</Descriptions.Item>
<Descriptions.Item label='Object Type'>
<Select
value={testInputs.objectType}
onChange={(value) => updateTestInput('objectType', value)}
style={{ width: '100%' }}
options={[
{ value: 'user', label: 'User' },
{ value: 'printer', label: 'Printer' },
{ value: 'job', label: 'Job' },
{ value: 'filament', label: 'Filament' },
{ value: 'gcodefile', label: 'GCode File' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Host ID'>
<Input
value={testInputs.hostId}
onChange={(e) => updateTestInput('hostId', e.target.value)}
placeholder='Enter host ID'
/>
</Descriptions.Item>
<Descriptions.Item label='Parent ID'>
<Input
value={testInputs.parentId}
onChange={(e) => updateTestInput('parentId', e.target.value)}
placeholder='Enter parent ID'
/>
</Descriptions.Item>
<Descriptions.Item label='File Name'>
<Input
value={testInputs.fileName}
onChange={(e) => updateTestInput('fileName', e.target.value)}
placeholder='Enter file name'
/>
</Descriptions.Item>
<Descriptions.Item label='Query'>
<Input
value={testInputs.query}
onChange={(e) => updateTestInput('query', e.target.value)}
placeholder='Enter search query'
/>
</Descriptions.Item>
<Descriptions.Item label='Action'>
<Select
value={testInputs.action}
onChange={(value) => updateTestInput('action', value)}
style={{ width: '100%' }}
options={[
{ value: 'start', label: 'Start' },
{ value: 'stop', label: 'Stop' },
{ value: 'pause', label: 'Pause' },
{ value: 'resume', label: 'Resume' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Event Type'>
<Select
value={testInputs.eventType}
onChange={(value) => updateTestInput('eventType', value)}
style={{ width: '100%' }}
options={[
{ value: 'status', label: 'Status' },
{ value: 'progress', label: 'Progress' },
{ value: 'error', label: 'Error' },
{ value: 'complete', label: 'Complete' }
]}
/>
</Descriptions.Item>
<Descriptions.Item label='Scale'>
<InputNumber
value={testInputs.scale}
onChange={(value) => updateTestInput('scale', value)}
min={0.1}
max={10}
step={0.1}
style={{ width: '100%' }}
/>
</Descriptions.Item>
<Descriptions.Item label='Template Content' span={1}>
<Input.TextArea
value={testInputs.templateContent}
onChange={(e) =>
updateTestInput('templateContent', e.target.value)
}
placeholder='Enter template content'
rows={2}
/>
</Descriptions.Item>
</Descriptions>
</InfoCollapse>
{/* Test Sections */}
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
{/* Fetch Tests */}
<InfoCollapse
title='Fetch Tests'
icon={<span>📥</span>}
active={collapseStates.fetchTests}
onToggle={() => toggleCollapse('fetchTests')}
collapseKey='fetchTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testFetchObject} block>
Test fetchObject
</Button>
<Button onClick={testFetchObjects} block>
Test fetchObjects
</Button>
<Button onClick={testFetchObjectLock} block>
Test fetchObjectLock
</Button>
<Button onClick={testFetchObjectsByProperty} block>
Test fetchObjectsByProperty
</Button>
<Button onClick={testFetchSpotlightData} block>
Test fetchSpotlightData
</Button>
<Button onClick={testFetchObjectContent} block>
Test fetchObjectContent
</Button>
<Button onClick={testFetchNotes} block>
Test fetchNotes
</Button>
</Space>
</InfoCollapse>
{/* CRUD Tests */}
<InfoCollapse
title='CRUD Tests'
icon={<span></span>}
active={collapseStates.crudTests}
onToggle={() => toggleCollapse('crudTests')}
collapseKey='crudTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testLockObject} block>
Test Lock Object
</Button>
<Button onClick={testUnlockObject} block>
Test Unlock Object
</Button>
<Button onClick={testUpdateObject} block>
Test updateObject
</Button>
<Button onClick={testCreateObject} block>
Test createObject
</Button>
<Button onClick={testDeleteObject} block>
Test deleteObject
</Button>
</Space>
</InfoCollapse>
{/* Special Tests */}
<InfoCollapse
title='Special Tests'
icon={<span>🔧</span>}
active={collapseStates.specialTests}
onToggle={() => toggleCollapse('specialTests')}
collapseKey='specialTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testFetchTemplatePreview} block>
Test fetchTemplatePreview
</Button>
<Button onClick={testFetchHostOTP} block>
Test fetchHostOTP
</Button>
<Button onClick={testSendObjectAction} block>
Test sendObjectAction
</Button>
</Space>
</InfoCollapse>
{/* Subscription Tests */}
<InfoCollapse
title='Subscription Tests'
icon={<span>📡</span>}
active={collapseStates.subscriptionTests}
onToggle={() => toggleCollapse('subscriptionTests')}
collapseKey='subscriptionTests'
>
<Space direction='vertical' style={{ width: '100%' }}>
<Button onClick={testSubscribeToObjectUpdates} block>
Test Subscribe Object Updates
</Button>
<Button onClick={testSubscribeToObjectEvent} block>
Test Subscribe Object Events
</Button>
<Button onClick={testSubscribeToObjectTypeUpdates} block>
Test Subscribe Object Type Updates
</Button>
<Button onClick={testSubscribeToObjectLock} block>
Test Subscribe Object Lock
</Button>
</Space>
</InfoCollapse>
{/* API Server Instance Info */}
<InfoCollapse
title='API Server Instance'
icon={<span>🖥</span>}
active={false}
onToggle={() => {}}
collapseKey='apiServer'
>
<pre style={{ margin: 0, fontSize: 12 }}>
{apiServer ? (
<Paragraph>
<pre>
{JSON.stringify(
{
connected: apiServer.connected,
id: apiServer.id,
transport: apiServer.io.engine.transport.name,
readyState: apiServer.io.engine.readyState
},
null,
2
)}
</pre>
</Paragraph>
) : (
<Text type='secondary'>No API server instance</Text>
)}
</pre>
</InfoCollapse>
</Flex>
</div>
</Flex>
)
}
export default ApiContextDebug

View File

@ -18,17 +18,17 @@ const items = [
path: '/dashboard/developer/authcontextdebug'
},
{
key: 'printservercontextdebug',
label: 'Print Server Debug',
icon: <Text>🖨</Text>,
path: '/dashboard/developer/printservercontextdebug'
key: 'apicontextdebug',
label: 'API Debug',
icon: <Text>🌐</Text>,
path: '/dashboard/developer/apicontextdebug'
}
]
const routeKeyMap = {
'/dashboard/developer/sessionstorage': 'sessionstorage',
'/dashboard/developer/authcontext': 'authcontextdebug',
'/dashboard/developer/printservercontext': 'printservercontextdebug'
'/dashboard/developer/authcontextdebug': 'authcontextdebug',
'/dashboard/developer/apicontextdebug': 'apicontextdebug'
}
const DeveloperSidebar = (props) => {

View File

@ -1,77 +0,0 @@
import { useContext } from 'react'
import {
Descriptions,
Button,
Typography,
Flex,
Space,
Dropdown,
message
} from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon.jsx'
import { PrintServerContext } from '../context/PrintServerContext.jsx'
import BoolDisplay from '../common/BoolDisplay.jsx'
const { Text, Paragraph } = Typography
const PrintServerContextDebug = () => {
const { printServer, error, connecting } = useContext(PrintServerContext)
const [msgApi, contextHolder] = message.useMessage()
const actionItems = {
items: [
{
label: 'Reload',
key: 'reload',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reload') {
msgApi.info('Reloading Page...')
window.location.reload()
}
}
}
// Helper to display socket info safely
const getSocketInfo = () => {
if (!printServer) return 'n/a'
// Only show safe properties
const { id, connected, disconnected, nsp } = printServer
return JSON.stringify({ id, connected, disconnected, nsp }, null, 2)
}
return (
<Flex vertical gap='large' style={{ height: '100%', minHeight: 0 }}>
{contextHolder}
<Flex justify={'space-between'} align={'center'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Descriptions bordered column={1}>
<Descriptions.Item label='Connected'>
<BoolDisplay value={printServer?.connected || false} />
</Descriptions.Item>
<Descriptions.Item label='Connecting'>
<BoolDisplay value={connecting} />
</Descriptions.Item>
<Descriptions.Item label='Error'>
{error ? <Text type='danger'>{error}</Text> : <Text>n/a</Text>}
</Descriptions.Item>
<Descriptions.Item label='Socket'>
<Paragraph>
<pre>{getSocketInfo()}</pre>
</Paragraph>
</Descriptions.Item>
</Descriptions>
</div>
</Flex>
)
}
export default PrintServerContextDebug

View File

@ -17,6 +17,7 @@ import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
import NoteIcon from '../../../Icons/NoteIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
import DocumentPrintButton from '../../common/DocumentPrintButton'
const FilamentStockInfo = () => {
const location = useLocation()
@ -39,7 +40,8 @@ const FilamentStockInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -75,6 +77,7 @@ const FilamentStockInfo = () => {
type='filamentStock'
id={filamentStockId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -87,6 +90,11 @@ const FilamentStockInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='filamentStock'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -0,0 +1,98 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewDocumentJob from './DocumentJobs/NewDocumentJob'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const DocumentJobs = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('documentJob')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('documentJob')
const actionItems = {
items: [
{
label: 'New Document Job',
key: 'newDocumentJob',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newDocumentJob') {
setNewDocumentJobOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='documentJob'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='documentJob'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newDocumentJobOpen}
onCancel={() => setNewDocumentJobOpen(false)}
footer={null}
destroyOnHidden={true}
width={900}
>
<NewDocumentJob
onOk={() => {
setNewDocumentJobOpen(false)
messageApi.success('New note type created successfully.')
tableRef.current?.reload()
}}
reset={!newDocumentJobOpen}
/>
</Modal>
</>
)
}
export default DocumentJobs

View File

@ -0,0 +1,195 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
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 LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('DocumentJobInfo')
log.setLevel(config.logLevel)
const DocumentJobInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const documentJobId = new URLSearchParams(location.search).get(
'documentJobId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentJobInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.fetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='documentJob'
id={documentJobId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Document Job Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='documentJob'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Document Job Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={documentJobId}
type='documentJob'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='documentJob'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={documentJobId} type='documentJob' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': documentJobId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
</>
)
}
export default DocumentJobInfo

View File

@ -0,0 +1,66 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
import TemplatePreview from '../../common/TemplatePreview'
const NewDocumentJob = ({ onOk, defaultValues = {} }) => {
return (
<NewObjectForm
type={'documentJob'}
defaultValues={{ objectType: 'documentJob', ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='documentJob'
column={1}
visibleProperties={{ name: false }}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
submitText='Print'
title={'Print Document'}
formValid={formValid}
loading={submitLoading}
sideBar={
<div style={{ minWidth: '400px', minHeight: '500px' }}>
<TemplatePreview
objectData={objectData?.object}
documentTemplate={objectData?.documentTemplate}
onPreviewMessage={(message) => {
console.log(message)
}}
/>
</div>
}
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewDocumentJob.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewDocumentJob

View File

@ -18,6 +18,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('DocumentPrinterInfo')
log.setLevel(config.logLevel)
@ -92,6 +93,7 @@ const DocumentPrinterInfo = () => {
type='documentPrinter'
id={documentPrinterId}
disabled={loading}
objectData={objectData}
/>
<ViewButton
disabled={loading}
@ -107,6 +109,11 @@ const DocumentPrinterInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='documentPrinter'
objectData={objectData}
disabled={loading}
/>
</Space>
<LockIndicator lock={lock} />
</Space>

View File

@ -19,6 +19,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('DocumentSizeInfo')
log.setLevel(config.logLevel)
@ -43,7 +44,8 @@ const DocumentSizeInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -79,6 +81,7 @@ const DocumentSizeInfo = () => {
type='documentSize'
id={documentSizeId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -90,6 +93,11 @@ const DocumentSizeInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='documentSize'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -19,6 +19,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('DocumentTemplateInfo')
log.setLevel(config.logLevel)
@ -45,7 +46,8 @@ const DocumentTemplateInfo = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -84,6 +86,7 @@ const DocumentTemplateInfo = () => {
type='documentTemplate'
id={documentTemplateId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -95,6 +98,11 @@ const DocumentTemplateInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='documentTemplate'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -20,6 +20,7 @@ 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 DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('FilamentInfo')
log.setLevel(config.logLevel)
@ -44,7 +45,8 @@ const FilamentInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -83,6 +85,7 @@ const FilamentInfo = () => {
type='filament'
id={filamentId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -95,6 +98,11 @@ const FilamentInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='filament'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -0,0 +1,68 @@
import { useRef } from 'react'
import { Button, Flex, Space, Dropdown } from 'antd'
import ObjectTable from '../common/ObjectTable'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const Files = () => {
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('file')
const [columnVisibility, setColumnVisibility] = useColumnVisibility('file')
const actionItems = {
items: [
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='file'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='file'
cards={viewMode === 'cards'}
/>
</Flex>
</>
)
}
export default Files

View File

@ -0,0 +1,192 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
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 LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('FileInfo')
log.setLevel(config.logLevel)
const FileInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const fileId = new URLSearchParams(location.search).get('fileId')
const [collapseState, updateCollapseState] = useCollapseState('FileInfo', {
info: true,
notes: true,
auditLogs: true
})
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='file'
id={fileId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'File Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='file'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='File Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={fileId}
type='file'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='file'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={fileId} type='file' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': fileId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
</>
)
}
export default FileInfo

View File

@ -20,6 +20,8 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import HostOTP from './HostOtp.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
const log = loglevel.getLogger('HostInfo')
log.setLevel(config.logLevel)
@ -31,7 +33,7 @@ const HostInfo = () => {
const hostId = new URLSearchParams(location.search).get('hostId')
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
info: true,
stocks: true,
printers: true,
notes: true,
auditLogs: true
})
@ -42,7 +44,8 @@ const HostInfo = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -85,17 +88,24 @@ const HostInfo = () => {
type='host'
id={hostId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Host Information' },
{ key: 'printers', label: 'Printers' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='host'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
@ -158,6 +168,27 @@ const HostInfo = () => {
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Printers'
icon={<PrinterIcon />}
active={collapseState.printers}
onToggle={(expanded) => updateCollapseState('printers', expanded)}
collapseKey='printers'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='printer'
masterFilter={{ 'host._id': hostId }}
visibleColumns={{
host: false,
'host._id': false
}}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}

View File

@ -15,6 +15,8 @@ import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon'
import DocumentTemplateIcon from '../../Icons/DocumentTemplateIcon'
import DocumentIcon from '../../Icons/DocumentIcon'
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
import DocumentJobIcon from '../../Icons/DocumentJobIcon'
import FileIcon from '../../Icons/FileIcon'
const items = [
{
@ -65,6 +67,12 @@ const items = [
label: 'Document Printers',
path: '/dashboard/management/documentprinters'
},
{
key: 'documentJobs',
icon: <DocumentJobIcon />,
label: 'Document Jobs',
path: '/dashboard/management/documentjobs'
},
{
key: 'documentTemplates',
icon: <DocumentTemplateIcon />,
@ -79,13 +87,14 @@ const items = [
}
]
},
{ type: 'divider' },
{
key: 'hosts',
icon: <HostIcon />,
label: 'Hosts',
path: '/dashboard/management/hosts'
},
{ type: 'divider' },
{
key: 'users',
icon: <PersonIcon />,
@ -98,6 +107,12 @@ const items = [
label: 'Settings',
path: '/dashboard/management/settings'
},
{
key: 'files',
icon: <FileIcon />,
label: 'Files',
path: '/dashboard/management/files'
},
{
key: 'auditLogs',
icon: <AuditLogIcon />,
@ -128,10 +143,12 @@ const routeKeyMap = {
'/dashboard/management/notetypes': 'noteTypes',
'/dashboard/management/settings': 'settings',
'/dashboard/management/auditlogs': 'auditLogs',
'/dashboard/management/files': 'files',
'/dashboard/management/hosts': 'hosts',
'/dashboard/management/documentsizes': 'documentSizes',
'/dashboard/management/documentprinters': 'documentPrinters',
'/dashboard/management/documenttemplates': 'documentTemplates'
'/dashboard/management/documenttemplates': 'documentTemplates',
'/dashboard/management/documentjobs': 'documentJobs'
}
const ManagementSidebar = (props) => {

View File

@ -15,6 +15,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const NoteTypeInfo = () => {
const location = useLocation()
@ -33,7 +34,8 @@ const NoteTypeInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -69,6 +71,7 @@ const NoteTypeInfo = () => {
type='noteType'
id={noteTypeId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -79,6 +82,11 @@ const NoteTypeInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='noteType'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -18,6 +18,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('NoteInfo')
log.setLevel(config.logLevel)
@ -37,7 +38,8 @@ const NoteInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -77,6 +79,7 @@ const NoteInfo = () => {
type='note'
id={noteId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -88,6 +91,11 @@ const NoteInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='note'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -16,6 +16,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
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 { ApiServerContext } from '../../context/ApiServerContext'
const PartInfo = () => {
@ -78,6 +79,7 @@ const PartInfo = () => {
type='part'
id={partId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -89,6 +91,11 @@ const PartInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='part'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -17,6 +17,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const ProductInfo = () => {
const location = useLocation()
@ -70,6 +71,7 @@ const ProductInfo = () => {
type='product'
id={productId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -82,6 +84,11 @@ const ProductInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='product'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -17,6 +17,7 @@ import ActionHandler from '../../common/ActionHandler'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const UserInfo = () => {
const location = useLocation()
@ -33,7 +34,8 @@ const UserInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -69,6 +71,7 @@ const UserInfo = () => {
type='user'
id={userId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -80,6 +83,11 @@ const UserInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='user'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -18,6 +18,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('VendorInfo')
log.setLevel(config.logLevel)
@ -37,7 +38,8 @@ const VendorInfo = () => {
editLoading: false,
formValid: false,
lock: null,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -77,6 +79,7 @@ const VendorInfo = () => {
type='vendor'
id={vendorId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -88,6 +91,11 @@ const VendorInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='vendor'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import EyeIcon from '../../../Icons/EyeIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const { Text } = Typography
@ -46,7 +47,8 @@ const GCodeFileInfo = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -85,6 +87,7 @@ const GCodeFileInfo = () => {
type='gcodeFile'
id={gcodeFileId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -97,6 +100,11 @@ const GCodeFileInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='gcodeFile'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -20,6 +20,7 @@ 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'
const log = loglevel.getLogger('JobInfo')
log.setLevel(config.logLevel)
@ -41,7 +42,8 @@ const JobInfo = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -80,6 +82,7 @@ const JobInfo = () => {
type='job'
id={jobId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -92,6 +95,11 @@ const JobInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='job'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>

View File

@ -87,6 +87,7 @@ const Printers = () => {
onCancel={() => {
setNewPrinterOpen(false)
}}
destroyOnHidden
>
<NewPrinter
onOk={() => {

View File

@ -10,7 +10,6 @@ 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 LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
@ -26,6 +25,7 @@ import SubJobIcon from '../../../Icons/SubJobIcon.jsx'
import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import { useMediaQuery } from 'react-responsive'
import AlertsDisplay from '../../common/AlertsDisplay.jsx'
const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel)
@ -68,7 +68,8 @@ const ControlPrinter = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -164,8 +165,8 @@ const ControlPrinter = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<AlertsDisplay alerts={objectFormState.objectData?.alerts} />
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons

View File

@ -1,564 +1,109 @@
import { useState, useContext, useEffect, useCallback } from 'react'
import axios from 'axios'
import {
Form,
Button,
message,
Typography,
Flex,
Steps,
Divider,
Input,
Select,
Space,
Descriptions,
List,
InputNumber,
notification,
Progress,
Modal,
Radio
} from 'antd'
import { SearchOutlined, SettingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { PrintServerContext } from '../../context/PrintServerContext'
import EditIcon from '../../../Icons/EditIcon.jsx'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
import config from '../../../../config.js'
const { Title } = Typography
const initialNewPrinterForm = {
const NewPrinter = ({ onOk }) => {
return (
<NewObjectForm
type={'printer'}
defaultValues={{
moonraker: {
protocol: 'ws',
host: '',
port: '',
apiKey: ''
}
}
const NewPrinter = ({ onOk, reset }) => {
NewPrinter.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool.isRequired
}
const { printServer } = useContext(PrintServerContext)
const [messageApi, contextHolder] = message.useMessage()
const [notificationApi, notificationContextHolder] =
notification.useNotification()
const [newPrinterLoading, setNewPrinterLoading] = useState(false)
const [currentStep, setCurrentStep] = useState(0)
const [nextEnabled, setNextEnabled] = useState(false)
const [newPrinterForm] = Form.useForm()
const [newPrinterFormValues, setNewPrinterFormValues] = useState(
initialNewPrinterForm
)
const [discoveredPrinters, setDiscoveredPrinters] = useState([])
const [discovering, setDiscovering] = useState(false)
const [showManualSetup, setShowManualSetup] = useState(false)
const [scanPort, setScanPort] = useState(7125)
const [scanProtocol, setScanProtocol] = useState('ws')
const [editingHostname, setEditingHostname] = useState(null)
const [hostnameInput, setHostnameInput] = useState('')
const [initialized, setInitialized] = useState(false)
const newPrinterFormUpdateValues = Form.useWatch([], newPrinterForm)
useEffect(() => {
newPrinterForm
.validateFields({
validateOnly: true
})
.then(() => {
if (currentStep === 0) {
const moonraker = newPrinterForm.getFieldValue('moonraker')
setNextEnabled(
!!(moonraker?.protocol && moonraker?.host && moonraker?.port)
)
} else if (currentStep === 1) {
const name = newPrinterForm.getFieldValue('name')
setNextEnabled(!!name)
} else {
setNextEnabled(true)
}
})
.catch(() => setNextEnabled(false))
}, [newPrinterForm, newPrinterFormUpdateValues, currentStep])
const summaryItems = [
{
key: 'name',
label: 'Name',
children: newPrinterFormValues.name
port: 7125,
protocol: 'ws'
},
{
key: 'protocol',
label: 'Protocol',
children: newPrinterFormValues.moonraker?.protocol
},
{
key: 'host',
label: 'Host',
children: newPrinterFormValues.moonraker?.host
},
{
key: 'port',
label: 'Port',
children: newPrinterFormValues.moonraker?.port
}
]
useEffect(() => {
if (reset) {
newPrinterForm.resetFields()
}
}, [reset, newPrinterForm])
const handlePrinterSelect = (printer) => {
newPrinterForm.setFieldsValue({
moonraker: {
protocol: printer.protocol,
host: printer.host,
port: printer.port
}
})
setNewPrinterFormValues({
...newPrinterFormValues,
moonraker: {
protocol: printer.protocol,
host: printer.host,
port: printer.port
}
})
}
const handleHostnameEdit = (printer, newHostname) => {
if (newHostname && newHostname.trim() !== '') {
const updatedPrinter = {
...printer,
host: newHostname.trim()
}
setDiscoveredPrinters((prev) =>
prev.map((p) => (p.host === printer.host ? updatedPrinter : p))
)
setEditingHostname(null)
setHostnameInput('')
}
}
const showEditHostnameDialog = (printer) => {
setEditingHostname(printer.host)
setHostnameInput(printer.host)
}
const handleNewPrinter = async () => {
setNewPrinterLoading(true)
try {
await axios.post(
`${config.backendUrl}/printers`,
{
...newPrinterFormValues
},
{
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
onOk()
} catch (error) {
messageApi.error('Error adding new printer: ' + error.message)
} finally {
setNewPrinterLoading(false)
}
}
const notifyScanNetworkFound = useCallback(
(data) => {
const newPrinter = {
protocol: scanProtocol,
host: data.hostname || data.ip,
port: scanPort
}
notificationApi.info({
message: 'Printer Found',
description: `Printer found: ${data.hostname || data.ip}!`
})
setDiscoveredPrinters((prev) => [...prev, newPrinter])
},
[scanProtocol, scanPort, notificationApi]
)
const notifyScanNetworkComplete = useCallback(
(data) => {
setDiscovering(false)
notificationApi.destroy('network-scan')
if (data == false) {
messageApi.error('Error discovering printers!')
} else {
messageApi.success('Finished discovering printers!')
}
},
[messageApi, notificationApi]
)
const notifyScanNetworkProgress = useCallback(
(data) => {
notificationApi.info({
message: 'Scanning Network',
description: (
<div>
<div style={{ marginBottom: 12 }}>
Scanning IP: {data.currentIP}
</div>
<Progress
percent={data.progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'network-scan',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'network-scan-notification',
closeIcon: null,
onClose: () => {},
btn: null
})
},
[notificationApi]
)
const discoverPrinters = useCallback(() => {
if (!discovering) {
setDiscovering(true)
setDiscoveredPrinters([])
messageApi.info('Discovering printers...')
printServer.off('notify_scan_network_found')
printServer.off('notify_scan_network_progress')
printServer.off('notify_scan_network_complete')
printServer.on('notify_scan_network_found', notifyScanNetworkFound)
printServer.on('notify_scan_network_progress', notifyScanNetworkProgress)
printServer.on('notify_scan_network_complete', notifyScanNetworkComplete)
printServer.emit('bridge.scan_network.start', {
port: scanPort,
protocol: scanProtocol
})
}
}, [
discovering,
printServer,
scanPort,
scanProtocol,
messageApi,
notifyScanNetworkFound,
notifyScanNetworkProgress,
notifyScanNetworkComplete
])
useEffect(() => {
setInitialized(true)
if (!initialized) {
discoverPrinters()
}
}, [initialized, discoverPrinters])
const stopDiscovery = () => {
if (discovering) {
setDiscovering(false)
notificationApi.destroy('network-scan')
messageApi.info('Stopping discovery...')
printServer.off('notify_scan_network_found')
printServer.off('notify_scan_network_progress')
printServer.off('notify_scan_network_complete')
printServer.emit('bridge.scan_network.stop', (response) => {
if (response == false) {
messageApi.error('Error stopping discovery!')
}
})
}
}
const handlePortChange = (value) => {
stopDiscovery()
setScanPort(value)
}
const handleProtocolChange = (value) => {
stopDiscovery()
setScanProtocol(value)
}
active: true
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Discovery',
key: 'discovery',
content: (
<>
<Flex vertical style={{ width: '100%' }} gap='large'>
{!showManualSetup ? (
<>
<Flex
style={{ width: '100%' }}
justify='space-between'
align='center'
gap='middle'
>
<Space.Compact>
<InputNumber
min={1}
max={65535}
value={scanPort}
onChange={handlePortChange}
style={{ width: '80px' }}
placeholder='Port'
/>
<Select
value={scanProtocol}
onChange={handleProtocolChange}
options={[
{ value: 'ws', label: 'ws' },
{ value: 'wss', label: 'wss' }
]}
/>
<Button
icon={<SearchOutlined />}
onClick={discoverPrinters}
loading={discovering}
>
{discovering ? 'Discovering...' : 'Discover'}
</Button>
</Space.Compact>
<Button
icon={<SettingOutlined />}
onClick={() => setShowManualSetup(true)}
>
Manual Setup
</Button>
</Flex>
<List
dataSource={discoveredPrinters}
renderItem={(printer) => (
<List.Item
key={`${printer.host}:${printer.port}`}
actions={[
<Radio
key='select'
defaultChecked={
newPrinterFormValues.moonraker?.host ===
printer.host
}
onChange={() => handlePrinterSelect(printer)}
/>
]}
>
<List.Item.Meta
title={
<Space>
{printer.host}
{!printer.hostname && (
<Button
type='text'
icon={<EditIcon />}
onClick={() => showEditHostnameDialog(printer)}
/>
)}
</Space>
}
description={`Protocol: ${printer.protocol}, Port: ${printer.port}`}
/>
</List.Item>
)}
/>
<Modal
title='Edit Host'
open={editingHostname !== null}
onOk={() => {
const printer = discoveredPrinters.find(
(p) => p.host === editingHostname
)
if (printer) {
handleHostnameEdit(printer, hostnameInput)
}
}}
onCancel={() => {
setEditingHostname(null)
setHostnameInput('')
}}
>
<Form.Item label='Host' required>
<Input
value={hostnameInput}
onChange={(e) => setHostnameInput(e.target.value)}
placeholder='Enter host'
autoFocus
/>
</Form.Item>
</Modal>
</>
) : (
<>
<Flex style={{ width: '100%' }} justify='end'>
<Button
icon={<SearchOutlined />}
onClick={() => setShowManualSetup(false)}
>
Back to Discovery
</Button>
</Flex>
<Flex vertical>
<Form.Item
label='Protocol'
name={['moonraker', 'protocol']}
rules={[
{ required: true, message: 'Protocol is required' }
]}
>
<Select
defaultValue='ws'
options={[
{ value: 'ws', label: 'Websocket' },
{ value: 'wss', label: 'Websocket Secure' }
]}
/>
</Form.Item>
<Form.Item
label='Host'
name={['moonraker', 'host']}
rules={[{ required: true, message: 'Host is required' }]}
>
<Input />
</Form.Item>
<Form.Item
label='Port'
name={['moonraker', 'port']}
rules={[{ required: true, message: 'Port is required' }]}
>
<InputNumber
min={1}
max={65535}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
label='API Key'
name={['moonraker', 'apiKey']}
rules={[{ required: false }]}
>
<Input.Password
placeholder='Optional API key'
style={{ width: '100%' }}
/>
</Form.Item>
</Flex>
</>
)}
</Flex>
</>
)
},
{
title: 'Required',
key: 'required',
content: (
<>
<Form.Item
label='Name'
name='name'
rules={[{ required: true, message: 'Name is required' }]}
>
<Input />
</Form.Item>
</>
<ObjectInfo
type='printer'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='printer'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
firmware: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false,
alerts: false
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<Form.Item>
<Descriptions column={1} items={summaryItems} size={'small'} />
</Form.Item>
<ObjectInfo
type='printer'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
connectedAt: false,
state: false,
firmware: false,
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false,
alerts: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<Flex gap={'middle'}>
{contextHolder}
{notificationContextHolder}
<div style={{ minWidth: '160px' }}>
<Steps current={currentStep} items={steps} direction='vertical' />
</div>
<Divider type={'vertical'} style={{ height: 'unset' }} />
<Flex vertical={'true'} style={{ flexGrow: 1 }}>
<Title level={2} style={{ marginTop: 0 }}>
New Printer
</Title>
<Form
name='basic'
autoComplete='off'
form={newPrinterForm}
onFinish={handleNewPrinter}
onValuesChange={(changedValues) =>
setNewPrinterFormValues((prevValues) => ({
...prevValues,
...changedValues
}))
}
initialValues={initialNewPrinterForm}
>
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
<Flex justify={'end'}>
<Button
style={{
margin: '0 8px'
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Printer'
onSubmit={() => {
handleSubmit()
onOk()
}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!(currentStep > 0)}
>
Previous
</Button>
{currentStep < steps.length - 1 && (
<Button
type='primary'
disabled={!nextEnabled}
onClick={() => {
setCurrentStep(currentStep + 1)
/>
)
}}
>
Next
</Button>
)}
{currentStep === steps.length - 1 && (
<Button
type='primary'
htmlType='submit'
loading={newPrinterLoading}
>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Flex>
</NewObjectForm>
)
}
NewPrinter.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewPrinter

View File

@ -19,6 +19,7 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
const log = loglevel.getLogger('PrinterInfo')
log.setLevel(config.logLevel)
@ -40,7 +41,8 @@ const PrinterInfo = () => {
editLoading: false,
formValid: false,
locked: false,
loading: false
loading: false,
objectData: {}
})
const actions = {
@ -80,6 +82,7 @@ const PrinterInfo = () => {
type='printer'
id={printerId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -91,6 +94,11 @@ const PrinterInfo = () => {
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='printer'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
@ -146,6 +154,15 @@ const PrinterInfo = () => {
isEditing={isEditing}
type='printer'
objectData={objectData}
visibleProperties={{
currentFilamentStock: false,
'currentFilamentStock._id': false,
currentJob: false,
'currentJob._id': false,
currentSubJob: false,
'currentSubJob._id': false,
alerts: false
}}
/>
)
}}

View File

@ -0,0 +1,52 @@
import PropTypes from 'prop-types'
import { Flex, Alert } from 'antd'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
const AlertsDisplay = ({ alerts = [] }) => {
const getAlertType = (type, priority) => {
if (type === 'error' || priority === '9') return 'error'
if (type === 'warning' || priority === '8') return 'warning'
return 'info'
}
const getAlertIcon = (type, priority) => {
if (type === 'error' || priority === '9') return <ExclamationOctagonIcon />
if (type === 'warning' || priority === '8')
return <ExclamationOctagonIcon />
return <InfoCircleIcon />
}
if (alerts.length == 0) {
return null
}
return (
<Flex gap='small'>
{alerts.map((alert, index) => (
<Alert
key={`${alert.createdAt}-${index}`}
message={alert.message}
style={{ padding: '4px 10px 4px 8px' }}
type={getAlertType(alert.type, alert.priority)}
icon={getAlertIcon(alert.type, alert.priority)}
showIcon
/>
))}
</Flex>
)
}
AlertsDisplay.propTypes = {
alerts: PropTypes.arrayOf(
PropTypes.shape({
priority: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired,
message: PropTypes.string.isRequired
})
).isRequired
}
export default AlertsDisplay

View File

@ -0,0 +1,136 @@
import PropTypes from 'prop-types'
import { useState, useEffect, useContext } from 'react'
// import { getModelByName } from '../../../database/ObjectModels'
import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon'
import { Button, Dropdown, Modal } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import DocumentTemplateIcon from '../../Icons/DocumentTemplateIcon'
import NewDocumentJob from '../Management/DocumentJobs/NewDocumentJob'
import { message } from 'antd'
import { AuthContext } from '../context/AuthContext'
const DocumentPrintButton = ({
type,
objectData,
disabled = false,
...buttonProps
}) => {
const { fetchObjects } = useContext(ApiServerContext)
const [documentTemplates, setDocumentTemplates] = useState([])
const [currentDocumentTemplate, setCurrentDocumentTemplate] = useState(null)
const [loading, setLoading] = useState(false)
const [messageApi, contextHolder] = message.useMessage()
const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false)
const { token } = useContext(AuthContext)
// Get the model by name
//const model = getModelByName(type)
// Fetch document templates when component mounts or type changes
useEffect(() => {
const loadDocumentTemplates = async () => {
if (!type || token == null) return
setLoading(true)
try {
const result = await fetchObjects('documentTemplate', {
filter: {
objectType: type,
global: false,
active: true
},
limit: 100 // Get more templates to show in dropdown
})
if (result && result.data) {
setDocumentTemplates(result.data)
}
} catch (error) {
console.error('Error fetching document templates:', error)
} finally {
setLoading(false)
}
}
loadDocumentTemplates()
}, [type, fetchObjects, token])
// 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.
}
// Create dropdown menu items
const menuItems = documentTemplates.map((template) => ({
key: template._id,
label: template.name,
icon: <DocumentTemplateIcon />,
onClick: () => handleTemplateSelect(template)
}))
// If no templates available, show disabled state
if (documentTemplates.length === 0 && !loading) {
return (
<Button
{...buttonProps}
icon={<DocumentPrinterIcon />}
disabled={true}
title='No document templates available for this object type'
/>
)
}
return (
<>
{contextHolder}
<Dropdown
menu={{
items: menuItems,
loading: loading
}}
trigger={['hover']}
disabled={disabled || loading}
>
<Button
{...buttonProps}
icon={<DocumentPrinterIcon />}
disabled={disabled || loading}
loading={loading}
title={loading ? 'Loading templates...' : 'Print document'}
/>
</Dropdown>
<Modal
open={newDocumentJobOpen}
onCancel={() => setNewDocumentJobOpen(false)}
footer={null}
destroyOnHidden={true}
width={900}
>
<NewDocumentJob
onOk={() => {
setNewDocumentJobOpen(false)
messageApi.success('New document job created successfully.')
}}
reset={!newDocumentJobOpen}
defaultValues={{
objectType: type,
object: objectData,
documentTemplate: currentDocumentTemplate
}}
/>
</Modal>
</>
)
}
DocumentPrintButton.propTypes = {
type: PropTypes.string.isRequired,
disabled: PropTypes.bool,
objectData: PropTypes.object.isRequired
}
export default DocumentPrintButton

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useContext } from 'react'
import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import PropTypes from 'prop-types'
import merge from 'lodash/merge'
/**
* NewObjectForm is a reusable form component for creating new objects.
@ -66,7 +67,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
layout='vertical'
style={style}
onValuesChange={(values) => {
setObjectData((prev) => ({ ...prev, ...values }))
setObjectData((prev) => {
return merge({}, prev, values)
})
}}
>
{contextHolder}

View File

@ -39,7 +39,8 @@ import CheckIcon from '../../Icons/CheckIcon'
import { useNavigate } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import unionBy from 'lodash/unionBy'
import { ElectronContext } from '../context/ElectronContext'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@ -58,6 +59,7 @@ const ObjectTable = forwardRef(
ref
) => {
const { token } = useContext(AuthContext)
const { isElectron } = useContext(ElectronContext)
const {
fetchObjects,
connected,
@ -73,6 +75,15 @@ const ObjectTable = forwardRef(
if (cards) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 280px)'
}
if (isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 244px)'
}
if (isMobile && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
}
if (cards && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 260px)'
}
const [, contextHolder] = message.useMessage()
const tableRef = useRef(null)
const model = getModelByName(type)
@ -108,7 +119,16 @@ const ObjectTable = forwardRef(
const renderActions = (objectData) => {
return (
<Flex gap='small' align='center' justify='center'>
{rowActions.map((action, index) => (
{rowActions.map((action, index) => {
var disabled = false
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled = action.disabled(objectData)
} else {
disabled = action.disabled
}
}
return (
<Tooltip key={index} title={action.label} arrow={false}>
<Button
icon={
@ -118,6 +138,7 @@ const ObjectTable = forwardRef(
<QuestionCircleIcon />
)
}
disabled={disabled}
type={'text'}
size={'small'}
onClick={() => {
@ -127,7 +148,8 @@ const ObjectTable = forwardRef(
}}
/>
</Tooltip>
))}
)
})}
</Flex>
)
}
@ -203,8 +225,6 @@ const ObjectTable = forwardRef(
const loadNextPage = useCallback(() => {
const highestPage = Math.max(...pages.map((p) => p.pageNum))
const nextPage = highestPage + 1
logger.debug('Next page', nextPage)
if (hasMore) {
setPages((prev) => {
const filteredPages = prev.map((page) => ({
@ -281,7 +301,6 @@ const ObjectTable = forwardRef(
const reload = useCallback(async () => {
setLazyLoading(true)
console.log('Pages during reload', pagesRef.current)
for (let i = 0; i < pagesRef.current.length; i++) {
const page = pagesRef.current[i]
await fetchData(page.pageNum)
@ -290,14 +309,14 @@ const ObjectTable = forwardRef(
// Update event handler for real-time updates
const updateEventHandler = useCallback((id, updatedData) => {
console.log('GOT UPDATE FOR', id)
setPages((prevPages) =>
prevPages.map((page) => {
const updatedItems = unionBy(
[{ ...updatedData, _id: id }],
page.items,
'_id'
)
const updatedItems = page.items.map((item) => {
if (item._id === id) {
return { ...item, ...updatedData }
}
return item
})
return {
...page,
items: updatedItems
@ -310,7 +329,6 @@ const ObjectTable = forwardRef(
updateEventHandlerRef.current = updateEventHandler
const newEventHandler = useCallback(() => {
console.log('GOT NEW EVENT')
reload()
}, [reload])
@ -331,7 +349,6 @@ const ObjectTable = forwardRef(
// Subscribe to new items only
newItemIds.forEach((itemId) => {
console.log('SUB', itemId)
const unsubscribe = subscribeToObjectUpdates(
itemId,
type,
@ -357,7 +374,6 @@ const ObjectTable = forwardRef(
subscribedIdsRef.current.splice(index, 1)
const unsubscribe = unsubscribesRef.current[index]
if (unsubscribe) {
console.log('UNSUB', itemId)
unsubscribe()
}
unsubscribesRef.current.splice(index, 1)
@ -369,12 +385,9 @@ const ObjectTable = forwardRef(
// Cleanup effect for component unmount
useEffect(() => {
return () => {
console.log('API: Call unsub', connected)
if (connected == true && unsubscribesRef.current) {
console.log('API: Got object unsub')
// Clean up all subscriptions when component unmounts
unsubscribesRef.current.forEach((unsubscribe) => {
console.log('CALLING UNSUB on unmount')
if (unsubscribe) unsubscribe()
})
unsubscribesRef.current = []
@ -383,8 +396,6 @@ const ObjectTable = forwardRef(
// Clean up type subscription
}
if (connected == true && subscribeToObjectTypeUpdatesRef.current) {
console.log('UNSUBBING type subscription on unmount')
console.log('API: Got type unsub')
subscribeToObjectTypeUpdatesRef.current()
subscribeToObjectTypeUpdatesRef.current = null
}
@ -396,10 +407,6 @@ const ObjectTable = forwardRef(
connected == true &&
subscribeToObjectTypeUpdatesRef.current == null
) {
console.log(
'API: Subbing to updates',
subscribeToObjectTypeUpdatesRef.current
)
subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
type,
newEventHandler
@ -521,7 +528,6 @@ const ObjectTable = forwardRef(
]
useEffect(() => {
console.log('Pages', pages)
pagesRef.current = pages
}, [pages])
// Add columns in the order specified by model.columns
@ -765,6 +771,7 @@ const ObjectTable = forwardRef(
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={isElectron ? 'small' : 'middle'}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>

View File

@ -24,6 +24,16 @@ import config from '../../../config'
import loglevel from 'loglevel'
import { ElectronContext } from './ElectronContext'
import { useLocation, useNavigate } from 'react-router-dom'
import {
getAuthCookies,
setAuthCookies,
clearAuthCookies,
areCookiesEnabled,
validateAuthCookies,
setupCookieSync,
checkAuthCookiesExpiry
} from '../../../utils/cookies'
const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel)
@ -37,7 +47,7 @@ const AuthProvider = ({ children }) => {
notification.useNotification()
const [authenticated, setAuthenticated] = useState(false)
const [initialized, setInitialized] = useState(false)
const [retreivedTokenFromSession, setRetreivedTokenFromSession] =
const [retreivedTokenFromCookies, setRetreivedTokenFromCookies] =
useState(false)
const [loading, setLoading] = useState(false)
const [token, setToken] = useState(null)
@ -60,13 +70,32 @@ const AuthProvider = ({ children }) => {
if (isElectron == true && import.meta.env.MODE != 'development') {
redirectType = 'app-scheme'
}
// Read token from session storage if present
// Check if cookies are enabled and show warning if not
useEffect(() => {
const storedToken = sessionStorage.getItem('authToken')
const storedUser = JSON.parse(sessionStorage.getItem('user'))
const storedExpiresAt = sessionStorage.getItem('authExpiresAt')
console.log('stored user', storedUser, storedToken)
if (storedToken && storedExpiresAt && storedUser) {
if (!areCookiesEnabled()) {
messageApi.warning(
'Cookies are disabled. Login state may not persist between tabs.'
)
}
}, [messageApi])
// Read token from cookies if present
useEffect(() => {
try {
// First validate existing cookies to clean up expired ones
if (validateAuthCookies()) {
const {
token: storedToken,
expiresAt: storedExpiresAt,
user: storedUser
} = getAuthCookies()
console.log('Retrieved from cookies:', {
storedUser,
storedToken,
storedExpiresAt
})
setToken(storedToken)
setUserProfile(storedUser)
setExpiresAt(storedExpiresAt)
@ -76,17 +105,63 @@ const AuthProvider = ({ children }) => {
setUserProfile(null)
setShowUnauthorizedModal(true)
}
setRetreivedTokenFromSession(true)
} catch (error) {
console.error('Error reading auth cookies:', error)
clearAuthCookies()
setAuthenticated(false)
setUserProfile(null)
setShowUnauthorizedModal(true)
}
setRetreivedTokenFromCookies(true)
}, [])
// Set up cookie synchronization between tabs
useEffect(() => {
const cleanupCookieSync = setupCookieSync(() => {
// When cookies change in another tab, re-validate and update state
try {
if (validateAuthCookies()) {
const {
token: newToken,
expiresAt: newExpiresAt,
user: newUser
} = getAuthCookies()
if (
newToken !== token ||
newExpiresAt !== expiresAt ||
JSON.stringify(newUser) !== JSON.stringify(userProfile)
) {
setToken(newToken)
setExpiresAt(newExpiresAt)
setUserProfile(newUser)
setAuthenticated(true)
console.log('Auth state synchronized from another tab')
}
} else {
// Cookies are invalid, clear state
setToken(null)
setExpiresAt(null)
setUserProfile(null)
setAuthenticated(false)
setShowUnauthorizedModal(true)
console.log(
'Auth state cleared due to invalid cookies from another tab'
)
}
} catch (error) {
console.error('Error syncing auth state:', error)
}
})
return cleanupCookieSync
}, [token, expiresAt, userProfile])
const logout = useCallback((redirectUri = '/login') => {
setAuthenticated(false)
setToken(null)
setExpiresAt(null)
setUserProfile(null)
sessionStorage.removeItem('authToken')
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
clearAuthCookies()
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
}, [])
@ -128,18 +203,20 @@ const AuthProvider = ({ children }) => {
if (response.status === 200 && response.data) {
logger.debug('Got auth token!')
setToken(response.data.access_token)
setExpiresAt(response.data.expires_at)
setUserProfile(response.data)
sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
const userObject = {
...response.data,
access_token: undefined,
refresh_token: undefined,
id_token: undefined
const authData = response.data
setToken(authData.access_token)
setExpiresAt(authData.expires_at)
setUserProfile(authData)
// Store in cookies for persistence between tabs
const cookieSuccess = setAuthCookies(authData)
if (!cookieSuccess) {
messageApi.warning(
'Authentication successful but failed to save login state. You may need to log in again if you close this tab.'
)
}
sessionStorage.setItem('user', JSON.stringify(userObject))
const searchParams = new URLSearchParams(location.search)
searchParams.delete('authCode')
const newSearch = searchParams.toString()
@ -167,8 +244,9 @@ const AuthProvider = ({ children }) => {
setLoading(false)
}
},
[isElectron]
[isElectron, navigate, location.search, location.pathname, messageApi]
)
// Function to check if the user is logged in
const checkAuthStatus = useCallback(async () => {
setLoading(true)
@ -183,12 +261,19 @@ const AuthProvider = ({ children }) => {
if (response.status === 200 && response.data) {
logger.debug('Got auth token!')
setToken(response.data.access_token)
setExpiresAt(response.data.expires_at)
setUserProfile(response.data)
sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
sessionStorage.setItem('user', response.data)
const authData = response.data
setToken(authData.access_token)
setExpiresAt(authData.expires_at)
setUserProfile(authData)
// Update cookies with fresh data
const cookieSuccess = setAuthCookies(authData)
if (!cookieSuccess) {
messageApi.warning(
'Failed to update login state. You may need to log in again if you close this tab.'
)
}
} else {
setAuthenticated(false)
setAuthError('Failed to authenticate user.')
@ -206,15 +291,13 @@ const AuthProvider = ({ children }) => {
} finally {
setLoading(false)
}
}, [token])
}, [token, messageApi])
const setUnauthenticated = () => {
setToken(null)
setExpiresAt(null)
setUserProfile(null)
sessionStorage.removeItem('authToken')
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
clearAuthCookies()
setShowUnauthorizedModal(true)
}
@ -227,15 +310,23 @@ const AuthProvider = ({ children }) => {
}
})
if (response.status === 200 && response.data) {
setToken(response.data.access_token)
setExpiresAt(response.data.expires_at)
sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
const authData = response.data
setToken(authData.access_token)
setExpiresAt(authData.expires_at)
// Update cookies with fresh token data
const cookieSuccess = setAuthCookies(authData)
if (!cookieSuccess) {
messageApi.warning(
'Failed to update login state. You may need to log in again if you close this tab.'
)
}
}
} catch (error) {
console.error('Token refresh failed', error)
}
}, [token])
}, [token, messageApi])
const handleSessionExpiredModalOk = () => {
setShowSessionExpiredModal(false)
@ -315,6 +406,55 @@ const AuthProvider = ({ children }) => {
notificationApi.destroy('token-expiration')
}
}
} else {
// Check cookies directly if expiresAt is not set in state
const expiryInfo = checkAuthCookiesExpiry(5) // Check if expiring within 5 minutes
if (expiryInfo.isExpiringSoon && expiryInfo.minutesRemaining <= 1) {
// Show notification for cookies expiring soon
const seconds = Math.floor((expiryInfo.timeRemaining % 60000) / 1000)
const totalSeconds = 60
const remainingSeconds = totalSeconds - seconds
const progress = (remainingSeconds / totalSeconds) * 100
notificationApi.info({
message: 'Session Expiring Soon',
description: (
<div>
<div style={{ marginBottom: 8 }}>
Your session will expire in {seconds} seconds
</div>
<Progress
percent={progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'token-expiration',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'token-expiration-notification',
closeIcon: null,
onClose: () => {},
btn: (
<Button
type='primary'
size='small'
onClick={() => {
notificationApi.destroy('token-expiration')
refreshToken()
}}
>
Reload Session
</Button>
)
})
}
}
}
@ -335,12 +475,12 @@ const AuthProvider = ({ children }) => {
getLoginToken(authCode)
} else if (
token == null &&
retreivedTokenFromSession == true &&
retreivedTokenFromCookies == true &&
initialized == false &&
authCode == null
) {
setInitialized(true)
console.log('Showing unauth')
console.log('Showing unauth', token, authCode)
setShowUnauthorizedModal(true)
setAuthenticated(false)
}
@ -352,7 +492,7 @@ const AuthProvider = ({ children }) => {
location.pathname,
navigate,
token,
retreivedTokenFromSession
retreivedTokenFromCookies
])
return (

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/documentjobicon.svg?react'
const DocumentJobIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default DocumentJobIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/fileicon.svg?react'
const FileIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default FileIcon

View File

@ -0,0 +1,119 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
export const DocumentJob = {
name: 'documentJob',
label: 'Document Job',
prefix: 'DSZ',
icon: DocumentJobIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`
}
],
columns: ['name', '_id', 'width', 'height', 'createdAt', 'updatedAt'],
filters: ['name', '_id', 'width', 'height'],
sorters: ['name', 'width', 'height', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'documentJob',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text',
columnWidth: 200,
columnFixed: 'left',
value: (objectData) => {
return `${objectData?.documentTemplate?.name || 'No template'} (${objectData?.object?.name || 'No name'})`
}
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'objectType',
label: 'Object Type',
required: true,
columnWidth: 150,
type: 'objectType'
},
{
name: 'object',
label: 'Object',
required: true,
columnWidth: 150,
type: 'object',
objectType: (objectData) => {
return objectData?.objectType
}
},
{
name: 'documentTemplate',
label: 'Template',
required: true,
columnWidth: 150,
type: 'object',
objectType: 'documentTemplate',
masterFilter: (objectData) => {
return {
active: true,
global: false,
objectType: objectData.objectType
}
}
},
{
name: 'documentPrinter',
label: 'Printer',
required: true,
columnWidth: 150,
type: 'object',
objectType: 'documentPrinter',
masterFilter: () => {
return {
active: true,
online: true
}
}
}
]
}

119
src/database/models/File.js Normal file
View File

@ -0,0 +1,119 @@
import FileIcon from '../../components/Icons/FileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const File = {
name: 'file',
label: 'File',
prefix: 'VEN',
icon: FileIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/files/info?fileId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/files/info?fileId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) => `/dashboard/management/files/info?fileId=${_id}&action=edit`
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/files/info?fileId=${_id}&action=delete`
}
],
url: (id) => `/dashboard/management/files/info?fileId=${id}`,
columns: ['name', '_id', 'type', 'size', 'temp', 'createdAt'],
filters: ['name', '_id', 'type', 'temp'],
sorters: ['name', 'type', 'size', 'createdAt', 'temp'],
group: ['type'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'file',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'type',
label: 'Type',
type: 'text',
readOnly: true,
required: true
},
{
name: 'size',
label: 'Size',
type: 'number',
readOnly: true,
required: true,
suffix: () => {
return 'gb'
}
},
{
name: 'email',
label: 'Email',
columnWidth: 300,
type: 'email',
readOnly: false,
required: false
},
{
name: 'phone',
label: 'Phone',
type: 'phone',
readOnly: false,
required: false
},
{
name: 'website',
label: 'Website',
columnWidth: 300,
type: 'url',
readOnly: false,
required: false
}
]
}

View File

@ -1,7 +1,7 @@
import { Route } from 'react-router-dom'
import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx'
import AuthContextDebug from '../components/Dashboard/Developer/AuthContextDebug.jsx'
import PrintServerContextDebug from '../components/Dashboard/Developer/PrintServerContextDebug.jsx'
import ApiContextDebug from '../components/Dashboard/Developer/ApiContextDebug.jsx'
const DeveloperRoutes = [
<Route
@ -15,9 +15,9 @@ const DeveloperRoutes = [
element={<AuthContextDebug />}
/>,
<Route
key='printservercontextdebug'
path='developer/printservercontextdebug'
element={<PrintServerContextDebug />}
key='apicontextdebug'
path='developer/apicontextdebug'
element={<ApiContextDebug />}
/>
]

View File

@ -24,7 +24,11 @@ import DocumentTemplates from '../components/Dashboard/Management/DocumentTempla
import DocumentTemplateInfo from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx'
import DocumentPrinters from '../components/Dashboard/Management/DocumentPrinters.jsx'
import DocumentPrinterInfo from '../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx'
import DocumentJobs from '../components/Dashboard/Management/DocumentJobs.jsx'
import DocumentJobInfo from '../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx'
import DocumentTemplateDesign from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx'
import Files from '../components/Dashboard/Management/Files.jsx'
import FileInfo from '../components/Dashboard/Management/Files/FileInfo.jsx'
const ManagementRoutes = [
<Route key='filaments' path='management/filaments' element={<Filaments />} />,
@ -62,6 +66,12 @@ const ManagementRoutes = [
path='management/vendors/info'
element={<VendorInfo />}
/>,
<Route key='files' path='management/files' element={<Files />} />,
<Route
key='files-info'
path='management/files/info'
element={<FileInfo />}
/>,
<Route key='materials' path='management/materials' element={<Materials />} />,
<Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />,
<Route
@ -100,6 +110,16 @@ const ManagementRoutes = [
path='management/documentprinters/info'
element={<DocumentPrinterInfo />}
/>,
<Route
key='documentjobs'
path='management/documentjobs'
element={<DocumentJobs />}
/>,
<Route
key='documentjobs-info'
path='management/documentjobs/info'
element={<DocumentJobInfo />}
/>,
<Route
key='documenttemplates-design'
path='management/documenttemplates/design'