Compare commits

...

20 Commits

Author SHA1 Message Date
899444e815 Enhance production sidebar by adding Printer Profiles and Filament Profiles sections, improving navigation for users. Update cookie utility functions to handle authentication expiry and max age more effectively, ensuring better session management.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
2026-07-20 02:11:15 +01:00
ce611e52f3 Add PrinterProfile and FilamentProfile to ObjectModels; enhance GCodeFile value handling 2026-07-20 02:11:09 +01:00
d7ad2208d4 Enhance GCodeFile, DeployJob, NewJob, and ControlPrinter components with slicer integration support
- Updated NewGCodeFile and NewJob components to include slicerIntegration, currentStep, and onStepChange props for improved workflow management.
- Modified DeployJob to handle printer online status and added slicerIntegration support for conditional rendering of actions and steps.
- Enhanced ControlPrinter to manage visibility of actions based on slicerIntegration, improving user experience in printer control.
- Refactored form handling to pass result objects to onOk callbacks, ensuring better data handling upon submission.
2026-07-20 02:11:01 +01:00
def9b9aefb Add Filament and Printer Profile components with associated functionality
- Introduced FilamentProfiles and PrinterProfiles components for managing filament and printer profiles, respectively.
- Implemented modal dialogs for creating and editing profiles, enhancing user interaction.
- Added ObjectTable for displaying profiles with action handling for edit and delete operations.
- Integrated context for API server interactions and improved state management for profile visibility and actions.
- Created detailed info components for both filament and printer profiles, allowing for comprehensive data management and display.
2026-07-20 02:10:41 +01:00
830cf5224a Add FilamentProfileIcon component for improved icon representation
- Introduced a new FilamentProfileIcon component that utilizes a custom SVG icon for better visual representation in the application.
- The component is designed to be reusable and integrates seamlessly with existing icon functionality.
2026-07-20 02:10:15 +01:00
c0a6968045 Add Slicer Integration feature with lazy loading and routing support
- Introduced SlicerIntegration component for managing slicer uploads and printer control.
- Added SlicerUploadFlow component to handle the upload process and job creation.
- Updated App.jsx to include a new route for the SlicerIntegration component.
- Enhanced ProductionRoutes with new routes for PrinterProfiles and FilamentProfiles, improving navigation and organization of printer-related features.
2026-07-20 02:09:59 +01:00
f5dffd5512 Update NewPrinter component to pass the result object to onOk callback upon successful submission, enhancing the handling of printer creation. 2026-07-20 02:09:20 +01:00
31f90000e4 Enhance App.css by adding styles for subSteps in the timeline component, including adjustments for item appearance, padding, and background colors. Introduce new styles for step indicators and improve visual consistency for completed steps, enhancing overall user experience. 2026-07-20 02:08:52 +01:00
48ff9b83e8 Enhance AuthContext component by adding support for processing authentication codes from URL parameters. Introduce a reference to track processed auth codes, ensuring proper handling of new logins and session management. Update modal visibility logic for unauthorized access and improve loading state handling. 2026-07-20 02:08:44 +01:00
938bea5342 Refactor ObjectSelect component to improve ID comparison and ensure case-insensitive matching. Update value and key properties to use lowercase string representation of object IDs for consistent handling. 2026-07-20 02:07:19 +01:00
8dc74f1a8e Enhance ObjectTable component by adding support for action handling with new props for row actions and visibility of action buttons. Refactor skeleton data creation and improve loading logic for better performance and user experience during data fetching. 2026-07-20 02:07:10 +01:00
46fd3b0c8e Enhance ObjectProperty component by adding support for min/max value handling and array input/display functionality. Introduce new MinMaxDisplay, MinMaxInput, ArrayDisplay, and ArrayInput components, improving data entry and visualization capabilities. Update validation logic for min/max and list inputs to ensure data integrity. 2026-07-20 02:06:40 +01:00
a38d9c91f6 Enhance ObjectInfo component by adding support for property definitions and controlled property changes. Introduce new props for parent data and property change handling, improving flexibility and data management in the component. 2026-07-20 02:06:22 +01:00
37d6ba9148 Add primary color override and disabled color properties in ThemeContext. Update CSS custom properties for primary and disabled colors based on theme mode and user-defined overrides, enhancing theme customization capabilities. 2026-07-20 02:06:13 +01:00
ca0128d986 Enhance ObjectChildTable component by adding detail modal functionality for property editing and viewing. Introduce new icons for edit and view actions, improve property resolution logic, and streamline item removal handling. Update table column rendering to accommodate hidden properties, enhancing user interaction and data management. 2026-07-20 02:06:04 +01:00
9442c9105b Enhance WizardView component by adding support for controlled current step management and optional sub-step descriptions using Timeline. Introduce new props for sidebar width, button visibility, and step skipping functionality, improving user interaction and flexibility in the wizard flow. 2026-07-20 02:05:58 +01:00
0c6fe5cbac Add ArrayDisplay and ArrayInput components for enhanced array handling. ArrayDisplay provides a visual representation of array items with optional prefix and suffix, while ArrayInput allows dynamic addition and removal of items, supporting both text and numeric inputs for improved user interaction. 2026-07-20 02:05:51 +01:00
8cb2bea2d3 Fix InputNumberCal component to handle empty string input correctly and streamline value handling in the InputNumber component for improved data entry experience. 2026-07-20 02:05:40 +01:00
bde1d5b896 Add MinMaxDisplay and MinMaxInput components for enhanced min/max value input and display functionality. Implement formatting and precision handling for better user experience in data entry and visualization. 2026-07-20 02:05:32 +01:00
aa1446b502 Add new SVG icons and corresponding React components for HotEnd, MultiMotion, Picture, PrinterProfile, Ruler, and FilamentProfile. Update sidebarIconMap to include new icons for improved UI consistency. 2026-07-20 02:05:18 +01:00
48 changed files with 5771 additions and 269 deletions

View File

@ -0,0 +1,10 @@
<?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 72 70" 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.093797,0,0,1.093797,0.640357,-0.002979)">
<path d="M39.703,63.992C33.273,63.965 32.923,63.847 31.428,63.333C28.308,62.274 25.677,60.059 24.093,57.166L23.605,56.273L22.819,56.189C21.521,56.058 19.247,55.63 17.735,55.249C11.032,53.558 6.21,50.355 4.507,46.45C4.352,46.116 4.138,45.39 4.031,44.83C3.459,41.997 4.555,39.068 7.079,36.734C7.626,36.222 7.876,35.877 8.031,35.424C8.138,35.091 8.341,34.638 8.46,34.436C8.686,34.067 8.686,34.055 8.341,33.305C7.4,31.269 7.388,29.197 8.329,27.244L8.686,26.506L8.4,25.947C7.698,24.577 7.436,22.732 7.757,21.386L7.9,20.767L7.103,20.041C4.519,17.636 3.4,14.6 4.066,11.718C4.781,8.539 7.198,5.896 11.389,3.681C15.104,1.728 19.473,0.585 25.236,0.073C26.51,-0.046 31.999,0.109 33.488,0.3C40.929,1.252 46.93,3.824 50.157,7.467C53.669,11.432 53.36,16.338 49.371,20.041L48.573,20.767L48.716,21.386C49.038,22.744 48.776,24.565 48.061,25.97C47.775,26.554 47.775,26.566 48.002,26.935C48.537,27.792 48.799,28.876 48.799,30.245C48.799,30.465 48.795,30.668 48.786,30.858C47.381,30.973 46.02,31.246 44.722,31.661C45.016,31.051 45.191,29.981 45.025,29.697C44.93,29.518 44.858,29.53 44.43,29.828C41.763,31.638 38.048,32.948 33.547,33.65C32.059,33.876 31.285,33.924 28.237,33.924C25.189,33.924 24.415,33.876 22.926,33.65C18.545,32.959 14.878,31.709 12.294,29.995C11.484,29.459 11.496,29.459 11.413,29.804C11.222,30.554 11.639,31.709 12.425,32.686C13.687,34.257 16.783,35.865 20.152,36.734C25.553,38.129 31.889,38.015 36.956,36.571C35.707,37.901 34.654,39.418 33.845,41.075C32.625,41.258 31.365,41.384 30.082,41.449L28.499,41.532L29.237,42.33C29.654,42.771 30.308,43.58 30.689,44.128C31.344,45.069 31.404,45.128 31.785,45.069C31.904,45.049 32.16,45.014 32.465,44.973C32.161,46.294 32,47.67 32,49.083C32,51.584 32.504,53.969 33.415,56.141C33.042,56.019 32.813,55.848 32.404,55.606C31.63,55.141 30.725,54.118 30.344,53.26C30.213,52.963 29.951,52.058 29.749,51.26C28.82,47.45 26.891,44.664 23.688,42.509C22.272,41.556 21.39,41.163 18.414,40.187C15.759,39.306 13.496,38.317 12.294,37.496C11.484,36.948 11.508,36.948 11.413,37.424C11.175,38.71 12.294,40.437 14.223,41.747C15.342,42.497 17.259,43.366 19.176,43.997C21.188,44.652 22.2,45.176 23.224,46.069C24.927,47.545 25.546,48.76 26.189,51.808C26.76,54.522 27.57,56.082 29.213,57.642C30.356,58.725 31.416,59.38 32.797,59.845L33.797,60.19L35.768,60.196C36.883,61.649 38.21,62.93 39.703,63.992ZM10.186,43.271C9.71,42.794 9.115,42.08 8.841,41.675L8.365,40.949L8.019,41.687C7.329,43.175 7.662,44.997 8.889,46.462C10.913,48.867 15.223,50.962 20.152,51.927C22.724,52.427 22.498,52.415 22.414,52.022C22.105,50.605 21.605,49.617 20.926,49.01C20.283,48.45 19.438,48.057 17.366,47.355C16.366,47.009 15.068,46.509 14.497,46.235C13.008,45.545 11.198,44.295 10.186,43.271ZM12.818,23.601C12.187,23.327 11.603,23.065 11.532,23.041C11.425,22.994 11.413,23.089 11.472,23.387C12.079,26.066 16.437,28.649 22.081,29.673C24.224,30.066 25.534,30.173 28.237,30.173C30.94,30.173 32.249,30.066 34.392,29.673C40.036,28.649 44.394,26.066 45.001,23.387C45.061,23.089 45.049,22.994 44.942,23.041C44.87,23.065 44.287,23.327 43.656,23.601C40.751,24.875 37.274,25.744 32.987,26.268C31.261,26.482 25.212,26.482 23.486,26.268C19.2,25.744 15.723,24.875 12.818,23.601ZM31.19,3.872C23.391,3.241 15.259,5.098 10.663,8.563C9.448,9.48 8.71,10.337 8.138,11.48C7.722,12.302 7.674,12.516 7.674,13.361C7.674,14.635 8.138,15.623 9.305,16.85C13.115,20.886 22.414,23.327 31.309,22.613C38.429,22.041 44.287,19.922 47.168,16.85C48.335,15.623 48.799,14.635 48.799,13.361C48.799,12.516 48.752,12.302 48.335,11.48C47.764,10.337 47.025,9.48 45.811,8.563C42.37,5.979 37.345,4.36 31.19,3.872ZM25.796,7.932C29.999,7.265 34.869,8.527 36.595,10.718C37.357,11.694 37.548,12.218 37.548,13.361C37.548,14.516 37.357,15.04 36.595,16.016C34.488,18.683 28.249,19.791 23.677,18.302C21.355,17.54 20.057,16.576 19.223,14.992C18.795,14.183 18.795,12.552 19.223,11.742C20.259,9.765 22.533,8.444 25.796,7.932ZM30.118,11.67C27.32,11.206 23.915,11.873 22.807,13.099C22.581,13.361 22.581,13.373 22.807,13.635C23.605,14.504 26.082,15.242 28.237,15.242C30.392,15.242 32.868,14.504 33.666,13.635C33.892,13.373 33.892,13.361 33.666,13.099C33.083,12.468 31.797,11.944 30.118,11.67Z"/>
</g>
<g transform="matrix(1,0,0,1,35.1075,-13.675)">
<path d="M19.178,83.675L21.86,83.675C22.896,83.675 23.714,83.056 23.949,82.006L24.677,79.011C25.075,78.891 25.513,78.706 25.906,78.51L28.52,80.101C29.389,80.672 30.396,80.644 31.191,79.849L33.035,77.979C33.748,77.216 33.821,76.234 33.251,75.277L31.691,72.637C31.913,72.276 32.021,71.92 32.166,71.528L35.186,70.826C36.217,70.564 36.892,69.747 36.892,68.705L36.892,66.06C36.892,65.038 36.191,64.272 35.186,64.011L32.197,63.289C32.038,62.825 31.867,62.484 31.722,62.148L33.37,59.446C33.904,58.571 33.804,57.481 33.086,56.8L31.18,54.956C30.427,54.209 29.411,54.031 28.562,54.596L25.906,56.269C25.436,56.028 25.075,55.914 24.683,55.769L23.949,52.722C23.714,51.712 22.896,51.042 21.86,51.042L19.178,51.042C18.131,51.042 17.304,51.737 17.063,52.728L16.381,55.737C15.886,55.903 15.519,56.028 15.101,56.269L12.444,54.596C11.633,54.068 10.585,54.204 9.852,54.956L7.997,56.8C7.279,57.501 7.077,58.571 7.642,59.446L9.316,62.148C9.166,62.484 8.981,62.876 8.81,63.289L5.852,64.011C4.841,64.272 4.177,65.064 4.177,66.06L4.177,68.705C4.177,69.747 4.847,70.564 5.852,70.826L8.847,71.528C9.037,71.92 9.182,72.276 9.342,72.637L7.782,75.302C7.211,76.234 7.311,77.267 8.023,77.979L9.847,79.849C10.585,80.638 11.613,80.646 12.487,80.101L15.158,78.51C15.596,78.757 15.989,78.891 16.381,79.011L17.063,82.006C17.299,83.005 18.131,83.675 19.178,83.675ZM20.491,72.507C17.681,72.507 15.371,70.197 15.371,67.387C15.371,64.551 17.681,62.236 20.491,62.236C23.326,62.236 25.642,64.551 25.642,67.387C25.642,70.197 23.326,72.507 20.491,72.507Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,7 @@
<?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.824327,0,0,0.976141,2.488276,-2.712014)">
<path d="M56.78,22.995C64.017,23.632 69.667,28.805 69.667,35.084L69.667,42.696C69.667,49.405 63.217,54.851 55.273,54.851L52.514,54.851L39.525,64.685C37.406,66.289 34.196,66.289 32.077,64.685L19.088,54.851L16.329,54.851C8.385,54.851 1.935,49.405 1.935,42.696L1.935,35.084C1.935,28.805 7.585,23.632 14.822,22.995L14.822,8.007C14.822,6.476 16.294,5.233 18.107,5.233C19.921,5.233 21.393,6.476 21.393,8.007L21.393,22.929L50.209,22.929L50.209,8.007C50.209,6.476 51.681,5.233 53.495,5.233C55.308,5.233 56.78,6.476 56.78,8.007L56.78,22.995ZM28.932,54.851L35.801,60.052L42.67,54.851L28.932,54.851ZM55.273,49.302C59.591,49.302 63.096,46.342 63.096,42.696L63.096,35.084C63.096,31.438 59.591,28.478 55.273,28.478L16.329,28.478C12.011,28.478 8.506,31.438 8.506,35.084L8.506,42.696C8.506,46.342 12.011,49.302 16.329,49.302L55.273,49.302Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,7 @@
<?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.881796,0,0,0.881796,2.413866,6)">
<path d="M8.144,48.624L11.105,53.718L33.55,42.441L55.994,53.718L58.987,48.624L36.537,37.341L36.537,10.769L30.594,10.769L30.594,37.341L8.144,48.624ZM0.384,53.413C-0.592,55.654 0.33,57.211 2.725,57.489L15.69,58.924C18.959,59.318 20.849,57.171 19.138,54.231L11.408,40.828C9.765,37.956 6.931,38.488 5.654,41.467L0.384,53.413ZM66.721,53.413L61.477,41.467C60.174,38.488 57.366,37.956 55.717,40.828L47.962,54.231C46.256,57.171 48.172,59.318 51.415,58.924L64.38,57.489C66.77,57.211 67.697,55.654 66.721,53.413ZM31.172,1.489L23.477,11.964C21.527,14.591 22.46,17.332 25.814,17.332L41.248,17.332C44.665,17.332 45.558,14.639 43.592,11.97L35.87,1.484C34.395,-0.486 32.642,-0.506 31.172,1.489Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 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 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.783639,0,0,0.783639,3,9.136381)">
<path d="M10.966,55.715L61.631,55.715C67.901,55.715 71.026,52.48 70.761,46.429L52.249,29.059C50.874,27.784 49.224,27.139 47.553,27.139C45.84,27.139 44.303,27.727 42.877,29.003L28.763,41.616L22.999,36.414C21.686,35.233 20.243,34.639 18.78,34.639C17.334,34.639 16.041,35.201 14.754,36.383L2.961,46.953C3.114,52.711 5.792,55.715 10.966,55.715ZM10.251,58.341L63.763,58.341C70.533,58.341 74.014,54.886 74.014,48.218L74.014,10.16C74.014,3.486 70.533,0.011 63.763,0.011L10.251,0.011C3.506,0.011 0,3.486 0,10.16L0,48.218C0,54.886 3.506,58.341 10.251,58.341ZM10.606,52.219C7.701,52.219 6.122,50.714 6.122,47.678L6.122,10.694C6.122,7.658 7.701,6.134 10.606,6.134L63.407,6.134C66.287,6.134 67.891,7.658 67.891,10.694L67.891,47.678C67.891,50.714 66.287,52.219 63.407,52.219L10.606,52.219Z" style="fill-rule:nonzero;"/>
<path d="M24.209,29.743C28.302,29.743 31.677,26.369 31.677,22.239C31.677,18.146 28.302,14.74 24.209,14.74C20.079,14.74 16.705,18.146 16.705,22.239C16.705,26.369 20.079,29.743 24.209,29.743Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 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 72 70" 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;">
<path d="M38.645,64.222L17.549,64.222C11.671,64.198 8.506,61.129 8.506,55.228L8.506,13.858C8.506,7.958 11.939,4.864 17.619,4.864L54.008,4.864C59.907,4.864 63.096,7.958 63.096,13.858L63.096,35.121C61.386,34.433 59.559,33.974 57.653,33.783L57.653,22.31L54.347,22.31L54.347,23.05C54.347,27.911 52.199,30.851 48.168,31.749L45.342,34.575C44.524,35.393 43.197,35.393 42.379,34.575L39.549,31.745C35.516,30.843 33.374,27.904 33.374,23.05L33.374,22.31L13.973,22.31L13.973,55.44C13.973,57.721 15.098,58.965 17.297,59.072C17.269,58.754 17.255,58.42 17.255,58.07L17.255,54.215C17.255,49.957 19.434,48.069 23.402,48.069L36.442,48.069C35.975,49.668 35.702,51.349 35.651,53.085L24.624,53.085C23.237,53.085 22.516,53.749 22.516,55.165L22.516,57.102C22.516,58.518 23.237,59.21 24.624,59.21L36.417,59.21C36.93,60.995 37.685,62.679 38.645,64.222ZM57.653,17.418L57.653,13.652C57.653,11.264 56.396,10.001 54.013,10.001L17.613,10.001C15.206,10.001 13.973,11.264 13.973,13.652L13.973,17.418L33.807,17.418C34.894,13.675 37.985,11.731 42.487,11.731L45.258,11.731C49.934,11.731 52.908,13.675 53.94,17.418L57.653,17.418ZM42.481,16.868C40.074,16.868 38.841,18.13 38.841,20.518L38.841,23.262C38.841,25.65 40.074,26.902 42.481,26.902L45.264,26.902C47.647,26.902 48.904,25.65 48.904,23.262L48.904,20.518C48.904,18.149 47.666,16.888 45.32,16.868L42.481,16.868Z"/>
<g transform="matrix(1,0,0,1,35.1075,-13.675)">
<path d="M19.178,83.675L21.86,83.675C22.896,83.675 23.714,83.056 23.949,82.006L24.677,79.011C25.075,78.891 25.513,78.706 25.906,78.51L28.52,80.101C29.389,80.672 30.396,80.644 31.191,79.849L33.035,77.979C33.748,77.216 33.821,76.234 33.251,75.277L31.691,72.637C31.913,72.276 32.021,71.92 32.166,71.528L35.186,70.826C36.217,70.564 36.892,69.747 36.892,68.705L36.892,66.06C36.892,65.038 36.191,64.272 35.186,64.011L32.197,63.289C32.038,62.825 31.867,62.484 31.722,62.148L33.37,59.446C33.904,58.571 33.804,57.481 33.086,56.8L31.18,54.956C30.427,54.209 29.411,54.031 28.562,54.596L25.906,56.269C25.436,56.028 25.075,55.914 24.683,55.769L23.949,52.722C23.714,51.712 22.896,51.042 21.86,51.042L19.178,51.042C18.131,51.042 17.304,51.737 17.063,52.728L16.381,55.737C15.886,55.903 15.519,56.028 15.101,56.269L12.444,54.596C11.633,54.068 10.585,54.204 9.852,54.956L7.997,56.8C7.279,57.501 7.077,58.571 7.642,59.446L9.316,62.148C9.166,62.484 8.981,62.876 8.81,63.289L5.852,64.011C4.841,64.272 4.177,65.064 4.177,66.06L4.177,68.705C4.177,69.747 4.847,70.564 5.852,70.826L8.847,71.528C9.037,71.92 9.182,72.276 9.342,72.637L7.782,75.302C7.211,76.234 7.311,77.267 8.023,77.979L9.847,79.849C10.585,80.638 11.613,80.646 12.487,80.101L15.158,78.51C15.596,78.757 15.989,78.891 16.381,79.011L17.063,82.006C17.299,83.005 18.131,83.675 19.178,83.675ZM20.491,72.507C17.681,72.507 15.371,70.197 15.371,67.387C15.371,64.551 17.681,62.236 20.491,62.236C23.326,62.236 25.642,64.551 25.642,67.387C25.642,70.197 23.326,72.507 20.491,72.507Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,7 @@
<?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.781552,0,0,0.781552,1,17.941979)">
<path d="M79.329,9.044C79.329,3.244 76.045,0.021 70.183,0.021L9.093,0.021C3.221,0.021 0,3.244 0,9.044L0,26.911C0,32.701 3.263,35.953 9.125,35.953L70.225,35.953C76.077,35.953 79.329,32.701 79.329,26.911L79.329,9.044ZM72.826,10.089L72.826,25.876C72.826,28.201 71.564,29.461 69.323,29.461L9.964,29.461C7.775,29.461 6.482,28.201 6.482,25.876L6.482,10.089C6.482,7.763 7.785,6.503 9.995,6.503L69.354,6.503C71.564,6.503 72.826,7.763 72.826,10.089ZM13.04,4.664L10.112,4.664L10.112,20.338C10.112,21.156 10.774,21.776 11.602,21.776C12.461,21.776 13.04,21.134 13.04,20.297L13.04,4.664ZM18.661,4.664L15.732,4.664L15.732,15.295C15.732,16.144 16.404,16.744 17.211,16.744C18.092,16.744 18.661,16.123 18.661,15.264L18.661,4.664ZM24.27,4.664L21.32,4.664L21.32,15.295C21.32,16.144 22.014,16.744 22.81,16.744C23.67,16.744 24.27,16.123 24.27,15.264L24.27,4.664ZM29.849,4.664L26.92,4.664L26.92,15.295C26.92,16.144 27.592,16.744 28.42,16.744C29.279,16.744 29.849,16.123 29.849,15.264L29.849,4.664ZM35.479,4.664L32.55,4.664L32.55,15.295C32.55,16.144 33.222,16.744 34.019,16.744C34.9,16.744 35.479,16.123 35.479,15.264L35.479,4.664ZM41.11,4.664L38.181,4.664L38.181,20.338C38.181,21.156 38.843,21.776 39.65,21.776C40.53,21.776 41.11,21.134 41.11,20.297L41.11,4.664ZM46.709,4.664L43.781,4.664L43.781,15.295C43.781,16.144 44.452,16.744 45.259,16.744C46.118,16.744 46.709,16.123 46.709,15.264L46.709,4.664ZM52.319,4.664L49.39,4.664L49.39,15.295C49.39,16.144 50.062,16.744 50.88,16.744C51.739,16.744 52.319,16.123 52.319,15.264L52.319,4.664ZM57.918,4.664L54.999,4.664L54.999,15.295C54.999,16.144 55.661,16.744 56.468,16.744C57.348,16.744 57.918,16.123 57.918,15.264L57.918,4.664ZM63.548,4.664L60.62,4.664L60.62,15.295C60.62,16.144 61.292,16.744 62.088,16.744C62.948,16.744 63.548,16.123 63.548,15.264L63.548,4.664ZM69.158,4.664L66.229,4.664L66.229,20.338C66.229,21.156 66.891,21.776 67.719,21.776C68.578,21.776 69.158,21.134 69.158,20.297L69.158,4.664Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -696,3 +696,66 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.ant-color-picker .ant-collapse .ant-collapse-item .ant-collapse-header {
margin: 5.5px 0;
}
.subSteps.ant-timeline .ant-timeline-item-last > .ant-timeline-item-content {
min-height: unset;
}
.subSteps.ant-timeline .ant-timeline-item-last {
padding-bottom: 6px;
}
.subSteps.ant-timeline {
margin-top: 16px;
}
.ant-steps.ant-steps-vertical
> .ant-steps-item
> .ant-steps-item-container
> .ant-steps-item-tail::after {
width: 1.5px;
}
.subSteps.ant-timeline .ant-timeline-item-head {
background-color: unset;
}
.subSteps.ant-timeline .subSteps-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--color-primary);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.subSteps.ant-timeline .subSteps-dot svg path {
stroke: var(--layout-modal-bg);
stroke-width: 10px;
}
.subSteps.ant-timeline .subSteps-dot span {
width: 5px;
height: 5px;
margin-top: 0px;
margin-right: 0px;
}
.subSteps.ant-timeline .subSteps-dot.subSteps-dot-disabled {
background-color: var(--color-primary-disabled);
}
.subSteps.ant-timeline .subSteps-dot.subSteps-dot-disabled svg path {
stroke: var(--color-primary);
}
.ant-steps .ant-steps-item-finish .ant-steps-item-icon svg path {
stroke: var(--color-primary);
stroke-width: 3px;
}
.ant-descriptions .ant-descriptions-item-label::after {
top: 0;
}

View File

@ -6,6 +6,7 @@ import {
Navigate
} from 'react-router-dom'
import { App, ConfigProvider } from 'antd'
import { lazy } from 'react'
import Dashboard from './components/Dashboard/Dashboard.jsx'
import PrivateRoute from './components/PrivateRoute'
@ -33,6 +34,10 @@ import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import AuthLaunch from './components/App/AppLaunch.jsx'
const SlicerIntegration = lazy(
() =>
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
)
import {
ProductionRoutes,
@ -121,6 +126,10 @@ const AppContent = () => {
path='/email/notification'
element={<EmailNotificationTemplate />}
/>
<Route
path='/slicer'
element={<SlicerIntegration />}
/>
<Route
path='/dashboard'

View File

@ -0,0 +1,144 @@
import { useContext, useRef, useState } from 'react'
import { Button, Dropdown, Flex, Modal, Space } from 'antd'
import NewFilamentProfile, {
EditFilamentProfile
} from './FilamentProfiles/NewFilamentProfile'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import { ApiServerContext } from '../context/ApiServerContext'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import useViewMode from '../hooks/useViewMode'
const FilamentProfiles = () => {
const { deleteObject } = useContext(ApiServerContext)
const [profileModal, setProfileModal] = useState({
open: false,
profileId: null
})
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('FilamentProfiles')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('filamentProfile')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('FilamentProfiles')
const actionItems = {
items: [
{
label: 'New Filament Profile',
key: 'newFilamentProfile',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newFilamentProfile') {
setProfileModal({ open: true, profileId: null })
}
}
}
const handleRowAction = (action, profile) => {
if (action.name === 'edit') {
setProfileModal({ open: true, profileId: profile._id })
return true
}
if (action.name === 'delete') {
Modal.confirm({
title: 'Delete filament profile?',
content: `This will permanently delete ${profile.name || profile._reference}.`,
okText: 'Delete',
okType: 'danger',
onOk: async () => {
await deleteObject(profile._id, 'filamentProfile')
await tableRef.current?.reload()
}
})
return true
}
return false
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='filamentProfile'
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='filamentProfile' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
type='filamentProfile'
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
onRowAction={handleRowAction}
/>
</Flex>
<Modal
open={profileModal.open}
footer={null}
width={800}
onCancel={() => setProfileModal({ open: false, profileId: null })}
destroyOnHidden
>
{profileModal.open &&
(profileModal.profileId ? (
<EditFilamentProfile
profileId={profileModal.profileId}
onOk={() => {
setProfileModal({ open: false, profileId: null })
tableRef.current?.reload()
}}
/>
) : (
<NewFilamentProfile
onOk={() => {
setProfileModal({ open: false, profileId: null })
tableRef.current?.reload()
}}
/>
))}
</Modal>
</>
)
}
export default FilamentProfiles

View File

@ -0,0 +1,529 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Card, Flex, Space } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import ActionHandler from '../../common/ActionHandler'
import AuditLogIcon from '../../../Icons/AuditLogIcon'
import DocumentPrintButton from '../../common/DocumentPrintButton'
import EditButtons from '../../common/EditButtons'
import FilamentIcon from '../../../Icons/FilamentIcon'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import HotEndIcon from '../../../Icons/HotEndIcon'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import InfoCollapse from '../../common/InfoCollapse'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
import LockIndicator from '../../common/LockIndicator'
import MultiMotionIcon from '../../../Icons/MultiMotionIcon'
import NoteIcon from '../../../Icons/NoteIcon'
import NotesPanel from '../../common/NotesPanel'
import ObjectActions from '../../common/ObjectActions'
import ObjectForm from '../../common/ObjectForm'
import ObjectInfo from '../../common/ObjectInfo'
import ObjectTable from '../../common/ObjectTable'
import RulerIcon from '../../../Icons/RulerIcon'
import ScrollBox from '../../common/ScrollBox'
import SettingsIcon from '../../../Icons/SettingsIcon'
import UserNotifierToggle from '../../common/UserNotifierToggle'
import ViewButton from '../../common/ViewButton'
import useCollapseState from '../../hooks/useCollapseState'
import {
FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES,
FILAMENT_PROFILE_COMPATIBILITY_PROPERTIES,
FILAMENT_PROFILE_COOLING_PROPERTIES,
FILAMENT_PROFILE_EXHAUST_FAN_PROPERTIES,
FILAMENT_PROFILE_FLOW_PRESSURE_PROPERTIES,
FILAMENT_PROFILE_GCODE_PROPERTIES,
FILAMENT_PROFILE_LOADING_PROPERTIES,
FILAMENT_PROFILE_MATERIAL_PROPERTIES,
FILAMENT_PROFILE_RETRACTION_PROPERTIES,
FILAMENT_PROFILE_SHRINKAGE_DRYING_PROPERTIES,
FILAMENT_PROFILE_SPEED_PROPERTIES,
FILAMENT_PROFILE_TEMPERATURE_PROPERTIES,
FILAMENT_PROFILE_WIPE_TOWER_PROPERTIES
} from '../../../../database/models/FilamentProfile'
const toVisibleProperties = (propertyNames) =>
Object.fromEntries(propertyNames.map((name) => [name, true]))
const FilamentProfileInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const filamentProfileId = new URLSearchParams(location.search).get(
'filamentProfileId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'FilamentProfileInfo',
{
info: true,
material: true,
flowPressure: true,
temperature: true,
bedTemperature: true,
speed: true,
cooling: true,
exhaustFan: true,
retraction: true,
loading: true,
wipeTower: true,
gCodeSettings: true,
compatibility: true,
shrinkageDrying: true,
notes: true,
auditLogs: false
}
)
const [objectFormState, setObjectFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
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='filamentProfile'
id={filamentProfileId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Filament Profile Information' },
{ key: 'material', label: 'Material' },
{ key: 'flowPressure', label: 'Flow & Pressure' },
{ key: 'temperature', label: 'Temperature' },
{ key: 'bedTemperature', label: 'Bed Temperatures' },
{ key: 'speed', label: 'Speed' },
{ key: 'cooling', label: 'Part Cooling Fan' },
{ key: 'exhaustFan', label: 'Exhaust Fan' },
{ key: 'retraction', label: 'Retraction Overrides' },
{ key: 'loading', label: 'Loading & Multi-tool' },
{ key: 'wipeTower', label: 'Wipe Tower & Flushing' },
{ key: 'gCodeSettings', label: 'G-Code Settings' },
{ key: 'compatibility', label: 'Compatibility' },
{ key: 'shrinkageDrying', label: 'Shrinkage & Drying' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='filamentProfile'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='filamentProfile'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</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}
/>
</Flex>
<ScrollBox>
<Flex vertical gap='large'>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={filamentProfileId}
type='filamentProfile'
ref={objectFormRef}
onStateChange={(state) =>
setObjectFormState((current) => ({ ...current, ...state }))
}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap='large'>
<InfoCollapse
title='Filament Profile Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='140px'
visibleProperties={{
_id: true,
name: true,
_reference: true,
filamentType: true,
filament: true,
createdAt: true,
updatedAt: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='Material'
icon={<FilamentIcon />}
active={collapseState.material}
onToggle={(expanded) =>
updateCollapseState('material', expanded)
}
collapseKey='material'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='220px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_MATERIAL_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Flow & Pressure'
icon={<SettingsIcon />}
active={collapseState.flowPressure}
onToggle={(expanded) =>
updateCollapseState('flowPressure', expanded)
}
collapseKey='flowPressure'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='325px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_FLOW_PRESSURE_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Temperature'
icon={<HotEndIcon />}
active={collapseState.temperature}
onToggle={(expanded) =>
updateCollapseState('temperature', expanded)
}
collapseKey='temperature'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='280px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_TEMPERATURE_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Bed Temperatures'
icon={<RulerIcon />}
active={collapseState.bedTemperature}
onToggle={(expanded) =>
updateCollapseState('bedTemperature', expanded)
}
collapseKey='bedTemperature'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='280px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Speed'
icon={<MultiMotionIcon />}
active={collapseState.speed}
onToggle={(expanded) =>
updateCollapseState('speed', expanded)
}
collapseKey='speed'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='265px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_SPEED_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Part Cooling Fan'
icon={<SettingsIcon />}
active={collapseState.cooling}
onToggle={(expanded) =>
updateCollapseState('cooling', expanded)
}
collapseKey='cooling'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='350px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_COOLING_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Exhaust Fan'
icon={<SettingsIcon />}
active={collapseState.exhaustFan}
onToggle={(expanded) =>
updateCollapseState('exhaustFan', expanded)
}
collapseKey='exhaustFan'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='285px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_EXHAUST_FAN_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Retraction Overrides'
icon={<SettingsIcon />}
active={collapseState.retraction}
onToggle={(expanded) =>
updateCollapseState('retraction', expanded)
}
collapseKey='retraction'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='250px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_RETRACTION_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Loading & Multi-tool'
icon={<FilamentIcon />}
active={collapseState.loading}
onToggle={(expanded) =>
updateCollapseState('loading', expanded)
}
collapseKey='loading'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='220px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_LOADING_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Wipe Tower & Flushing'
icon={<SettingsIcon />}
active={collapseState.wipeTower}
onToggle={(expanded) =>
updateCollapseState('wipeTower', expanded)
}
collapseKey='wipeTower'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='330px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_WIPE_TOWER_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='G-Code Settings'
icon={<GCodeFileIcon />}
active={collapseState.gCodeSettings}
onToggle={(expanded) =>
updateCollapseState('gCodeSettings', expanded)
}
collapseKey='gCodeSettings'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='270px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_GCODE_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Compatibility'
icon={<SettingsIcon />}
active={collapseState.compatibility}
onToggle={(expanded) =>
updateCollapseState('compatibility', expanded)
}
collapseKey='compatibility'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='270px'
visibleProperties={toVisibleProperties(
FILAMENT_PROFILE_COMPATIBILITY_PROPERTIES
)}
/>
</InfoCollapse>
<InfoCollapse
title='Shrinkage & Drying'
icon={<SettingsIcon />}
active={collapseState.shrinkageDrying}
onToggle={(expanded) =>
updateCollapseState('shrinkageDrying', expanded)
}
collapseKey='shrinkageDrying'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='filamentProfile'
objectData={objectData}
labelWidth='340px'
visibleProperties={{
...toVisibleProperties(
FILAMENT_PROFILE_SHRINKAGE_DRYING_PROPERTIES
),
filamentNotes: true
}}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={filamentProfileId} type='filamentProfile' />
</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': filamentProfileId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
)
}
export default FilamentProfileInfo

View File

@ -0,0 +1,420 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import NewObjectForm from '../../common/NewObjectForm'
import ObjectForm from '../../common/ObjectForm'
import ObjectInfo from '../../common/ObjectInfo'
import WizardView from '../../common/WizardView'
import {
FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES,
FILAMENT_PROFILE_COMPATIBILITY_PROPERTIES,
FILAMENT_PROFILE_COOLING_PROPERTIES,
FILAMENT_PROFILE_EXHAUST_FAN_PROPERTIES,
FILAMENT_PROFILE_FLOW_PRESSURE_PROPERTIES,
FILAMENT_PROFILE_MATERIAL_PROPERTIES,
FILAMENT_PROFILE_SPEED_PROPERTIES,
FILAMENT_PROFILE_TEMPERATURE_PROPERTIES
} from '../../../../database/models/FilamentProfile'
const PRUSA_GENERIC_PETG_DEFAULTS = {
filamentType: 'filament',
filamentIsSupport: false,
filamentSoluble: false,
filamentPrintable: 3,
filamentAdhesivenessCategory: 0,
temperatureVitrification: 80,
idleTemperature: 0,
pelletFlowCoefficient: 0.4157,
requiredNozzleHrc: 0,
filamentFlowRatio: 0.98,
enablePressureAdvance: false,
pressureAdvance: 0.02,
adaptivePressureAdvance: false,
adaptivePressureAdvanceBridges: false,
adaptivePressureAdvanceOverhangs: false,
adaptivePressureAdvanceModel: '0,0,0\n0,0,0',
activateChamberTempControl: false,
chamberTemperature: 0,
chamberMinimalTemperature: 0,
nozzleTemperatureInitialLayer: 230,
nozzleTemperature: 240,
nozzleTemperatureRangeLow: 220,
nozzleTemperatureRangeHigh: 260,
hotPlateTempInitialLayer: 85,
hotPlateTemp: 85,
coolPlateTempInitialLayer: 60,
coolPlateTemp: 60,
engPlateTempInitialLayer: 0,
engPlateTemp: 0,
texturedPlateTempInitialLayer: 45,
texturedPlateTemp: 45,
texturedCoolPlateTempInitialLayer: 40,
texturedCoolPlateTemp: 40,
supertackPlateTempInitialLayer: 35,
supertackPlateTemp: 35,
filamentAdaptiveVolumetricSpeed: false,
filamentMaxVolumetricSpeed: 8,
volumetricSpeedCoefficients: '',
closeFanTheFirstXLayers: 3,
fullFanSpeedLayer: 0,
fanMinSpeed: 40,
fanMaxSpeed: 90,
reduceFanStopStartFreq: true,
slowDownForLayerCooling: true,
dontSlowDownOuterWall: false,
slowDownMinSpeed: 10,
slowDownLayerTime: 8,
fanCoolingLayerTime: 30,
enableOverhangBridgeFan: true,
overhangFanThreshold: '25%',
overhangFanSpeed: 90,
internalBridgeFanSpeed: -1,
supportMaterialInterfaceFanSpeed: -1,
ironingFanSpeed: -1,
initialLayerFanSpeed: -1,
firstXLayerFanSpeed: 0,
additionalCoolingFanSpeed: 0,
additionalFanFullSpeedLayer: 0,
closeAdditionalFanFirstXLayers: true,
activateAirFiltration: false,
activateAirFiltrationDuringPrint: true,
activateAirFiltrationOnCompletion: true,
duringPrintExhaustFanSpeed: 60,
completePrintExhaustFanSpeed: 80,
filamentLoadingSpeed: 28,
filamentLoadingSpeedStart: 3,
filamentUnloadingSpeed: 90,
filamentUnloadingSpeedStart: 100,
filamentChangeLength: 10,
filamentChangeLengthNc: 10,
filamentToolchangeDelay: 0,
filamentExtruderCompatibility: 0,
filamentExtruderVariant: 'Direct Drive Standard',
filamentMultitoolRamming: false,
filamentMultitoolRammingFlow: 10,
filamentMultitoolRammingVolume: 10,
filamentRammingParameters:
'120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6',
filamentRammingTravelTime: 0,
filamentRammingTravelTimeNc: 0,
filamentRammingVolumetricSpeed: -1,
filamentRammingVolumetricSpeedNc: -1,
filamentMinimalPurgeOnWipeTower: 15,
filamentTowerInterfacePreExtrusionDist: 10,
filamentTowerInterfacePreExtrusionLength: 0,
filamentTowerInterfacePrintTemp: -1,
filamentTowerInterfacePurgeVolume: 20,
filamentTowerIroningArea: 4,
filamentCoolingBeforeTower: 10,
filamentCoolingInitialSpeed: 2.2,
filamentCoolingFinalSpeed: 3.4,
filamentCoolingMoves: 4,
filamentFlushTemp: 0,
filamentFlushTempFast: 0,
filamentFlushVolumetricSpeed: 0,
filamentPreCoolingTemperature: 0,
filamentPreCoolingTemperatureNc: 0,
filamentPreheatTemperatureDelta: 0,
filamentPrimeVolumeNc: 60,
filamentStampingDistance: 0,
filamentStampingLoadingSpeed: 0,
longRetractionsWhenEc: false,
retractionDistancesWhenEc: 10,
filamentStartGcode:
'; filament start gcode\nM900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.04{else}0.08{endif} ; Filament gcode LA 1.5',
filamentEndGcode: '; filament end gcode \n',
filamentChangeExtrusionRoleGcode: '',
filamentShrink: '100%',
filamentShrinkageCompensationZ: '100%',
compatiblePrinters: [],
compatiblePrintersCondition: '',
compatiblePrints: [],
compatiblePrintsCondition: '',
filamentNotes: ''
}
const REQUIRED_PROPERTIES = ['name', 'filamentType', 'filament']
const SUMMARY_PROPERTIES = [
...REQUIRED_PROPERTIES,
...FILAMENT_PROFILE_MATERIAL_PROPERTIES,
...FILAMENT_PROFILE_FLOW_PRESSURE_PROPERTIES,
...FILAMENT_PROFILE_TEMPERATURE_PROPERTIES,
...FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES,
...FILAMENT_PROFILE_SPEED_PROPERTIES,
...FILAMENT_PROFILE_COOLING_PROPERTIES,
...FILAMENT_PROFILE_EXHAUST_FAN_PROPERTIES
]
const FilamentProfileWizard = ({
objectData,
formValid,
loading,
onSubmit,
editing,
filamentId = null
}) => {
const visibleProperties = useMemo(() => {
if (!filamentId) return undefined
return { filament: false, 'filament._id': false }
}, [filamentId])
const steps = useMemo(
() => [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='filamentProfile'
properties={REQUIRED_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='110px'
required={true}
objectData={objectData}
visibleProperties={visibleProperties}
/>
)
},
{
title: 'Material',
key: 'material',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_MATERIAL_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='210px'
objectData={objectData}
/>
)
},
{
title: 'Flow & Pressure',
key: 'flowPressure',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_FLOW_PRESSURE_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='290px'
objectData={objectData}
/>
)
},
{
title: 'Temperature',
key: 'temperature',
content: (
<ObjectInfo
type='filamentProfile'
properties={[
...FILAMENT_PROFILE_TEMPERATURE_PROPERTIES,
...FILAMENT_PROFILE_BED_TEMPERATURE_PROPERTIES
]}
column={1}
bordered={false}
isEditing
labelWidth='250px'
objectData={objectData}
/>
)
},
{
title: 'Speed',
key: 'speed',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_SPEED_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='220px'
objectData={objectData}
/>
)
},
{
title: 'Cooling',
key: 'cooling',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_COOLING_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='320px'
objectData={objectData}
/>
)
},
{
title: 'Exhaust Fan',
key: 'exhaustFan',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_EXHAUST_FAN_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='270px'
objectData={objectData}
/>
)
},
{
title: 'Compatibility',
key: 'compatibility',
content: (
<ObjectInfo
type='filamentProfile'
properties={FILAMENT_PROFILE_COMPATIBILITY_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='270px'
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='filamentProfile'
properties={SUMMARY_PROPERTIES}
column={1}
bordered={false}
isEditing={false}
labelWidth='250px'
objectData={objectData}
visibleProperties={visibleProperties}
/>
)
}
],
[objectData, visibleProperties]
)
return (
<WizardView
sizeBarWidth='185px'
steps={steps}
loading={loading}
formValid={formValid}
title={editing ? 'Edit Filament Profile' : 'New Filament Profile'}
submitText={editing ? 'Save' : 'Create'}
onSubmit={onSubmit}
/>
)
}
FilamentProfileWizard.propTypes = {
objectData: PropTypes.object,
formValid: PropTypes.bool.isRequired,
loading: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
editing: PropTypes.bool,
filamentId: PropTypes.string
}
const NewFilamentProfile = ({ onOk, filamentId = null }) => (
<NewObjectForm
type='filamentProfile'
defaultValues={{
...PRUSA_GENERIC_PETG_DEFAULTS,
...(filamentId ? { filamentType: 'filament', filament: filamentId } : {})
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => (
<FilamentProfileWizard
objectData={objectData}
formValid={formValid}
loading={submitLoading}
filamentId={filamentId}
onSubmit={async () => {
const result = await handleSubmit()
if (result?._id) onOk(result)
}}
/>
)}
</NewObjectForm>
)
NewFilamentProfile.propTypes = {
onOk: PropTypes.func.isRequired,
filamentId: PropTypes.string
}
export const EditFilamentProfile = ({ profileId, onOk, filamentId = null }) => {
const formRef = useRef(null)
const startedEditingRef = useRef(false)
const saveRequestedRef = useRef(false)
const stateRef = useRef({})
const [formState, setFormState] = useState({
loading: true,
editLoading: false,
formValid: false,
isEditing: false,
objectData: {}
})
useEffect(() => {
if (
!formState.loading &&
!formState.isEditing &&
!startedEditingRef.current
) {
startedEditingRef.current = true
formRef.current?.startEditing()
}
}, [formState.isEditing, formState.loading])
return (
<ObjectForm
id={profileId}
type='filamentProfile'
ref={formRef}
onStateChange={(nextState) => {
stateRef.current = { ...stateRef.current, ...nextState }
setFormState((current) => ({ ...current, ...nextState }))
if (saveRequestedRef.current && nextState.isEditing === false) {
saveRequestedRef.current = false
onOk(stateRef.current.objectData)
}
}}
>
{({ objectData, formValid, editLoading, handleUpdate }) => (
<FilamentProfileWizard
editing
objectData={objectData}
formValid={formValid}
loading={formState.loading || editLoading}
filamentId={filamentId}
onSubmit={() => {
saveRequestedRef.current = true
handleUpdate()
}}
/>
)}
</ObjectForm>
)
}
EditFilamentProfile.propTypes = {
profileId: PropTypes.string.isRequired,
onOk: PropTypes.func.isRequired,
filamentId: PropTypes.string
}
export default NewFilamentProfile

View File

@ -1,19 +1,31 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewGCodeFile = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'gcodeFile'}
defaultValues={{
const NewGCodeFile = ({
onOk,
defaultValues,
slicerIntegration = false,
shouldPrint = false,
currentStep,
onStepChange
}) => {
const initialValues = useMemo(
() => ({
state: { type: 'draft' },
...defaultValues
}}
>
}),
[defaultValues]
)
return (
<NewObjectForm type={'gcodeFile'} defaultValues={initialValues}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
...(!slicerIntegration
? [
{
title: 'Upload',
key: 'upload',
@ -29,7 +41,9 @@ const NewGCodeFile = ({ onOk, defaultValues }) => {
showLabels={false}
/>
)
},
}
]
: []),
{
title: 'Required',
key: 'required',
@ -73,10 +87,13 @@ const NewGCodeFile = ({ onOk, defaultValues }) => {
loading={submitLoading}
formValid={formValid}
title='New GCode File'
showSteps={!slicerIntegration || !shouldPrint}
currentStep={currentStep}
onStepChange={onStepChange}
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
onOk(result)
}
}}
/>
@ -89,7 +106,11 @@ const NewGCodeFile = ({ onOk, defaultValues }) => {
NewGCodeFile.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
defaultValues: PropTypes.object,
slicerIntegration: PropTypes.bool,
currentStep: PropTypes.number,
onStepChange: PropTypes.func,
shouldPrint: PropTypes.bool
}
export default NewGCodeFile

View File

@ -2,17 +2,20 @@ import { useState, useContext, useEffect, useCallback } from 'react'
import PropTypes from 'prop-types'
import WizardView from '../../common/WizardView'
import { ApiServerContext } from '../../context/ApiServerContext'
import { Flex, Typography } from 'antd'
import { Descriptions, Flex, Typography } from 'antd'
import ObjectTable from '../../common/ObjectTable'
import ObjectSelect from '../../common/ObjectSelect'
import ObjectProperty from '../../common/ObjectProperty'
const { Text } = Typography
const DeployJob = ({ onOk, objectData = undefined }) => {
const DeployJob = ({
onOk,
objectData = undefined,
slicerIntegration = false
}) => {
const [deployLoading, setDeployLoading] = useState(false)
const [job, setJob] = useState(objectData)
const [deployedSubJobsCount, setDeployedSubJobsCount] = useState(0)
const [subJobsCount, setSubJobsCount] = useState(999)
const [allPrintersOnline, setAllPrintersOnline] = useState(false)
const { sendObjectAction, fetchObjects } = useContext(ApiServerContext)
const handleDeploy = useCallback(async () => {
@ -51,22 +54,95 @@ const DeployJob = ({ onOk, objectData = undefined }) => {
}
}, [deployedSubJobsCount, subJobsCount, deployLoading, onOk])
useEffect(() => {
if (objectData?.printers) {
if (objectData.printers.every((printer) => printer.online)) {
setAllPrintersOnline(true)
} else {
setAllPrintersOnline(false)
}
} else {
setAllPrintersOnline(false)
}
}, [objectData])
console.log(objectData)
const steps = [
{
title: 'Confirm',
key: 'confirm',
content: (
<Flex vertical gap={'middle'} style={{ width: '100%' }}>
<Flex gap={'small'} align='center' style={{ width: '100%' }}>
<Text type='secondary'>Job:</Text>
<ObjectSelect
type={'job'}
style={{ flexGrow: 1 }}
value={objectData}
<Flex gap={'middle'} align='center' style={{ width: '100%' }}>
<Descriptions
column={1}
items={[
{
label: (
<Flex
align='center'
gap={'small'}
style={{ height: '100%' }}
>
<Text type='secondary'>Job</Text>
</Flex>
),
children: (
<div
style={{
flexGrow: 1,
marginLeft: '3px',
marginBottom: '1px'
}}
>
<ObjectProperty
type={'object'}
name={'job'}
objectData={{ job: objectData }}
value={(object) => object.job}
readOnly={true}
isEditing={true}
objectType={'job'}
onChange={(newJob) => {
setJob(newJob)
}}
/>
</div>
)
},
{
label: (
<Flex
align='center'
gap={'small'}
style={{ height: '100%' }}
>
<Text type='secondary'>Printers</Text>
</Flex>
),
children: (
<div
style={{
flexGrow: 1,
marginLeft: '3px',
marginBottom: '1px'
}}
>
<ObjectProperty
type={'objectList'}
name={'printers'}
objectData={{ printers: objectData.printers }}
value={(object) => object.printers}
readOnly={true}
isEditing={true}
objectType={'printer'}
/>
</div>
)
}
]}
/>
</Flex>
<ObjectTable
@ -74,11 +150,11 @@ const DeployJob = ({ onOk, objectData = undefined }) => {
scrollHeight={'200px'}
visibleColumns={{
printer: false,
'job._id': false,
'printer._id': false
job: false
}}
masterFilter={{ 'job._id': job?._id }}
size={'small'}
showActions={!slicerIntegration}
/>
</Flex>
)
@ -89,9 +165,13 @@ const DeployJob = ({ onOk, objectData = undefined }) => {
<WizardView
steps={steps}
loading={deployLoading}
formValid={objectData != undefined}
formValid={objectData != undefined && allPrintersOnline}
title='Deploy Job'
showSteps={false}
showSkipButton={slicerIntegration}
onSkip={() => {
onOk()
}}
submitText='Deploy'
progress={(deployedSubJobsCount / subJobsCount) * 100}
onSubmit={() => {
@ -104,7 +184,8 @@ const DeployJob = ({ onOk, objectData = undefined }) => {
DeployJob.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object,
reset: PropTypes.bool
reset: PropTypes.bool,
slicerIntegration: PropTypes.bool
}
export default DeployJob

View File

@ -1,17 +1,26 @@
import { useMemo } from 'react'
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewJob = ({ onOk, defaultValues }) => {
return (
<NewObjectForm
type={'job'}
defaultValues={{
const NewJob = ({
onOk,
defaultValues,
slicerIntegration = false,
currentStep,
onStepChange
}) => {
const initialValues = useMemo(
() => ({
state: { type: 'draft' },
...defaultValues
}}
>
}),
[defaultValues]
)
return (
<NewObjectForm type={'job'} defaultValues={initialValues}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
@ -59,10 +68,17 @@ const NewJob = ({ onOk, defaultValues }) => {
loading={submitLoading}
formValid={formValid}
title='New Job'
showSteps={!slicerIntegration}
currentStep={currentStep}
onStepChange={onStepChange}
showSkipButton={slicerIntegration}
onSkip={() => {
onOk(null)
}}
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
onOk(result)
}
}}
/>
@ -75,7 +91,10 @@ const NewJob = ({ onOk, defaultValues }) => {
NewJob.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
defaultValues: PropTypes.object,
slicerIntegration: PropTypes.bool,
currentStep: PropTypes.number,
onStepChange: PropTypes.func
}
export default NewJob

View File

@ -0,0 +1,143 @@
import { useContext, useRef, useState } from 'react'
import { Button, Dropdown, Flex, Modal, Space } from 'antd'
import NewPrinterProfile, {
EditPrinterProfile
} from './PrinterProfiles/NewPrinterProfile'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
import FilterSidebarButton from '../common/FilterSidebarButton'
import ObjectTable from '../common/ObjectTable'
import ObjectTableViewButton from '../common/ObjectTableViewButton'
import { ApiServerContext } from '../context/ApiServerContext'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
import useViewMode from '../hooks/useViewMode'
const PrinterProfiles = () => {
const { deleteObject } = useContext(ApiServerContext)
const [profileModal, setProfileModal] = useState({
open: false,
profileId: null
})
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('PrinterProfiles')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('printerProfile')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('PrinterProfiles')
const actionItems = {
items: [
{
label: 'New Printer Profile',
key: 'newPrinterProfile',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newPrinterProfile') {
setProfileModal({ open: true, profileId: null })
}
}
}
const handleRowAction = (action, profile) => {
if (action.name === 'edit') {
setProfileModal({ open: true, profileId: profile._id })
return true
}
if (action.name === 'delete') {
Modal.confirm({
title: 'Delete printer profile?',
content: `This will permanently delete ${profile.name || profile._reference}.`,
okText: 'Delete',
okType: 'danger',
onOk: async () => {
await deleteObject(profile._id, 'printerProfile')
await tableRef.current?.reload()
}
})
return true
}
return false
}
return (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'}>
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='printerProfile'
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='printerProfile' />
</Space>
<Space>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
type='printerProfile'
cards={viewMode === 'cards'}
visibleColumns={columnVisibility}
showFilterSidebar={showFilterSidebar}
expandHeight={true}
onRowAction={handleRowAction}
/>
</Flex>
<Modal
open={profileModal.open}
footer={null}
width={800}
onCancel={() => setProfileModal({ open: false, profileId: null })}
destroyOnHidden
>
{profileModal.open &&
(profileModal.profileId ? (
<EditPrinterProfile
profileId={profileModal.profileId}
onOk={() => {
setProfileModal({ open: false, profileId: null })
tableRef.current?.reload()
}}
/>
) : (
<NewPrinterProfile
onOk={() => {
setProfileModal({ open: false, profileId: null })
tableRef.current?.reload()
}}
/>
))}
</Modal>
</>
)
}
export default PrinterProfiles

View File

@ -0,0 +1,372 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import NewObjectForm from '../../common/NewObjectForm'
import ObjectForm from '../../common/ObjectForm'
import ObjectInfo from '../../common/ObjectInfo'
import WizardView from '../../common/WizardView'
const DEFAULT_EXTRUDER = {
nozzleDiameter: 0.4,
nozzleVolume: 0,
nozzleType: 'hardened_steel',
minLayerHeight: 0.05,
maxLayerHeight: 0.3,
positionOffsetX: 0,
positionOffsetY: 0,
retractionLength: 0.8,
retractionExtraLengthOnRestart: 0,
retractionSpeed: 35,
deretractionSpeed: 0,
retractionTravelDistanceThreshold: 2,
retractOnLayerChange: false,
wipeWhileRetracting: false,
wipeDistance: 1,
retractAmountBeforeWipe: 70,
retractAmountAfterWipe: 0,
zHopOnSurfaces: 'All Surfaces',
zHopType: 'Auto',
zHopHeight: 0,
zHopTravelingAngle: 0,
zHopOnlyLiftZAbove: 0,
zHopOnlyLiftZBelow: 0,
materialSwitchRetractionLength: 10,
materialSwitchExtraLengthOnRestart: 0,
longRetractionWhenCut: false
}
const PRUSA_MK3S_PROFILE_DEFAULTS = {
extruders: [{ ...DEFAULT_EXTRUDER }],
printBedWidth: 250,
printBedHeight: 210,
originX: 0,
originY: 0,
bedExcludeArea: [{ x: 0, y: 0 }],
printableHeight: 210,
supportMultiBedTypes: false,
bestObjectPositionX: 125,
bestObjectPositionY: 105,
zOffset: 0,
preferredOrientation: 0,
machineMaxAccelerationRetracting: { min: 2500, max: 2500 },
machineMaxSpeedE: { min: 120, max: 120 },
machineMaxSpeedX: { min: 200, max: 200 },
machineMaxSpeedY: { min: 200, max: 200 },
machinePauseGcode: 'M601',
machineStartGcode: `M862.1 P[nozzle_diameter] ; nozzle diameter check
M115 U3.13.0 ; tell printer latest fw version
G90 ; use absolute coordinates
M83 ; extruder relative mode
M104 S[first_layer_temperature] ; set extruder temp
M140 S[first_layer_bed_temperature] ; set bed temp
M190 S[first_layer_bed_temperature] ; wait for bed temp
M109 S[first_layer_temperature] ; wait for extruder temp
G28 W ; home all without mesh bed level
G80 ; mesh bed leveling
G1 Z0.3 F720
G1 Y-3 F1000 ; go outside print area
G92 E0
G1 X60 E9 F1000 ; intro line
G1 X100 E9 F1000 ; intro line
{else}
G1 Z0.2 F720
G1 Y-3 F1000 ; go outside print area
G92 E0
G1 X60 E9 F1000 ; intro line
G1 X100 E12.5 F1000 ; intro line
{endif}
G92 E0
M221 S{if layer_height<0.075}100{else}95{endif}`,
machineEndGcode: `{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+1, max_print_height)} F720 ; Move print head up{endif}
G1 X0 Y200 F3600 ; park
{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+49, max_print_height)} F720 ; Move print head further up{endif}
G4 ; wait
M221 S100 ; reset flow
M900 K0 ; reset LA
M104 S0 ; turn off temperature
M140 S0 ; turn off heatbed
M107 ; turn off fan
M84 ; disable motors
; max_layer_z = [max_layer_z]`,
layerChangeGcode: `;AFTER_LAYER_CHANGE
;[layer_z]`,
beforeLayerChangeGcode: `;BEFORE_LAYER_CHANGE
;[layer_z]
G92 E0
`,
printerNotes: `Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.
PRINTER_VENDOR_PRUSA3D
PRINTER_MODEL_MK3
`,
scanFirstLayer: false,
machineLoadFilamentTime: 17,
machineUnloadFilamentTime: 16,
thumbnails: [{ width: 160, height: 120 }],
auxiliaryFan: false,
machineMaxJunctionDeviation: { min: 0.02, max: 0 }
}
const REQUIRED_PROPERTIES = ['name', 'printer']
const DIMENSION_PROPERTIES = [
'printBedWidth',
'printBedHeight',
'originX',
'originY',
'printableHeight',
'supportMultiBedTypes',
'bestObjectPositionX',
'bestObjectPositionY',
'zOffset',
'preferredOrientation'
]
const EXTRUDER_STEP_PROPERTIES = ['extruders']
const MOTION_PROPERTIES = [
'machineMaxAccelerationRetracting',
'machineMaxSpeedE',
'machineMaxSpeedX',
'machineMaxSpeedY',
'machineMaxJunctionDeviation'
]
const OPTIONAL_PROPERTIES = [
'scanFirstLayer',
'machineLoadFilamentTime',
'machineUnloadFilamentTime',
'auxiliaryFan'
]
const SUMMARY_PROPERTIES = [
...REQUIRED_PROPERTIES,
...DIMENSION_PROPERTIES,
...MOTION_PROPERTIES,
...OPTIONAL_PROPERTIES
]
const PrinterProfileWizard = ({
objectData,
formValid,
loading,
onSubmit,
editing,
printerId = null
}) => {
const visibleProperties = useMemo(() => {
if (!printerId) return undefined
return { printer: false, 'printer._id': false }
}, [printerId])
const steps = useMemo(
() => [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='printerProfile'
properties={REQUIRED_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='75px'
required={true}
objectData={objectData}
visibleProperties={visibleProperties}
/>
)
},
{
title: 'Dimensions',
key: 'dimensions',
content: (
<ObjectInfo
type='printerProfile'
properties={DIMENSION_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='190px'
objectData={objectData}
/>
)
},
{
title: 'Extruders',
key: 'extruders',
content: (
<ObjectInfo
type='printerProfile'
properties={EXTRUDER_STEP_PROPERTIES}
column={1}
bordered={false}
isEditing
showLabels={false}
labelWidth='90px'
objectData={objectData}
/>
)
},
{
title: 'Motion',
key: 'motion',
content: (
<ObjectInfo
type='printerProfile'
properties={MOTION_PROPERTIES}
column={1}
bordered={false}
isEditing
labelWidth='185px'
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='printerProfile'
properties={OPTIONAL_PROPERTIES}
column={1}
labelWidth='170px'
bordered={false}
isEditing
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='printerProfile'
properties={SUMMARY_PROPERTIES}
column={1}
bordered={false}
isEditing={false}
labelWidth='210px'
objectData={objectData}
visibleProperties={visibleProperties}
/>
)
}
],
[objectData, visibleProperties]
)
return (
<WizardView
sizeBarWidth='160px'
steps={steps}
loading={loading}
formValid={formValid}
title={editing ? 'Edit Printer Profile' : 'New Printer Profile'}
submitText={editing ? 'Save' : 'Create'}
onSubmit={onSubmit}
/>
)
}
PrinterProfileWizard.propTypes = {
objectData: PropTypes.object,
formValid: PropTypes.bool.isRequired,
loading: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
editing: PropTypes.bool,
printerId: PropTypes.string
}
const NewPrinterProfile = ({ onOk, printerId = null }) => (
<NewObjectForm
type='printerProfile'
defaultValues={{
...PRUSA_MK3S_PROFILE_DEFAULTS,
...(printerId ? { printer: printerId } : {})
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => (
<PrinterProfileWizard
objectData={objectData}
formValid={formValid}
loading={submitLoading}
printerId={printerId}
onSubmit={async () => {
const result = await handleSubmit()
if (result?._id) onOk(result)
}}
/>
)}
</NewObjectForm>
)
NewPrinterProfile.propTypes = {
onOk: PropTypes.func.isRequired,
printerId: PropTypes.string
}
export const EditPrinterProfile = ({ profileId, onOk, printerId = null }) => {
const formRef = useRef(null)
const startedEditingRef = useRef(false)
const saveRequestedRef = useRef(false)
const stateRef = useRef({})
const [formState, setFormState] = useState({
loading: true,
editLoading: false,
formValid: false,
isEditing: false,
objectData: {}
})
useEffect(() => {
if (
!formState.loading &&
!formState.isEditing &&
!startedEditingRef.current
) {
startedEditingRef.current = true
formRef.current?.startEditing()
}
}, [formState.isEditing, formState.loading])
return (
<ObjectForm
id={profileId}
type='printerProfile'
ref={formRef}
onStateChange={(nextState) => {
stateRef.current = { ...stateRef.current, ...nextState }
setFormState((current) => ({ ...current, ...nextState }))
if (saveRequestedRef.current && nextState.isEditing === false) {
saveRequestedRef.current = false
onOk(stateRef.current.objectData)
}
}}
>
{({ objectData, formValid, editLoading, handleUpdate }) => (
<PrinterProfileWizard
editing
objectData={objectData}
formValid={formValid}
loading={formState.loading || editLoading}
printerId={printerId}
onSubmit={() => {
saveRequestedRef.current = true
handleUpdate()
}}
/>
)}
</ObjectForm>
)
}
EditPrinterProfile.propTypes = {
profileId: PropTypes.string.isRequired,
onOk: PropTypes.func.isRequired,
printerId: PropTypes.string
}
export default NewPrinterProfile

View File

@ -0,0 +1,381 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Card, Flex, Space } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import ActionHandler from '../../common/ActionHandler'
import AuditLogIcon from '../../../Icons/AuditLogIcon'
import DocumentPrintButton from '../../common/DocumentPrintButton'
import EditButtons from '../../common/EditButtons'
import HotEndIcon from '../../../Icons/HotEndIcon'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import MultiMotionIcon from '../../../Icons/MultiMotionIcon'
import PictureIcon from '../../../Icons/PictureIcon'
import RulerIcon from '../../../Icons/RulerIcon'
import InfoCollapse from '../../common/InfoCollapse'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
import LockIndicator from '../../common/LockIndicator'
import NoteIcon from '../../../Icons/NoteIcon'
import NotesPanel from '../../common/NotesPanel'
import ObjectActions from '../../common/ObjectActions'
import ObjectForm from '../../common/ObjectForm'
import ObjectInfo from '../../common/ObjectInfo'
import ObjectTable from '../../common/ObjectTable'
import ScrollBox from '../../common/ScrollBox'
import UserNotifierToggle from '../../common/UserNotifierToggle'
import ViewButton from '../../common/ViewButton'
import useCollapseState from '../../hooks/useCollapseState'
import ObjectProperty from '../../common/ObjectProperty'
import { getModelProperty } from '../../../../database/ObjectModels'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import SettingsIcon from '../../../Icons/SettingsIcon'
import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon'
const PrinterProfileInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const printerProfileId = new URLSearchParams(location.search).get(
'printerProfileId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'PrinterProfileInfo',
{
info: true,
dimensions: true,
bedExcludeArea: true,
extruders: true,
motion: true,
gCodeSettings: true,
optional: true,
thumbnails: true,
notes: true,
auditLogs: false
}
)
const [objectFormState, setObjectFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
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='printerProfile'
id={printerProfileId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Printer Profile Information' },
{ key: 'dimensions', label: 'Dimensions' },
{ key: 'bedExcludeArea', label: 'Excluded Bed Area' },
{ key: 'extruders', label: 'Extruders' },
{ key: 'motion', label: 'Motion' },
{ key: 'gCodeSettings', label: 'G-Code Settings' },
{ key: 'miscellaneous', label: 'Miscellaneous' },
{ key: 'thumbnails', label: 'Thumbnail Sizes' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='printerProfile'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='printerProfile'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</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}
/>
</Flex>
<ScrollBox>
<Flex vertical gap='large'>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={printerProfileId}
type='printerProfile'
ref={objectFormRef}
onStateChange={(state) =>
setObjectFormState((current) => ({ ...current, ...state }))
}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap='large'>
<InfoCollapse
title='Printer Profile Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printerProfile'
objectData={objectData}
labelWidth='140px'
visibleProperties={{
_id: true,
name: true,
_reference: true,
printer: true,
createdAt: true,
updatedAt: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='Dimensions'
icon={<RulerIcon />}
active={collapseState.dimensions}
onToggle={(expanded) =>
updateCollapseState('dimensions', expanded)
}
collapseKey='dimensions'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printerProfile'
objectData={objectData}
labelWidth='230px'
visibleProperties={{
printBedWidth: true,
printBedHeight: true,
originX: true,
originY: true,
printableHeight: true,
supportMultiBedTypes: true,
bestObjectPositionX: true,
bestObjectPositionY: true,
zOffset: true,
preferredOrientation: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='Excluded Bed Area'
icon={<ExclamationOctagonIcon />}
active={collapseState.bedExcludeArea}
onToggle={(expanded) =>
updateCollapseState('bedExcludeArea', expanded)
}
collapseKey='bedExcludeArea'
>
<ObjectProperty
{...getModelProperty('printerProfile', 'bedExcludeArea')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
<InfoCollapse
title='Extruders'
icon={<HotEndIcon />}
active={collapseState.extruders}
onToggle={(expanded) =>
updateCollapseState('extruders', expanded)
}
collapseKey='extruders'
>
<ObjectProperty
{...getModelProperty('printerProfile', 'extruders')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
<InfoCollapse
title='Motion'
icon={<MultiMotionIcon />}
active={collapseState.motion}
onToggle={(expanded) =>
updateCollapseState('motion', expanded)
}
collapseKey='motion'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printerProfile'
objectData={objectData}
labelWidth='225px'
visibleProperties={{
machineMaxAccelerationRetracting: true,
machineMaxSpeedE: true,
machineMaxSpeedX: true,
machineMaxSpeedY: true,
machineMaxJunctionDeviation: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='G-Code Settings'
icon={<GCodeFileIcon />}
active={collapseState.gCodeSettings}
onToggle={(expanded) =>
updateCollapseState('gCodeSettings', expanded)
}
collapseKey='gCodeSettings'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printerProfile'
objectData={objectData}
labelWidth='240px'
visibleProperties={{
machinePauseGcode: true,
machineStartGcode: true,
machineEndGcode: true,
layerChangeGcode: true,
beforeLayerChangeGcode: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='Miscellaneous'
icon={<SettingsIcon />}
active={collapseState.miscellaneous}
onToggle={(expanded) =>
updateCollapseState('miscellaneous', expanded)
}
collapseKey='miscellaneous'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='printerProfile'
objectData={objectData}
labelWidth='210px'
visibleProperties={{
printerNotes: true,
scanFirstLayer: true,
machineLoadFilamentTime: true,
machineUnloadFilamentTime: true,
auxiliaryFan: true
}}
/>
</InfoCollapse>
<InfoCollapse
title='Thumbnail Sizes'
icon={<PictureIcon />}
active={collapseState.thumbnails}
onToggle={(expanded) =>
updateCollapseState('thumbnails', expanded)
}
collapseKey='thumbnails'
>
<ObjectProperty
{...getModelProperty('printerProfile', 'thumbnails')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={printerProfileId} type='printerProfile' />
</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': printerProfileId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
)
}
export default PrinterProfileInfo

View File

@ -9,9 +9,9 @@ import InfoCollapse from '../../common/InfoCollapse.jsx'
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 ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
@ -35,7 +35,7 @@ import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel)
const ControlPrinter = () => {
const ControlPrinter = ({ slicerIntegration = false }) => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
@ -226,7 +226,11 @@ const ControlPrinter = () => {
type='printer'
id={printerId}
disabled={objectFormState.loading}
visibleActions={{ edit: false }}
visibleActions={{
edit: false,
info: !slicerIntegration,
control: !slicerIntegration
}}
objectData={objectFormState.objectData}
/>
<ViewButton
@ -275,24 +279,6 @@ const ControlPrinter = () => {
/>
</Space>
</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>
<ScrollBox>
@ -353,11 +339,16 @@ const ControlPrinter = () => {
currentSubJob: false,
'currentSubJob._id': false,
createdAt: false,
updatedAt: false
updatedAt: false,
state: !slicerIntegration,
name: !slicerIntegration
}}
objectPropertyProps={{
showHyperlink: !slicerIntegration
}}
objectData={printerObjectData}
type='printer'
labelWidth='100px'
labelWidth='120px'
/>
)
}}
@ -386,12 +377,15 @@ const ControlPrinter = () => {
<ObjectInfo
loading={jobObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
showHyperlink={!slicerIntegration}
visibleProperties={{
printers: false,
createdAt: false,
finishedAt: false
}}
objectPropertyProps={{
showHyperlink: !slicerIntegration
}}
objectData={jobObjectData}
type='job'
/>
@ -428,12 +422,15 @@ const ControlPrinter = () => {
<ObjectInfo
loading={subJobObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
showHyperlink={!slicerIntegration}
visibleProperties={{
printers: false,
createdAt: false,
finishedAt: false
}}
objectPropertyProps={{
showHyperlink: !slicerIntegration
}}
objectData={subJobObjectData}
type='subJob'
/>
@ -474,7 +471,7 @@ const ControlPrinter = () => {
<ObjectInfo
loading={filamentStockObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
showHyperlink={!slicerIntegration}
visibleProperties={{
updatedAt: false,
createdAt: false
@ -558,4 +555,8 @@ const ControlPrinter = () => {
)
}
ControlPrinter.propTypes = {
slicerIntegration: PropTypes.bool
}
export default ControlPrinter

View File

@ -96,8 +96,8 @@ const NewPrinter = ({ onOk, defaultValues }) => {
title='New Printer'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
if (result?._id) {
onOk(result)
}
}}
/>

View File

@ -0,0 +1,225 @@
import { useContext, useEffect, useState } from 'react'
import { Divider, Flex, Layout, Typography, Button, Badge, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { useLocation } from 'react-router-dom'
import ControlPrinter from './ControlPrinter.jsx'
import SlicerUploadFlow from './SlicerUploadFlow.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import FileIcon from '../../../Icons/FileIcon.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import { useThemeContext } from '../../context/ThemeContext.jsx'
const { Content } = Layout
const { Title } = Typography
const SlicerIntegration = () => {
const location = useLocation()
const printerId = new URLSearchParams(location.search).get('printerId')
const { updateObject } = useContext(ApiServerContext)
const [pendingSlicerUploads, setPendingSlicerUploads] = useState([])
const [currentPendingSlicerUpload, setCurrentPendingSlicerUpload] =
useState(null)
const { setPrimaryColorOverride } = useThemeContext()
useEffect(() => {
setPrimaryColorOverride('#00baa8')
}, [])
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {
pendingSlicerUploads: []
}
})
const [showSlicerUploadFlow, setShowSlicerUploadFlow] = useState(false)
useEffect(() => {
setPendingSlicerUploads(
objectFormState.objectData?.pendingSlicerUploads || []
)
}, [objectFormState.objectData?.pendingSlicerUploads])
useEffect(() => {
const nextUpload = pendingSlicerUploads.find(
(upload) => upload.new === true
)
if (nextUpload) {
setCurrentPendingSlicerUpload(nextUpload)
setShowSlicerUploadFlow(true)
} else {
setCurrentPendingSlicerUpload(null)
setShowSlicerUploadFlow(false)
}
}, [pendingSlicerUploads])
const showPendingSlicerUpload = () => {
const upload =
pendingSlicerUploads.find(
(pendingUpload) => pendingUpload.new === true
) || pendingSlicerUploads[0]
if (upload) {
setCurrentPendingSlicerUpload(upload)
setShowSlicerUploadFlow(true)
}
}
const handleGCodeFileCreated = async (gcodeFile) => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, gcodeFile: gcodeFile }
: upload
)
setPendingSlicerUploads(updatedUploads)
setCurrentPendingSlicerUpload((previous) => ({
...previous,
gcodeFile
}))
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleJobCreated = async (job) => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, job }
: upload
)
setPendingSlicerUploads(updatedUploads)
setCurrentPendingSlicerUpload((previous) => ({
...previous,
job
}))
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleSlicerUploadComplete = async () => {
const updatedUploads = pendingSlicerUploads.filter(
(upload) => upload._id !== currentPendingSlicerUpload?._id
)
setPendingSlicerUploads(updatedUploads)
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const handleSlicerUploadFlowClosed = async () => {
const updatedUploads = pendingSlicerUploads.map((upload) =>
upload._id === currentPendingSlicerUpload?._id
? { ...upload, new: false }
: upload
)
setPendingSlicerUploads(updatedUploads)
await updateObject(printerId, 'printer', {
...objectFormState.objectData,
pendingSlicerUploads: updatedUploads
})
}
const slicerUploadsButton = () => {
if (pendingSlicerUploads.length > 0) {
return (
<Badge
count={pendingSlicerUploads.length}
style={{ marginTop: '8px' }}
size='small'
>
<Button
type='text'
icon={<FileIcon />}
onClick={showPendingSlicerUpload}
/>
</Badge>
)
}
return null
}
return (
<Layout style={{ height: 'var(--unit-100vh)' }}>
<Layout style={{ padding: '24px' }}>
<Content>
<Flex vertical style={{ height: '100%' }} gap='20px'>
<ObjectForm
id={printerId}
type='printer'
onStateChange={(state) =>
setEditFormState((previousState) => ({
...previousState,
...state
}))
}
>
{({ loading, objectData }) => (
<Flex justify='space-between' align='center'>
<Flex align='center' gap='middle'>
{loading ? (
<LoadingOutlined spin style={{ fontSize: '26px' }} />
) : (
<PrinterIcon style={{ fontSize: '26px' }} />
)}
<Title level={3} style={{ margin: 0 }}>
{loading
? 'Loading...'
: objectData?.name || 'Slicer Integration'}
</Title>
{!loading && (
<ObjectProperty
objectData={objectData}
{...getModelProperty('printer', 'state')}
/>
)}
</Flex>
{slicerUploadsButton()}
</Flex>
)}
</ObjectForm>
<Divider style={{ margin: '0' }} />
<ControlPrinter slicerIntegration={true} />
</Flex>
</Content>
<Modal
open={showSlicerUploadFlow}
onCancel={() => setShowSlicerUploadFlow(false)}
afterClose={handleSlicerUploadFlowClosed}
footer={null}
width={740}
destroyOnHidden={true}
>
{currentPendingSlicerUpload && (
<SlicerUploadFlow
key={currentPendingSlicerUpload._id}
currentPendingSlicerUpload={currentPendingSlicerUpload}
printer={objectFormState.objectData}
onGCodeFileCreated={handleGCodeFileCreated}
onJobCreated={handleJobCreated}
onComplete={handleSlicerUploadComplete}
/>
)}
</Modal>
</Layout>
</Layout>
)
}
export default SlicerIntegration

View File

@ -0,0 +1,152 @@
import { useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import NewGCodeFile from '../GCodeFiles/NewGCodeFile.jsx'
import NewJob from '../Jobs/NewJob.jsx'
import DeployJob from '../Jobs/DeployJob.jsx'
import WizardView from '../../common/WizardView.jsx'
const SlicerUploadFlow = ({
currentPendingSlicerUpload,
printer,
onGCodeFileCreated,
onJobCreated,
onComplete
}) => {
const [createdGCodeFile, setCreatedGCodeFile] = useState(
() => currentPendingSlicerUpload.gcodeFile || null
)
const [createdJob, setCreatedJob] = useState(
() => currentPendingSlicerUpload.job || null
)
const [currentSubStep, setCurrentSubStep] = useState(0)
const gcodeFileDefaults = useMemo(
() => ({
...(currentPendingSlicerUpload?.properties || {}),
file: currentPendingSlicerUpload?.file
}),
[currentPendingSlicerUpload]
)
const jobDefaults = useMemo(
() => ({
quantity: 1,
gcodeFile: createdGCodeFile,
printers: [printer]
}),
[createdGCodeFile, printer]
)
const steps = [
{
title: 'GCode File',
key: 'gcodeFile',
subSteps: [
{
title: 'Required',
key: 'required'
},
{
title: 'Summary',
key: 'summary'
}
],
content: (
<NewGCodeFile
defaultValues={gcodeFileDefaults}
slicerIntegration={true}
shouldPrint={currentPendingSlicerUpload.shouldPrint}
currentStep={currentSubStep}
onStepChange={setCurrentSubStep}
onOk={(gcodeFile) => {
if (currentPendingSlicerUpload.shouldPrint) {
setCurrentSubStep(0)
setCreatedGCodeFile(gcodeFile)
onGCodeFileCreated(gcodeFile)
} else {
onComplete({ gcodeFile })
}
}}
/>
)
}
]
if (currentPendingSlicerUpload.shouldPrint) {
steps.push({
title: 'Print Job',
key: 'job',
subSteps: [
{
title: 'Required',
key: 'required'
},
{
title: 'Summary',
key: 'summary'
}
],
content: createdGCodeFile ? (
<NewJob
slicerIntegration={true}
defaultValues={jobDefaults}
currentStep={currentSubStep}
onStepChange={setCurrentSubStep}
onOk={(job) => {
if (job == null || job == undefined) {
onComplete({ gcodeFile: createdGCodeFile })
} else {
setCurrentSubStep(0)
setCreatedJob(job)
onJobCreated(job)
}
}}
/>
) : null
})
steps.push({
title: 'Deploy',
key: 'deploy',
content: createdJob ? (
<DeployJob
objectData={createdJob}
slicerIntegration={true}
onOk={() =>
onComplete({ gcodeFile: createdGCodeFile, job: createdJob })
}
/>
) : null
})
}
return (
<WizardView
title='Slicer Upload'
steps={steps}
currentStep={createdJob ? 2 : createdGCodeFile ? 1 : 0}
currentSubStep={currentSubStep}
showButtons={false}
showTitle={false}
showSteps={currentPendingSlicerUpload.shouldPrint}
formValid={true}
onSubmit={() => {}}
/>
)
}
SlicerUploadFlow.propTypes = {
currentPendingSlicerUpload: PropTypes.shape({
file: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
gcodeFile: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
job: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
shouldPrint: PropTypes.bool.isRequired,
properties: PropTypes.object
}).isRequired,
printer: PropTypes.object.isRequired,
onGCodeFileCreated: PropTypes.func.isRequired,
onJobCreated: PropTypes.func.isRequired,
onComplete: PropTypes.func.isRequired
}
export default SlicerUploadFlow

View File

@ -0,0 +1,30 @@
import PropTypes from 'prop-types'
import { Flex, Tag, Typography } from 'antd'
const { Text } = Typography
const ArrayDisplay = ({ value, prefix, suffix }) => {
if (!Array.isArray(value) || value.length === 0) {
return <Text type='secondary'>n/a</Text>
}
return (
<Flex gap='small' wrap>
{value.map((item, index) => (
<Tag key={`${item}-${index}`}>
{prefix}
{item}
{suffix}
</Tag>
))}
</Flex>
)
}
ArrayDisplay.propTypes = {
value: PropTypes.array,
prefix: PropTypes.node,
suffix: PropTypes.node
}
export default ArrayDisplay

View File

@ -0,0 +1,83 @@
import PropTypes from 'prop-types'
import { Button, Flex, Input, InputNumber } from 'antd'
const ArrayInput = ({
value = [],
onChange,
disabled = false,
numeric = false,
min,
max,
step,
placeholder
}) => {
const values = Array.isArray(value) ? value : []
const updateValue = (index, nextValue) => {
const nextValues = [...values]
nextValues[index] = nextValue
onChange?.(nextValues)
}
const removeValue = (index) => {
onChange?.(values.filter((_, valueIndex) => valueIndex !== index))
}
return (
<Flex vertical gap='small' style={{ width: '100%' }}>
{values.map((item, index) => (
<Flex key={index} gap='small' style={{ width: '100%' }}>
{numeric ? (
<InputNumber
value={item}
onChange={(nextValue) => updateValue(index, nextValue)}
min={min}
max={max}
step={step}
placeholder={placeholder}
disabled={disabled}
style={{ flex: 1 }}
/>
) : (
<Input
value={item}
onChange={(event) => updateValue(index, event.target.value)}
placeholder={placeholder}
disabled={disabled}
style={{ flex: 1 }}
/>
)}
<Button
onClick={() => removeValue(index)}
disabled={disabled}
danger
aria-label={`Remove ${placeholder || 'item'}`}
>
Remove
</Button>
</Flex>
))}
<Button
onClick={() => onChange?.([...values, numeric ? null : ''])}
disabled={disabled}
type='dashed'
block
>
Add {placeholder || 'item'}
</Button>
</Flex>
)
}
ArrayInput.propTypes = {
value: PropTypes.array,
onChange: PropTypes.func,
disabled: PropTypes.bool,
numeric: PropTypes.bool,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
placeholder: PropTypes.string
}
export default ArrayInput

View File

@ -76,7 +76,7 @@ const InputNumberCal = ({
const next = e.target.value
setExprValue(next)
const num = parseFloat(next)
if (!next) {
if (next === '') {
onChange?.(null)
} else if (!next.match(/[+\-*/=]/) && !Number.isNaN(num)) {
onChange?.(num)
@ -142,11 +142,11 @@ const InputNumberCal = ({
return (
<InputNumber
value={value}
{...commonProps}
value={value != null ? value : null}
onChange={onChange}
onBlur={onBlur}
onKeyDown={handleNumberKeyDown}
{...commonProps}
/>
)
}

View File

@ -0,0 +1,66 @@
import { Flex, Typography, Tag, Space } from 'antd'
import PropTypes from 'prop-types'
const { Text } = Typography
const getPrecision = (step) => {
if (step == null) return null
const stepStr = String(step)
const dotIndex = stepStr.indexOf('.')
return dotIndex === -1 ? 0 : stepStr.length - dotIndex - 1
}
const formatValue = (value, step) => {
if (value == null || value === '') return null
const parsed = parseFloat(value)
if (Number.isNaN(parsed)) return null
const precision = getPrecision(step)
return precision == null ? parsed.toString() : parsed.toFixed(precision)
}
const MinMaxDisplay = ({ value, prefix = '', suffix = '', step }) => {
const min = formatValue(value?.min, step)
const max = formatValue(value?.max, step)
return (
<Flex gap={'middle'} align='center' style={{ height: '100%' }}>
<Flex wrap>
<Text style={{ marginRight: 4 }}>
{min != null ? (
prefix + min + suffix
) : (
<Text type='secondary'>n/a</Text>
)}
</Text>
<Space>
<Tag>Min</Tag>
</Space>
</Flex>
<Flex wrap>
<Text style={{ marginRight: 4 }}>
{max != null ? (
prefix + max + suffix
) : (
<Text type='secondary'>n/a</Text>
)}
</Text>
<Space>
<Tag>Max</Tag>
</Space>
</Flex>
</Flex>
)
}
MinMaxDisplay.propTypes = {
value: PropTypes.shape({
min: PropTypes.number,
max: PropTypes.number
}),
prefix: PropTypes.string,
suffix: PropTypes.string,
step: PropTypes.number
}
export default MinMaxDisplay

View File

@ -0,0 +1,65 @@
import { Flex, InputNumber, Typography } from 'antd'
import PropTypes from 'prop-types'
const { Text } = Typography
const MinMaxInput = ({
value = {},
onChange,
prefix = '',
suffix = '',
min,
max,
step,
disabled = false
}) => {
const handleChange = (field, val) => {
if (!onChange) return
onChange({ ...value, [field]: val })
}
return (
<Flex gap={'middle'} align='center' style={{ width: '100%' }}>
<InputNumber
addonBefore={<Text type='secondary'>Min:</Text>}
value={value?.min}
onChange={(val) => handleChange('min', val)}
prefix={prefix}
suffix={suffix}
min={min}
max={max}
step={step}
disabled={disabled}
style={{ minWidth: 120, flex: 1 }}
/>
<InputNumber
addonBefore={<Text type='secondary'>Max:</Text>}
value={value?.max}
onChange={(val) => handleChange('max', val)}
prefix={prefix}
suffix={suffix}
min={min}
max={max}
step={step}
disabled={disabled}
style={{ minWidth: 120, flex: 1 }}
/>
</Flex>
)
}
MinMaxInput.propTypes = {
value: PropTypes.shape({
min: PropTypes.number,
max: PropTypes.number
}),
onChange: PropTypes.func,
prefix: PropTypes.string,
suffix: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
disabled: PropTypes.bool
}
export default MinMaxInput

View File

@ -3,8 +3,11 @@ import PropTypes from 'prop-types'
import { Table, Skeleton, Card, Button, Flex, Typography, Modal } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import ObjectProperty from './ObjectProperty'
import ObjectInfo from './ObjectInfo'
import { LoadingOutlined } from '@ant-design/icons'
import BinIcon from '../../Icons/BinIcon'
import EyeIcon from '../../Icons/EyeIcon'
import EditIcon from '../../Icons/EditIcon'
const { Text, Link, Title } = Typography
const DEFAULT_COLUMN_WIDTHS = {
@ -64,6 +67,7 @@ const ObjectChildTable = ({
value = [],
rollups = [],
onChange,
hiddenPropertyWidth = '300px',
minimal = false,
label = '',
...tableProps
@ -73,6 +77,9 @@ const ObjectChildTable = ({
const generatedRowKeysRef = useRef(new WeakMap())
const generatedRowKeyCountRef = useRef(0)
const [minimalModelOpen, setMinimalModelOpen] = useState(false)
const [detailModalOpen, setDetailModalOpen] = useState(false)
const [detailModalRecord, setDetailModalRecord] = useState(null)
const [detailModalIndex, setDetailModalIndex] = useState(null)
const getFallbackRowKey = (record) => {
if (!record || typeof record !== 'object') {
return `object-child-table-row-${String(record)}`
@ -124,15 +131,14 @@ const ObjectChildTable = ({
}, [columns, properties])
const resolvedProperties = useMemo(() => {
const explicit = orderedPropertyNames
const baseProperties =
columns && columns.length > 0
? orderedPropertyNames
.map((name) => propertyMap.get(name))
.filter(Boolean)
: properties
const remaining = properties.filter(
(property) => !orderedPropertyNames.includes(property.name)
)
return [...explicit, ...remaining].filter((property) => {
return baseProperties.filter((property) => {
if (!property?.name) return false
if (
visibleColumns &&
@ -142,13 +148,87 @@ const ObjectChildTable = ({
}
return true
})
}, [orderedPropertyNames, propertyMap, properties, visibleColumns])
}, [columns, orderedPropertyNames, propertyMap, properties, visibleColumns])
const hasHiddenProperties = useMemo(() => {
return (
Array.isArray(columns) &&
columns.length > 0 &&
Array.isArray(properties) &&
columns.length < properties.length
)
}, [columns, properties])
// When used inside antd Form.Item without Form.List, `value` will be the controlled array.
const itemsSource = useMemo(() => {
return value ?? []
}, [value])
const handleRemoveItem = useCallback(
(record, index) => {
const currentItems = Array.isArray(itemsSource) ? itemsSource : []
let newItems
if (typeof rowKey === 'string' && record[rowKey] != null) {
newItems = currentItems.filter(
(item) => item[rowKey] !== record[rowKey]
)
} else if (typeof rowKey === 'function') {
const recordKey = getResolvedRecordKey(record)
newItems = currentItems.filter((item) => {
const itemKey = getResolvedRecordKey(item)
return itemKey !== recordKey
})
} else {
newItems = currentItems.filter((_, i) => i !== index)
}
if (typeof onChange === 'function') {
onChange(newItems)
}
},
[itemsSource, onChange, rowKey, getResolvedRecordKey]
)
const handleOpenDetailModal = useCallback((record, index) => {
setDetailModalRecord({ ...record })
setDetailModalIndex(index)
setDetailModalOpen(true)
}, [])
const handleDetailPropertyChange = useCallback(
(propName, newVal) => {
const property = propertyMap.get(propName)
const resolved = resolveChangeValue(newVal, property?.type)
setDetailModalRecord((prev) =>
prev ? { ...prev, [propName]: resolved } : prev
)
if (detailModalIndex == null) return
const currentItems = Array.isArray(itemsSource) ? [...itemsSource] : []
const existingRowKey =
currentItems[detailModalIndex] != null
? getResolvedRecordKey(currentItems[detailModalIndex])
: null
const updatedItem = {
...currentItems[detailModalIndex],
[propName]: resolved
}
if (existingRowKey) {
generatedRowKeysRef.current.set(updatedItem, existingRowKey)
}
currentItems[detailModalIndex] = updatedItem
if (typeof onChange === 'function') {
onChange(currentItems)
}
},
[propertyMap, detailModalIndex, itemsSource, onChange, getResolvedRecordKey]
)
const tableColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property) => ({
title: property.label || property.name,
@ -197,18 +277,32 @@ const ObjectChildTable = ({
}
}))
const deleteColumn =
isEditing && canAddRemove
const actionsColumn =
hasHiddenProperties || (isEditing && canAddRemove)
? {
title: '',
key: 'delete',
width: 10,
key: 'actions',
width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right',
render: (_text, record, index) => {
if (record?.isSkeleton) {
return null
}
return (
<Flex gap={4}>
{hasHiddenProperties && (
<Button
type='text'
size='small'
icon={isEditing ? <EditIcon /> : <EyeIcon />}
onClick={(e) => {
e.stopPropagation()
handleOpenDetailModal(record, index)
}}
/>
)}
{isEditing && canAddRemove && (
<Button
type='text'
danger
@ -216,35 +310,11 @@ const ObjectChildTable = ({
icon={<BinIcon />}
onClick={(e) => {
e.stopPropagation()
const currentItems = Array.isArray(itemsSource)
? itemsSource
: []
// Use record's unique identifier if available, otherwise use index
let newItems
if (typeof rowKey === 'string' && record[rowKey] != null) {
// Use the unique key to find and remove the item
newItems = currentItems.filter(
(item) => item[rowKey] !== record[rowKey]
)
} else if (typeof rowKey === 'function') {
// If rowKey is a function, find the item by comparing resolved keys.
// Ant Design deprecates index-based rowKey callbacks.
const recordKey = getResolvedRecordKey(record)
newItems = currentItems.filter((item) => {
const itemKey = getResolvedRecordKey(item)
return itemKey !== recordKey
})
} else {
// Fallback to index-based removal
newItems = currentItems.filter((_, i) => i !== index)
}
if (typeof onChange === 'function') {
onChange(newItems)
}
handleRemoveItem(record, index)
}}
/>
)}
</Flex>
)
}
}
@ -253,17 +323,20 @@ const ObjectChildTable = ({
return [
...propertyColumns,
...additionalColumns,
...(deleteColumn ? [deleteColumn] : [])
...(actionsColumn ? [actionsColumn] : [])
]
}, [
resolvedProperties,
additionalColumns,
hasHiddenProperties,
isEditing,
canAddRemove,
itemsSource,
onChange,
rowKey,
getResolvedRecordKey
getResolvedRecordKey,
handleOpenDetailModal,
handleRemoveItem
])
const skeletonData = useMemo(() => {
@ -351,8 +424,8 @@ const ObjectChildTable = ({
}, [properties, rollups, objectData, value, name])
const rollupColumns = useMemo(() => {
const propertyColumns = properties.map((property, index) => {
const nextProperty = properties[index + 1]
const propertyColumns = resolvedProperties.map((property, index) => {
const nextProperty = resolvedProperties[index + 1]
var nextRollup = null
if (nextProperty) {
nextRollup = rollups?.find(
@ -384,12 +457,12 @@ const ObjectChildTable = ({
}
}
})
const blankDeleteColumn =
isEditing && canAddRemove
const blankActionsColumn =
hasHiddenProperties || (isEditing && canAddRemove)
? {
title: '',
key: 'delete',
width: 40,
key: 'actions',
width: hasHiddenProperties && isEditing && canAddRemove ? 70 : 40,
fixed: 'right',
render: () => {
return <Flex></Flex>
@ -398,9 +471,15 @@ const ObjectChildTable = ({
: null
return [
...propertyColumns,
...(blankDeleteColumn ? [blankDeleteColumn] : [])
...(blankActionsColumn ? [blankActionsColumn] : [])
]
}, [properties, rollups, isEditing, canAddRemove])
}, [
resolvedProperties,
rollups,
isEditing,
canAddRemove,
hasHiddenProperties
])
const hasRollups = useMemo(
() => Array.isArray(rollups) && rollups.length > 0,
@ -495,8 +574,37 @@ const ObjectChildTable = ({
</Flex>
)
const detailModal = hasHiddenProperties ? (
<Modal
open={detailModalOpen}
onCancel={() => setDetailModalOpen(false)}
footer={null}
width='900px'
>
{label ? (
<Title
level={2}
style={{ marginTop: 0, lineHeight: '0.7', marginBottom: 20 }}
>
{label}
</Title>
) : null}
<ObjectInfo
propertyDefinitions={properties}
objectData={detailModalRecord}
parentData={objectData}
labelWidth={hiddenPropertyWidth}
isEditing={isEditing}
onPropertyChange={isEditing ? handleDetailPropertyChange : undefined}
column={1}
showLabels={true}
/>
</Modal>
) : null
if (isEditing === true) {
return (
<>
<Card>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
@ -514,6 +622,8 @@ const ObjectChildTable = ({
{tableComponent}
</Flex>
</Card>
{detailModal}
</>
)
}
@ -541,11 +651,17 @@ const ObjectChildTable = ({
</Title>
{tableComponent}
</Modal>
{detailModal}
</>
)
}
return tableComponent
return (
<>
{tableComponent}
{detailModal}
</>
)
}
ObjectChildTable.propTypes = {
@ -574,7 +690,8 @@ ObjectChildTable.propTypes = {
rollups: PropTypes.arrayOf(PropTypes.object),
objectData: PropTypes.object,
canAddRemove: PropTypes.bool,
minimal: PropTypes.bool
minimal: PropTypes.bool,
hiddenPropertyWidth: PropTypes.string
}
export default ObjectChildTable

View File

@ -13,7 +13,10 @@ const ObjectInfo = ({
showHyperlink,
showLabels = true,
objectData = null,
parentData = null,
properties = [],
propertyDefinitions = null,
onPropertyChange = null,
required = undefined,
visibleProperties = {},
objectPropertyProps = {},
@ -39,7 +42,9 @@ const ObjectInfo = ({
// If properties array is empty, show all properties
// Otherwise, filter and order by the properties array
let items
if (properties.length === 0) {
if (Array.isArray(propertyDefinitions) && propertyDefinitions.length > 0) {
items = propertyDefinitions
} else if (properties.length === 0) {
items = allItems
} else {
items = properties
@ -80,6 +85,8 @@ const ObjectInfo = ({
}
})
const isControlled = typeof onPropertyChange === 'function'
// Map items to Descriptions 'items' prop format
const descriptionItems = items.map((item, idx) => {
const key = item.name || item.label || idx
@ -98,7 +105,17 @@ const ObjectInfo = ({
{...objectPropertyProps}
isEditing={isEditing}
objectData={combinedObjectData}
parentData={parentData}
showSince={true}
useFormItem={isControlled ? false : objectPropertyProps.useFormItem}
value={
isControlled ? combinedObjectData?.[item.name] : undefined
}
onChange={
isControlled
? (newVal) => onPropertyChange(item.name, newVal)
: objectPropertyProps.onChange
}
/>
</div>
),
@ -130,8 +147,11 @@ ObjectInfo.propTypes = {
bordered: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.object),
isEditing: PropTypes.bool,
type: PropTypes.string.isRequired,
type: PropTypes.string,
objectData: PropTypes.object,
parentData: PropTypes.object,
propertyDefinitions: PropTypes.array,
onPropertyChange: PropTypes.func,
required: PropTypes.bool,
visibleProperties: PropTypes.object,
objectPropertyProps: PropTypes.object,

View File

@ -27,6 +27,8 @@ import { getPropertyValue } from '../../../database/ObjectModels'
import PropertyChanges from './PropertyChanges'
import NetGrossDisplay from './NetGrossDisplay'
import NetGrossInput from './NetGrossInput'
import MinMaxDisplay from './MinMaxDisplay'
import MinMaxInput from './MinMaxInput'
import InputNumberCal from './InputNumberCal'
import ObjectList from './ObjectList'
import VarianceDisplay from './VarianceDisplay'
@ -47,10 +49,35 @@ import FileList from './FileList'
import ObjectChildTable from './ObjectChildTable'
import MiscId from './MiscId'
import AddressDisplay from './AddressDisplay'
import ArrayDisplay from './ArrayDisplay'
import ArrayInput from './ArrayInput'
import { round } from '../utils/Utils'
const { Text } = Typography
const getTextValue = (value) => {
if (value == null || typeof value === 'string' || typeof value === 'number') {
return value
}
if (typeof value === 'object') {
if (typeof value._id === 'string') {
return value._id
}
if (typeof value._id?._id === 'string') {
return value._id._id
}
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
return String(value)
}
const timeEditFormItemProps = {
getValueProps: (v) => ({ value: v ? dayjs(v) : null })
}
@ -97,7 +124,10 @@ const ObjectProperty = ({
loading = false,
rollups = [],
showCard = true,
showDownload = true,
columns = [],
style = {},
hiddenPropertyWidth = '100px',
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@ -152,7 +182,11 @@ const ObjectProperty = ({
type = type(objectData, parentData)
}
if (!value) {
if (columns && typeof columns == 'function' && objectData) {
columns = columns(objectData, parentData)
}
if (value == null) {
value = getPropertyValue(objectData, name)
}
@ -183,6 +217,15 @@ const ObjectProperty = ({
return (
<NetGrossDisplay value={value} prefix={prefix} suffix={suffix} />
)
case 'minMax':
return (
<MinMaxDisplay
value={value}
prefix={prefix}
suffix={suffix}
step={step}
/>
)
case 'secret':
if (value != null) {
return <SecretDisplay value={value} {...rest} />
@ -313,6 +356,9 @@ const ObjectProperty = ({
)
}
}
case 'numberList':
case 'stringList':
return <ArrayDisplay value={value} prefix={prefix} suffix={suffix} />
case 'variance': {
if (value != null) {
return (
@ -331,7 +377,7 @@ const ObjectProperty = ({
return (
<Text ellipsis style={style}>
{prefix}
{value}
{getTextValue(value)}
{suffix}
</Text>
)
@ -432,8 +478,10 @@ const ObjectProperty = ({
loading={loading}
rollups={rollups}
size={size}
hiddenPropertyWidth={hiddenPropertyWidth}
minimal={minimal}
label={label}
columns={columns}
canAddRemove={canAddRemove}
/>
)
@ -571,6 +619,7 @@ const ObjectProperty = ({
defaultPreviewOpen={previewOpen}
showPreview={showPreview}
showInfo={showHyperlink}
showDownload={showDownload}
/>
)
}
@ -588,8 +637,8 @@ const ObjectProperty = ({
)
}
default: {
if (value) {
return <Text {...textParams}>{value}</Text>
if (value != null && value !== '') {
return <Text {...textParams}>{getTextValue(value)}</Text>
} else {
return (
<Text type='secondary' {...textParams}>
@ -620,6 +669,59 @@ const ObjectProperty = ({
}
mergedFormItemProps.rules = rules
}
if (type === 'minMax' && disabled == false) {
const rules = [...(mergedFormItemProps.rules || [])]
rules.push({
validator: (_, range) => {
if (range == null) return Promise.resolve()
if (typeof range !== 'object' || Array.isArray(range)) {
return Promise.reject(new Error('Value must be a min/max range'))
}
const values = [range.min, range.max].filter((item) => item != null)
const valid = values.every(
(item) =>
typeof item === 'number' &&
Number.isFinite(item) &&
(min == null || item >= min) &&
(max == null || item <= max)
)
return valid
? Promise.resolve()
: Promise.reject(new Error('Enter valid min and max values'))
}
})
mergedFormItemProps.rules = rules
}
if ((type === 'numberList' || type === 'stringList') && disabled == false) {
const rules = [...(mergedFormItemProps.rules || [])]
rules.push({
validator: (_, items) => {
if (items == null) return Promise.resolve()
if (!Array.isArray(items)) {
return Promise.reject(new Error('Value must be a list'))
}
const valid = items.every((item) => {
if (type === 'numberList') {
return (
typeof item === 'number' &&
Number.isFinite(item) &&
(min == null || item >= min) &&
(max == null || item <= max)
)
}
return typeof item === 'string' && item.trim().length > 0
})
return valid
? Promise.resolve()
: Promise.reject(new Error('Complete or remove empty list items'))
}
})
mergedFormItemProps.rules = rules
}
// Remove name from mergedFormItemProps if present
if (mergedFormItemProps.name) {
delete mergedFormItemProps.name
@ -657,6 +759,17 @@ const ObjectProperty = ({
{...inputProps}
/>
)
case 'minMax':
return (
<MinMaxInput
prefix={prefix}
suffix={suffix}
min={min}
max={max}
step={step}
{...inputProps}
/>
)
case 'secret':
return (
<Input.Password
@ -751,6 +864,26 @@ const ObjectProperty = ({
{...inputProps}
/>
)
case 'numberList':
return (
<ArrayInput
numeric
min={min}
max={max}
step={step}
placeholder={label}
disabled={disabled}
{...inputProps}
/>
)
case 'stringList':
return (
<ArrayInput
placeholder={label}
disabled={disabled}
{...inputProps}
/>
)
case 'text':
return <Input placeholder={label} {...inputProps} />
case 'codeBlock':
@ -831,9 +964,11 @@ const ObjectProperty = ({
<ObjectChildTable
name={name}
properties={properties}
columns={columns}
objectData={objectData}
isEditing={true}
rollups={rollups}
hiddenPropertyWidth={hiddenPropertyWidth}
size={size}
canAddRemove={canAddRemove}
{...inputProps}
@ -901,7 +1036,9 @@ ObjectProperty.propTypes = {
rollups: PropTypes.arrayOf(PropTypes.object),
canAddRemove: PropTypes.bool,
showCard: PropTypes.bool,
style: PropTypes.object
showDownload: PropTypes.bool,
style: PropTypes.object,
hiddenPropertyWidth: PropTypes.string
}
export default ObjectProperty

View File

@ -217,7 +217,14 @@ const ObjectSelect = ({
if (pIdx >= properties.length) {
return data.map((object) => {
setObjectList((prev) => {
if (prev.some((p) => p._id === object._id)) return prev
if (
prev.some(
(p) =>
p._id.toString().toLowerCase() ===
object._id.toString().toLowerCase()
)
)
return prev
return [...prev, object]
})
return {
@ -235,8 +242,8 @@ const ObjectSelect = ({
/>
</div>
),
value: object._id,
key: object._id,
value: object._id.toString().toLowerCase(),
key: object._id.toString().toLowerCase(),
isLeaf: true,
parentKeys,
filterPath

View File

@ -106,7 +106,9 @@ const ObjectTable = forwardRef(
size = 'middle',
onStateChange,
showFilterSidebar = false,
expandHeight = false
expandHeight = false,
showActions = true,
onRowAction
},
ref
) => {
@ -186,14 +188,17 @@ const ObjectTable = forwardRef(
const rowActions =
model.actions?.filter((action) => action.row == true) || []
const createSkeletonData = useCallback((pageNum) => {
const createSkeletonData = useCallback(
(pageNum) => {
return Array(pageSize)
.fill(null)
.map((_, index) => ({
_id: `skeleton-${pageNum}-${index}`,
isSkeleton: true
}))
}, [pageSize])
},
[pageSize]
)
const createSkeletonPage = useCallback(
(pageNum) => ({
@ -236,6 +241,12 @@ const ObjectTable = forwardRef(
type={'text'}
size={'small'}
onClick={() => {
if (
onRowAction &&
onRowAction(action, objectData) !== false
) {
return
}
if (action.url) {
navigate(action.url(objectData._id))
}
@ -314,9 +325,7 @@ const ObjectTable = forwardRef(
hasMore: result.hasMore
}
setPages((prev) =>
prev.map((page) =>
page.pageNum === pageNum ? loadedPage : page
)
prev.map((page) => (page.pageNum === pageNum ? loadedPage : page))
)
setLoading(false)
return result.data || []
@ -330,9 +339,9 @@ const ObjectTable = forwardRef(
const findRenderedRow = useCallback((scrollTarget, id) => {
if (!scrollTarget || id == null) return null
return Array.from(
scrollTarget.querySelectorAll('tr[data-row-key]')
).find((row) => row.getAttribute('data-row-key') === String(id))
return Array.from(scrollTarget.querySelectorAll('tr[data-row-key]')).find(
(row) => row.getAttribute('data-row-key') === String(id)
)
}, [])
const captureScrollAnchor = useCallback(
@ -360,9 +369,7 @@ const ObjectTable = forwardRef(
}
const placeholderAnchor = closestRow(placeholderPage.items)
const loadedPages = currentPages.filter(
(page) => !page.isSkeletonPage
)
const loadedPages = currentPages.filter((page) => !page.isSkeletonPage)
const retainedPage =
direction === 'previous'
? loadedPages[0]
@ -380,8 +387,7 @@ const ObjectTable = forwardRef(
(scrollTarget, anchor, loadedPage) => {
if (!scrollTarget || !anchor) return
const loadedItem =
loadedPage.items[anchor.placeholderAnchor?.index]
const loadedItem = loadedPage.items[anchor.placeholderAnchor?.index]
const anchorId = loadedItem?._id ?? anchor.fallbackAnchor?.id
const previousTop = loadedItem
? anchor.placeholderAnchor?.top
@ -463,11 +469,7 @@ const ObjectTable = forwardRef(
}
}
},
[
captureScrollAnchor,
createPageWindow,
fetchPage
]
[captureScrollAnchor, createPageWindow, fetchPage]
)
const loadNextPage = useCallback(
@ -726,10 +728,7 @@ const ObjectTable = forwardRef(
]
if (firstResult.hasMore) {
setPages([
...loadedPages,
createSkeletonPage(pageNum + 1)
])
setPages([...loadedPages, createSkeletonPage(pageNum + 1)])
const secondResult = await fetchPage(pageNum + 1)
loadedPages.push({
pageNum: pageNum + 1,
@ -1004,7 +1003,11 @@ const ObjectTable = forwardRef(
}
})
if (rowActions.length > 0 && tableData.some((item) => !item?.isSkeleton)) {
if (
showActions &&
rowActions.length > 0 &&
tableData.some((item) => !item?.isSkeleton)
) {
columnsWithSkeleton.push({
title: (
<Flex gap='small' align='center' justify='center'>
@ -1185,7 +1188,9 @@ ObjectTable.propTypes = {
size: PropTypes.string,
onStateChange: PropTypes.func,
showFilterSidebar: PropTypes.bool,
expandHeight: PropTypes.bool
expandHeight: PropTypes.bool,
showActions: PropTypes.bool,
onRowAction: PropTypes.func
}
export default ObjectTable

View File

@ -9,13 +9,15 @@ import {
Divider,
Progress,
Button,
Dropdown
Dropdown,
Timeline
} from 'antd'
import NewObjectButtons from './NewObjectButtons'
const { Title } = Typography
const WizardView = ({
sizeBarWidth = '160px',
showSteps = true,
steps,
title = 'Wizard View',
@ -27,22 +29,76 @@ const WizardView = ({
progress = 0,
actions = [],
sideBarGrow = false,
disabled = false
disabled = false,
showButtons = true,
showTitle = true,
currentStep: controlledCurrentStep,
currentSubStep = 0,
onStepChange,
showSkipButton = false,
onSkip = () => {}
}) => {
const [currentStep, setCurrentStep] = useState(0)
const [internalCurrentStep, setInternalCurrentStep] = useState(0)
const currentStep = controlledCurrentStep ?? internalCurrentStep
const isMobile = useMediaQuery({ maxWidth: 768 })
const setCurrentStep = (step) => {
if (controlledCurrentStep === undefined) {
setInternalCurrentStep(step)
}
onStepChange?.(step)
}
return (
<Flex gap='middle' style={{ width: '100%' }}>
{!isMobile && showSteps == true ? (
sideBar != null ? (
sideBar
) : (
<div style={{ minWidth: sideBarGrow == true ? '100%' : '160px' }}>
<div
style={{ minWidth: sideBarGrow == true ? '100%' : sizeBarWidth }}
>
<Steps
current={currentStep}
items={steps.map((step, index) => ({
items={steps.map(({ subSteps, ...step }, index) => ({
...step,
description: subSteps ? (
<Timeline
className='subSteps'
items={subSteps.map((subStep, subStepIndex) => {
const complete =
index < currentStep ||
(index === currentStep && subStepIndex < currentSubStep)
const current =
index === currentStep && subStepIndex === currentSubStep
const disabled = index < currentStep
return {
...subStep,
children: subStep.title,
dot: complete ? (
<div
className={`subSteps-dot ${disabled ? 'subSteps-dot-disabled' : ''}`}
>
<CheckIcon
style={{
fontSize: '5px',
marginTop: '0px',
marginRight: '0px',
color: disabled
? 'var(--color-primary)'
: 'var(--layout-modal-bg)'
}}
/>
</div>
) : undefined,
color: complete ? 'blue' : current ? 'blue' : 'gray'
}
})}
/>
) : (
step.description
),
icon:
index < currentStep ? (
<div className='ant-steps-item-icon'>
@ -76,13 +132,27 @@ const WizardView = ({
}
>
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>
{showTitle ? (
<Title level={2} style={{ margin: '0 0 8px 0', lineHeight: 0.7 }}>
{title}
</Title>
) : null}
<div style={{ minHeight: '260px', marginBottom: 4, width: '100%' }}>
{steps[currentStep].content}
</div>
</Flex>
{showButtons || progress > 0 || actions.length > 0 ? (
<Flex
gap={'middle'}
align='center'
justify={showSkipButton ? 'space-between' : 'end'}
>
{showSkipButton && showButtons && (
<Button onClick={onSkip} disabled={disabled}>
Skip
</Button>
)}
<Flex gap={'middle'} align='center' justify='end'>
{progress > 0 ? (
<Progress
@ -95,7 +165,10 @@ const WizardView = ({
if (action.steps.includes(steps[currentStep].key)) {
if (action.children) {
return (
<Dropdown menu={{ items: action.children }} key={action.key}>
<Dropdown
menu={{ items: action.children }}
key={action.key}
>
<Button
onClick={action?.onClick}
disabled={action?.disabled || disabled}
@ -120,19 +193,23 @@ const WizardView = ({
return null
})}
{showButtons && (
<NewObjectButtons
disabled={disabled}
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onPrevious={() => setCurrentStep(currentStep - 1)}
onNext={() => setCurrentStep(currentStep + 1)}
onSubmit={onSubmit}
formValid={formValid}
submitLoading={loading}
submitText={submitText}
/>
)}
</Flex>
</Flex>
) : null}
</Flex>
</Flex>
)
}
@ -149,7 +226,15 @@ WizardView.propTypes = {
submitText: PropTypes.string,
progress: PropTypes.number,
actions: PropTypes.array,
sideBarGrow: PropTypes.bool
sideBarGrow: PropTypes.bool,
showButtons: PropTypes.bool,
showTitle: PropTypes.bool,
currentStep: PropTypes.number,
currentSubStep: PropTypes.number,
onStepChange: PropTypes.func,
showSkipButton: PropTypes.bool,
onSkip: PropTypes.func,
sizeBarWidth: PropTypes.string
}
export default WizardView

View File

@ -69,6 +69,7 @@ const AuthProvider = ({ children }) => {
const [userProfile, setUserProfile] = useState(null)
const [profileImageUrl, setProfileImageUrl] = useState(null)
const profileImageUrlRef = useRef(null)
const processedAuthCodeRef = useRef(null)
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
const [showAuthErrorModal, setShowAuthErrorModal] = useState(false)
@ -165,6 +166,8 @@ const AuthProvider = ({ children }) => {
// Read token from cookies (web) or electron session storage if present
useEffect(() => {
let cancelled = false
const authCode =
new URLSearchParams(location.search).get('authCode') || null
const load = async () => {
try {
@ -227,7 +230,9 @@ const AuthProvider = ({ children }) => {
}
}
if (location.pathname === '/applaunch') {
// An auth code represents a newly completed login and must take precedence
// over any existing persisted session.
if (location.pathname === '/applaunch' || authCode != null) {
return
}
@ -431,6 +436,7 @@ const AuthProvider = ({ children }) => {
setExpiresAt(nextExpiresAt)
setUserProfile(nextUser)
setAuthenticated(true)
setShowUnauthorizedModal(false)
// Persist session (cookies on web, electron storage on desktop)
const persisted = await persistSession({
@ -469,6 +475,7 @@ const AuthProvider = ({ children }) => {
setAuthenticated(false)
} finally {
setLoading(false)
setRetreivedTokenFromCookies(true)
}
},
[
@ -723,7 +730,11 @@ const AuthProvider = ({ children }) => {
useEffect(() => {
const authCode =
new URLSearchParams(location.search).get('authCode') || null
if (authCode != null) {
if (
authCode != null &&
processedAuthCodeRef.current !== authCode
) {
processedAuthCodeRef.current = authCode
getLoginToken(authCode)
} else if (
token == null &&
@ -800,7 +811,12 @@ const AuthProvider = ({ children }) => {
Please log in to continue
</Space>
}
open={showUnauthorizedModal && !loading && !showAuthErrorModal}
open={
showUnauthorizedModal &&
!authenticated &&
!loading &&
!showAuthErrorModal
}
onOk={() => {
setShowUnauthorizedModal(false)
loginWithSSO()

View File

@ -12,6 +12,8 @@ const ThemeContext = createContext()
const COLORS = {
colorPrimary: '#0091FF',
colorPrimaryDisabledDark: '#0e3a5b',
colorPrimaryDisabledLight: '#e6f8ff',
colorSuccess: '#30D158',
colorWarning: '#FF9230',
colorError: '#FF4245',
@ -38,6 +40,8 @@ export const ThemeProvider = ({ children }) => {
return savedCompact ? JSON.parse(savedCompact) : false
})
const [primaryColorOverride, setPrimaryColorOverride] = useState(null)
// System theme detection
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
@ -124,7 +128,16 @@ export const ThemeProvider = ({ children }) => {
// Set CSS custom properties for theme colors
useEffect(() => {
const root = document.documentElement
root.style.setProperty('--color-primary', COLORS.colorPrimary)
root.style.setProperty(
'--color-primary',
primaryColorOverride == null ? COLORS.colorPrimary : primaryColorOverride
)
root.style.setProperty(
'--color-primary-disabled',
isDarkMode
? COLORS.colorPrimaryDisabledDark
: COLORS.colorPrimaryDisabledLight
)
root.style.setProperty('--color-success', COLORS.colorSuccess)
root.style.setProperty('--color-warning', COLORS.colorWarning)
root.style.setProperty('--color-error', COLORS.colorError)
@ -139,12 +152,20 @@ export const ThemeProvider = ({ children }) => {
'--layout-header-bg',
isDarkMode ? '#141414' : '#ffffff'
)
}, [isDarkMode])
root.style.setProperty(
'--layout-modal-bg',
isDarkMode ? '#1f1f1f' : '#ffffff'
)
}, [isDarkMode, primaryColorOverride])
const themeConfig = {
algorithm: getThemeAlgorithm(),
token: {
...COLORS,
colorPrimary:
primaryColorOverride == null
? COLORS.colorPrimary
: primaryColorOverride,
borderRadius: '12px'
},
components: {
@ -166,6 +187,7 @@ export const ThemeProvider = ({ children }) => {
setThemeMode,
setDensityMode,
getColors,
setPrimaryColorOverride,
themeConfig
}}
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
import ProductionIcon from './ProductionIcon'
import PrinterIcon from './PrinterIcon'
import PrinterProfileIcon from './PrinterProfileIcon'
import FilamentProfileIcon from './FilamentProfileIcon'
import JobIcon from './JobIcon'
import GCodeFileIcon from './GCodeFileIcon'
import SubJobIcon from './SubJobIcon'
@ -55,6 +57,8 @@ const toEmoji = (emoji) => <span aria-hidden>{emoji}</span>
const sidebarIconMap = {
production: <ProductionIcon />,
printer: <PrinterIcon />,
printerProfile: <PrinterProfileIcon />,
filamentProfile: <FilamentProfileIcon />,
job: <JobIcon />,
subJob: <SubJobIcon />,
gcodeFile: <GCodeFileIcon />,

View File

@ -1,4 +1,6 @@
import { Printer } from './models/Printer.js'
import { PrinterProfile } from './models/PrinterProfile.js'
import { FilamentProfile } from './models/FilamentProfile.js'
import { Host } from './models/Host.js'
import { Filament } from './models/Filament.js'
import { Material } from './models/Material.js'
@ -49,6 +51,8 @@ import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
Printer,
PrinterProfile,
FilamentProfile,
Host,
Filament,
FilamentSku,
@ -100,6 +104,8 @@ export const objectModels = [
// Re-export individual models for direct access
export {
Printer,
PrinterProfile,
FilamentProfile,
Host,
Filament,
FilamentSku,

File diff suppressed because it is too large Load Diff

View File

@ -144,9 +144,14 @@ export const GCodeFile = {
label: 'Name',
columnFixed: 'left',
type: 'text',
value: null,
required: true,
columnWidth: 200
columnWidth: 200,
value: (objectData) => {
if (objectData?.name == undefined) {
return objectData?.file?.name
}
return objectData?.name
}
},
{
name: 'file',

View File

@ -0,0 +1,665 @@
import PrinterProfileIcon from '../../components/Icons/PrinterProfileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
const THUMBNAIL_PROPERTIES = [
{
name: 'width',
label: 'Width',
type: 'number',
min: 0,
step: 1,
columnWidth: 120
},
{
name: 'height',
label: 'Height',
type: 'number',
min: 0,
step: 1,
columnWidth: 120
}
]
const BED_EXCLUDE_AREA_PROPERTIES = [
{
name: 'x',
label: 'X',
type: 'number',
step: 0.01,
columnWidth: 120
},
{
name: 'y',
label: 'Y',
type: 'number',
step: 0.01,
columnWidth: 120
}
]
const EXTRUDER_PROPERTIES = [
{
name: 'nozzleDiameter',
label: 'Nozzle Diameter',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.05,
columnWidth: 150
},
{
name: 'nozzleVolume',
label: 'Nozzle Volume',
type: 'number',
suffix: ' mm³',
min: 0,
step: 0.1,
columnWidth: 150
},
{
name: 'nozzleType',
label: 'Nozzle Type',
type: 'text',
columnWidth: 160
},
{
name: 'minLayerHeight',
label: 'Min Layer Height',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 180
},
{
name: 'maxLayerHeight',
label: 'Max Layer Height',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 180
},
{
name: 'positionOffsetX',
label: 'Extruder Offset X',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 180
},
{
name: 'positionOffsetY',
label: 'Extruder Offset Y',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 180
},
{
name: 'retractionLength',
label: 'Retraction Length',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 170
},
{
name: 'retractionExtraLengthOnRestart',
label: 'Extra Length on Restart',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 210
},
{
name: 'retractionSpeed',
label: 'Retraction Speed',
type: 'number',
suffix: ' mm/s',
min: 0,
step: 1,
columnWidth: 170
},
{
name: 'deretractionSpeed',
label: 'Deretraction Speed',
type: 'number',
suffix: ' mm/s',
min: 0,
step: 1,
columnWidth: 180
},
{
name: 'retractionTravelDistanceThreshold',
label: 'Travel Distance Threshold',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 220
},
{
name: 'retractOnLayerChange',
label: 'Retract on Layer Change',
type: 'bool',
columnWidth: 210
},
{
name: 'wipeWhileRetracting',
label: 'Wipe While Retracting',
type: 'bool',
columnWidth: 190
},
{
name: 'wipeDistance',
label: 'Wipe Distance',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 150
},
{
name: 'retractAmountBeforeWipe',
label: 'Retract Amount Before Wipe',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 230
},
{
name: 'retractAmountAfterWipe',
label: 'Retract Amount After Wipe',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 230
},
{
name: 'zHopOnSurfaces',
label: 'Z-Hop On Surfaces',
type: 'select',
options: [
{ label: 'All Surfaces', value: 'All Surfaces' },
{ label: 'Top Only', value: 'Top Only' },
{ label: 'Bottom Only', value: 'Bottom Only' },
{ label: 'None', value: 'None' }
],
columnWidth: 170
},
{
name: 'zHopType',
label: 'Z-Hop Type',
type: 'select',
options: [
{ label: 'Auto', value: 'Auto' },
{ label: 'Normal', value: 'Normal' },
{ label: 'Spiral', value: 'Spiral' },
{ label: 'Slope', value: 'Slope' }
],
columnWidth: 140
},
{
name: 'zHopHeight',
label: 'Z-Hop Height',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 150
},
{
name: 'zHopTravelingAngle',
label: 'Traveling Angle',
type: 'number',
suffix: '°',
min: 0,
step: 1,
columnWidth: 160
},
{
name: 'zHopOnlyLiftZAbove',
label: 'Only Lift Z Above',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 170
},
{
name: 'zHopOnlyLiftZBelow',
label: 'Only Lift Z Below',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 170
},
{
name: 'materialSwitchRetractionLength',
label: 'Material Switch Retraction Length',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 250
},
{
name: 'materialSwitchExtraLengthOnRestart',
label: 'Material Switch Extra Length on Restart',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.1,
columnWidth: 290
},
{
name: 'longRetractionWhenCut',
label: 'Long Retraction When Cut (beta)',
type: 'bool',
columnWidth: 240
}
]
export const PrinterProfile = {
name: 'printerProfile',
label: 'Printer Profile',
labelPlural: 'Printer Profiles',
url: '/dashboard/production/printerprofiles',
prefix: 'PPF',
icon: PrinterProfileIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/production/printerprofiles/info?printerProfileId=${_id}`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/production/printerprofiles/info?printerProfileId=${_id}&action=edit`,
visible: (objectData) => !objectData?._isEditing
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/production/printerprofiles/info?printerProfileId=${_id}&action=finishEdit`,
visible: (objectData) => objectData?._isEditing === true
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/production/printerprofiles/info?printerProfileId=${_id}&action=cancelEdit`,
visible: (objectData) => objectData?._isEditing === true
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
row: true,
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/production/printerprofiles/info?printerProfileId=${_id}&action=delete`
}
],
columns: [
'_reference',
'name',
'printer',
'printBedWidth',
'printBedHeight',
'printableHeight',
'extruders.0.nozzleDiameter',
'extruders.0.nozzleType',
'zOffset',
'scanFirstLayer',
'auxiliaryFan',
'updatedAt'
],
filters: ['_id', 'name', 'printer'],
sorters: [
'name',
'printBedWidth',
'printBedHeight',
'printableHeight',
'zOffset',
'createdAt',
'updatedAt'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'printerProfile',
showCopy: true,
readOnly: true,
columnWidth: 140
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
objectType: 'printerProfile',
showCopy: true,
readOnly: true,
columnFixed: 'left',
columnWidth: 180
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'name',
label: 'Name',
type: 'text',
required: true,
columnFixed: 'left',
columnWidth: 220,
value: (objectData) => {
if (objectData?.name == undefined) {
return objectData?.printer?.name
}
return objectData?.name
}
},
{
name: 'printer',
label: 'Printer',
type: 'object',
objectType: 'printer',
required: true,
showHyperlink: true,
columnWidth: 220
},
{
name: 'printBedWidth',
label: 'Print Bed Width',
type: 'number',
suffix: ' mm',
min: 0,
step: 1,
columnWidth: 170
},
{
name: 'printBedHeight',
label: 'Print Bed Height',
type: 'number',
suffix: ' mm',
min: 0,
step: 1,
columnWidth: 170
},
{
name: 'originX',
label: 'Origin X',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 150
},
{
name: 'originY',
label: 'Origin Y',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 150
},
{
name: 'bestObjectPositionX',
label: 'Best Object Position X',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 200
},
{
name: 'bestObjectPositionY',
label: 'Best Object Position Y',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 200
},
{
name: 'bedExcludeArea',
label: 'Excluded Bed Area',
type: 'objectChildren',
canAddRemove: true,
span: 2,
columns: ['x', 'y'],
properties: BED_EXCLUDE_AREA_PROPERTIES
},
{
name: 'printableHeight',
label: 'Printable Height',
type: 'number',
suffix: ' mm',
min: 0,
step: 1,
columnWidth: 170
},
{
name: 'supportMultiBedTypes',
label: 'Support Multi Bed Types',
type: 'bool',
columnWidth: 200
},
{
name: 'zOffset',
label: 'Z Offset',
type: 'number',
suffix: ' mm',
step: 0.01,
columnWidth: 140
},
{
name: 'preferredOrientation',
label: 'Preferred Orientation',
type: 'number',
suffix: '°',
min: 0,
max: 360,
step: 1,
columnWidth: 190
},
{
name: 'extruders.0.nozzleDiameter',
label: 'Nozzle Diameter',
type: 'number',
suffix: ' mm',
min: 0,
step: 0.05,
readOnly: true,
columnWidth: 150
},
{
name: 'extruders.0.nozzleType',
label: 'Nozzle Type',
type: 'text',
readOnly: true,
columnWidth: 160
},
{
name: 'extruders',
label: 'Extruders',
type: 'objectChildren',
canAddRemove: true,
span: 2,
columns: [
'nozzleDiameter',
'nozzleType',
'nozzleVolume',
'minLayerHeight',
'maxLayerHeight',
'positionOffsetX',
'positionOffsetY'
],
hiddenPropertyWidth: 330,
properties: EXTRUDER_PROPERTIES
},
{
name: 'machineMaxAccelerationRetracting',
label: 'Max Retracting Accel',
type: 'minMax',
suffix: ' mm/s²',
min: 0,
step: 1,
columnWidth: 260
},
{
name: 'machineMaxSpeedE',
label: 'Max Extruder Speed',
type: 'minMax',
suffix: ' mm/s',
min: 0,
step: 1,
columnWidth: 220
},
{
name: 'machineMaxSpeedX',
label: 'Max X Speed',
type: 'minMax',
suffix: ' mm/s',
min: 0,
step: 1,
columnWidth: 190
},
{
name: 'machineMaxSpeedY',
label: 'Max Y Speed',
type: 'minMax',
suffix: ' mm/s',
min: 0,
step: 1,
columnWidth: 190
},
{
name: 'machinePauseGcode',
label: 'Pause G-code',
type: 'codeBlock',
language: 'gcode',
height: '180px',
span: 2
},
{
name: 'machineStartGcode',
label: 'Start G-code',
type: 'codeBlock',
language: 'gcode',
height: '240px',
span: 2
},
{
name: 'machineEndGcode',
label: 'End G-code',
type: 'codeBlock',
language: 'gcode',
height: '240px',
span: 2
},
{
name: 'layerChangeGcode',
label: 'Layer Change G-code',
type: 'codeBlock',
language: 'gcode',
height: '180px',
span: 2
},
{
name: 'beforeLayerChangeGcode',
label: 'Before Layer Change G-code',
type: 'codeBlock',
language: 'gcode',
height: '180px',
span: 2
},
{
name: 'machineLoadFilamentTime',
label: 'Filament Load Time',
type: 'number',
suffix: ' s',
min: 0,
step: 0.1,
columnWidth: 180
},
{
name: 'scanFirstLayer',
label: 'Scan First Layer',
type: 'bool',
columnWidth: 150
},
{
name: 'machineUnloadFilamentTime',
label: 'Filament Unload Time',
type: 'number',
suffix: ' s',
min: 0,
step: 0.1,
columnWidth: 190
},
{
name: 'thumbnails',
label: 'Thumbnail Sizes',
type: 'objectChildren',
canAddRemove: true,
span: 2,
columns: ['width', 'height'],
properties: THUMBNAIL_PROPERTIES
},
{
name: 'auxiliaryFan',
label: 'Auxiliary Fan',
type: 'bool',
columnWidth: 140
},
{
name: 'printerNotes',
label: 'Printer Notes',
type: 'markdown',
span: 2
},
{
name: 'machineMaxJunctionDeviation',
label: 'Max Junction Deviation',
type: 'minMax',
suffix: ' mm',
min: 0,
step: 0.01,
columnWidth: 240
}
]
}

View File

@ -24,11 +24,24 @@ const productionSidebarItems = [
label: 'Sub Jobs',
path: '/dashboard/production/subjobs'
},
{ type: 'divider' },
{
key: 'gcodeFiles',
iconKey: 'gcodeFile',
label: 'GCode Files',
path: '/dashboard/production/gcodefiles'
},
{
key: 'printerProfiles',
iconKey: 'printerProfile',
label: 'Printer Profiles',
path: '/dashboard/production/printerprofiles'
},
{
key: 'filamentProfiles',
iconKey: 'filamentProfile',
label: 'Filament Profiles',
path: '/dashboard/production/filamentprofiles'
}
]

View File

@ -13,6 +13,20 @@ const ControlPrinter = lazy(
const PrinterInfo = lazy(
() => import('../components/Dashboard/Production/Printers/PrinterInfo.jsx')
)
const PrinterProfiles = lazy(
() => import('../components/Dashboard/Production/PrinterProfiles.jsx')
)
const PrinterProfileInfo = lazy(
() =>
import('../components/Dashboard/Production/PrinterProfiles/PrinterProfileInfo.jsx')
)
const FilamentProfiles = lazy(
() => import('../components/Dashboard/Production/FilamentProfiles.jsx')
)
const FilamentProfileInfo = lazy(
() =>
import('../components/Dashboard/Production/FilamentProfiles/FilamentProfileInfo.jsx')
)
const Jobs = lazy(() => import('../components/Dashboard/Production/Jobs.jsx'))
const JobInfo = lazy(
() => import('../components/Dashboard/Production/Jobs/JobInfo.jsx')
@ -52,6 +66,26 @@ const ProductionRoutes = [
path='production/printers/info'
element={<PrinterInfo />}
/>,
<Route
key='printerprofiles'
path='production/printerprofiles'
element={<PrinterProfiles />}
/>,
<Route
key='printerprofiles-info'
path='production/printerprofiles/info'
element={<PrinterProfileInfo />}
/>,
<Route
key='filamentprofiles'
path='production/filamentprofiles'
element={<FilamentProfiles />}
/>,
<Route
key='filamentprofiles-info'
path='production/filamentprofiles/info'
element={<FilamentProfileInfo />}
/>,
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
<Route

View File

@ -6,6 +6,18 @@ const COOKIE_OPTIONS = {
maxAge: 7 * 24 * 60 * 60 // 7 days in seconds
}
const getAuthExpiryTime = (expiresAt) => {
const numericExpiry = Number(expiresAt)
if (Number.isFinite(numericExpiry) && numericExpiry > 0) return numericExpiry
return new Date(expiresAt).getTime()
}
const getAuthCookieMaxAge = (expiresAt) => {
const expiryTime = getAuthExpiryTime(expiresAt)
if (!Number.isFinite(expiryTime)) return COOKIE_OPTIONS.maxAge
return Math.max(Math.floor((expiryTime - Date.now()) / 1000), 1)
}
/**
* Set a cookie with authentication data
* @param {string} name - Cookie name
@ -137,10 +149,10 @@ export const validateAuthCookies = () => {
return false
}
const now = new Date()
const expirationDate = new Date(expiresAt)
const now = Date.now()
const expirationTime = getAuthExpiryTime(expiresAt)
if (expirationDate <= now) {
if (!Number.isFinite(expirationTime) || expirationTime <= now) {
// Cookies are expired, clean them up
clearAuthCookies()
return false
@ -167,9 +179,7 @@ export const checkAuthCookiesExpiry = (minutesBeforeExpiry = 5) => {
return { isExpiringSoon: false, timeRemaining: 0 }
}
const now = new Date()
const expirationDate = new Date(expiresAt)
const timeRemaining = expirationDate - now
const timeRemaining = getAuthExpiryTime(expiresAt) - Date.now()
const minutesRemaining = timeRemaining / (1000 * 60)
return {
@ -240,20 +250,21 @@ export const setAuthCookies = (authData) => {
}
let success = true
const maxAge = getAuthCookieMaxAge(authData.expires_at)
if (authData.access_token) {
success =
success &&
setCookie('authToken', authData.access_token, {
maxAge: 7 * 24 * 60 * 60
}) // 7 days
maxAge
})
}
if (authData.expires_at) {
success =
success &&
setCookie('authExpiresAt', authData.expires_at, {
maxAge: 7 * 24 * 60 * 60
maxAge
})
}
@ -267,7 +278,7 @@ export const setAuthCookies = (authData) => {
success =
success &&
setCookie('user', JSON.stringify(userObject), {
maxAge: 7 * 24 * 60 * 60
maxAge
})
}