Compare commits

...

30 Commits

Author SHA1 Message Date
97d5bfcee7 Update Filament and Printer models for improved data structure
- Added 'createdAt' to the sorters array in the Filament model for enhanced sorting capabilities.
- Refactored the Printer model to change 'moonraker.host' to 'host' and introduced a new 'host._id' field for better object representation and clarity.
- Updated the 'hostname' field to improve consistency in naming conventions.
2025-08-18 01:05:18 +01:00
ee90e75133 Refactor PrinterInfo component for improved state management and UI consistency
- Updated the PrinterInfo component to utilize React hooks for managing edit form state and actions.
- Replaced the previous job-related collapsible sections with a unified audit logs section for better clarity.
- Enhanced the layout and structure of the component to improve user experience and maintainability.
- Updated imports to ensure consistency with file extensions and component organization.
2025-08-18 01:05:07 +01:00
2eccf6736f Refactor PrinterState import to use StateDisplay component
- Updated imports in LoadFilamentStock and ControlPrinter components to replace PrinterState with StateDisplay for improved consistency in state representation.
- This change aligns with recent refactoring efforts to unify state display components across the application.
2025-08-18 01:04:58 +01:00
963f36194e Add route exports for application structure
- Created a new index.js file to export routes for Production, Inventory, Management, and Developer sections.
- This centralizes route management and enhances modularity within the application.
2025-08-18 01:04:32 +01:00
a18831e67a Add document management section to ManagementSidebar
- Introduced a new 'Documents' section in the ManagementSidebar with sub-items for Document Printers, Document Templates, and Document Sizes.
- Updated existing keys for consistency, changing 'notetypes' to 'noteTypes' and 'auditlogs' to 'auditLogs'.
- Enhanced routeKeyMap to include new document-related paths for improved navigation.
2025-08-18 01:04:23 +01:00
8e823603f7 Enhance PropertyChanges component to support minimal rendering
- Added a 'minimal' prop to ObjectProperty components within PropertyChanges for improved rendering control.
- Updated both old and new object data displays to utilize the new 'minimal' prop, enhancing component flexibility.
2025-08-18 01:03:54 +01:00
11c76a0725 Refactor PrinterJobsTree component to enhance state display
- Replaced JobState and SubJobState components with a unified StateDisplay component for improved consistency in state representation.
- Updated the rendering logic to utilize StateDisplay for both job and sub-job states, enhancing code clarity and maintainability.
2025-08-18 01:03:44 +01:00
9a406bb7df Add ObjectTypeDisplay and ObjectTypeSelect components for enhanced object type management
- Introduced ObjectTypeDisplay component to visually represent object types with optional icons and labels.
- Created ObjectTypeSelect component to allow users to select object types from a dropdown, utilizing ObjectTypeDisplay for rendering options.
- Implemented prop types for both components to ensure proper type checking and documentation clarity.
2025-08-18 01:03:17 +01:00
b4763e01af Refactor ObjectTable component to improve real-time update handling
- Updated subscription methods to use more descriptive names for clarity.
- Simplified the new event handler to always reload data on updates.
- Enhanced code readability by restructuring useEffect dependencies and callback functions.
2025-08-18 01:03:05 +01:00
dcf90469f4 Refactor ObjectSelect component to improve code clarity and maintainability
- Removed unnecessary whitespace to enhance code readability.
- Streamlined data handling logic for better performance and consistency.
- Improved structure of the loadData function for clearer data fetching and processing.
2025-08-18 01:02:48 +01:00
e423f32493 Enhance ObjectSelect component to support additional filtering and improved data handling
- Introduced a new 'masterFilter' prop to allow for more complex filtering scenarios.
- Updated data fetching logic to handle both array and object responses from the API, ensuring robust data management.
- Improved tree data handling by refining how children nodes are built and updated.
- Added a 'disabled' prop to control the component's interactivity.
- Enhanced prop types for better type checking and documentation clarity.
2025-08-18 01:02:27 +01:00
08dbbefada Enhance ObjectActions component to support action visibility filtering
- Introduced a recursive function to filter actions based on visibility, allowing for dynamic control over which actions are displayed.
- Updated the component to accept a new 'visibleActions' prop for improved customization.
- Improved code readability by restructuring the action filtering logic and ensuring proper handling of nested actions.
2025-08-18 01:01:29 +01:00
27e843e183 Refactor DashboardNavigation to streamline API server state management
- Removed unused PrintServerContext and related print server state management.
- Updated API server state handling to use a single context for connection status.
- Improved code readability by consolidating state checks and simplifying the rendering logic for connection status indicators.
2025-08-18 01:00:40 +01:00
df1115022a Refactor DashboardSidebar to support recursive item mapping
- Introduced a recursive function to map sidebar items and their children, enhancing the structure and maintainability of the component.
- Added onClick functionality for items with paths, improving navigation capabilities.
- Improved code readability by restructuring the item mapping logic.
2025-08-18 01:00:25 +01:00
b74a4bb174 Enhance CopyButton component to support customizable button type
- Added a new 'type' prop to the CopyButton component, allowing users to specify the button type.
- Updated the component's prop types to include the new 'type' prop for better type checking and documentation.
- Improved code readability by restructuring the component's props definition.
2025-08-18 01:00:11 +01:00
b45df50da5 Remove inital gcode. 2025-08-18 01:00:02 +01:00
653e4e6ab2 Refactor DashboardBreadcrumb component to enhance breadcrumb functionality
- Introduced a new segmentToModel function to dynamically retrieve model names based on URL segments.
- Simplified breadcrumb name mapping by removing unused entries and adding support for pluralized model labels.
- Improved breadcrumb item rendering logic for better clarity and maintainability.
2025-08-18 00:59:47 +01:00
177b439c48 Enhance AuthContext to manage user profile in session storage
- Added functionality to store and retrieve user profile data from session storage.
- Updated authentication logic to check for user data alongside token and expiration.
- Ensured user profile is cleared upon logout and set to null when authentication fails.
- Improved logging for better debugging of user authentication flow.
2025-08-18 00:59:37 +01:00
678d5a0e90 Refactor ActionHandler and EditObjectForm components to support forward refs and improve functionality
- Updated ActionHandler to use forwardRef, allowing parent components to access the callAction method.
- Enhanced EditObjectForm with forwardRef, enabling external control over editing state and form handling.
- Added onEdit and onStateChange props to EditObjectForm for better integration with parent components.
- Improved form validation and state management within EditObjectForm, ensuring a smoother user experience.
- Cleaned up code for better readability and maintainability.
2025-08-18 00:58:52 +01:00
4201f2b4a3 Add CodeBlockEditor component for enhanced code editing experience
- Introduced CodeBlockEditor component to support multiple programming languages with syntax highlighting using CodeMirror.
- Implemented features such as customizable styles, read-only mode, and line number visibility.
- Added support for JSON object parsing and modal display for minimal view.
- Integrated theme context for dark mode support and improved user interaction with onChange handling.
- Defined prop types for better type checking and documentation.
2025-08-18 00:58:30 +01:00
a9a2801e27 Refactor ObjectProperty component and introduce StateDisplay
- Removed unused components (PrinterState, SubJobState, JobState, FilamentStockState).
- Added new StateDisplay component for handling state representation.
- Enhanced ObjectProperty to support new properties and rendering logic for code blocks and object types.
- Updated import paths and prop types accordingly for improved clarity and functionality.
2025-08-18 00:58:10 +01:00
a6a52fa81b Updated control and design icon. 2025-08-18 00:56:50 +01:00
93848602bb Refactor ApiServerContext to improve socket event handling and authentication
- Updated socket connection to use 'user' type for authentication.
- Refactored event handling methods for object updates and new objects to use more descriptive names.
- Enhanced subscription methods for object updates and types, ensuring better organization and clarity.
- Introduced new methods for fetching template previews, generating host OTP, and sending object actions.
- Improved logging for better debugging and tracking of socket events.
2025-08-18 00:56:39 +01:00
e4ab4ea9c7 More document templates code. 2025-08-18 00:56:14 +01:00
e904cc10b4 Removed old audit log tabel. 2025-08-18 00:55:32 +01:00
aa41ad0192 Added splitter margin. 2025-08-18 00:55:20 +01:00
257cebf15c Refactor routing structure in App component to utilize modular route files for Production, Inventory, Management, and Developer sections. Update import paths for stylesheets and streamline route management for improved maintainability and organization. 2025-08-18 00:54:54 +01:00
ed322436e6 Add OTP functionality and related components for host management
- Introduced HostOTP component to handle one-time passcode generation and display.
- Added OTPIcon for visual representation of OTP functionality.
- Updated HostInfo component to integrate OTP modal and manage state for OTP actions.
- Refactored Host model to include a new 'connect' action for OTP access.
- Created new SVG and design assets for OTP icon representation.
- Enhanced user experience with loading states and progress indicators for OTP validity.
2025-08-18 00:54:20 +01:00
ec2d656b6e Add package-lock.json and update package.json with new dependencies
- Introduced package-lock.json to manage exact versions of dependencies.
- Updated package.json to include additional CodeMirror language support and UI components.
- Adjusted yarn.lock to reflect changes in package resolutions and versions.
2025-08-18 00:53:44 +01:00
c73b6fb33d Add new document management components and icons
- Introduced DocumentPrinters, DocumentSizes, and DocumentTemplates components for managing respective entities.
- Added corresponding info and design components for each entity.
- Created new SVG icons for document-related functionalities.
- Implemented hooks for column visibility and view mode management in the new components.
- Enhanced user experience with action buttons and modals for creating new entries.
2025-08-18 00:53:14 +01:00
80 changed files with 36815 additions and 3735 deletions

29714
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,9 +10,25 @@
"dependencies": {
"@ant-design/charts": "^2.6.2",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.3.4",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sql": "^6.9.1",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@simplewebauthn/browser": "^13.1.2",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
"@uiw/react-codemirror": "^4.25.1",
"antd": "^5.27.0",
"antd-style": "^3.7.1",
"axios": "^1.11.0",

View File

@ -8,69 +8,29 @@ import {
} from 'react-router-dom'
import { App, ConfigProvider } from 'antd'
import ProductionOverview from './components/Dashboard/Production/ProductionOverview'
import Printers from './components/Dashboard/Production/Printers'
import ControlPrinter from './components/Dashboard/Production/Printers/ControlPrinter.jsx'
import PrinterInfo from './components/Dashboard/Production/Printers/PrinterInfo.jsx'
import Jobs from './components/Dashboard/Production/Jobs.jsx'
import JobInfo from './components/Dashboard/Production/Jobs/JobInfo.jsx'
import Filaments from './components/Dashboard/Management/Filaments'
import FilamentInfo from './components/Dashboard/Management/Filaments/FilamentInfo.jsx'
import GCodeFiles from './components/Dashboard/Production/GCodeFiles'
import GCodeFileInfo from './components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
import Parts from './components/Dashboard/Management/Parts.jsx'
import PartInfo from './components/Dashboard/Management/Parts/PartInfo.jsx'
import Products from './components/Dashboard/Management/Products.jsx'
import ProductInfo from './components/Dashboard/Management/Products/ProductInfo.jsx'
import Vendors from './components/Dashboard/Management/Vendors'
import VendorInfo from './components/Dashboard/Management/Vendors/VendorInfo'
import Materials from './components/Dashboard/Management/Materials'
import FilamentStocks from './components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from './components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from './components/Dashboard/Inventory/PartStocks.jsx'
import StockAudits from './components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
import Dashboard from './components/Dashboard/Dashboard.jsx'
import PrivateRoute from './components/PrivateRoute'
import './App.css'
import './assets/stylesheets/App.css'
import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.js'
import { AuthProvider } from './components/Dashboard/context/AuthContext.js'
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js'
import StockEvents from './components/Dashboard/Inventory/StockEvents.jsx'
import Settings from './components/Dashboard/Management/Settings'
import AuditLogs from './components/Dashboard/Management/AuditLogs.jsx'
import {
ThemeProvider,
useThemeContext
} from './components/Dashboard/context/ThemeContext'
import AppError from './components/App/AppError'
import NoteTypes from './components/Dashboard/Management/NoteTypes.jsx'
import NoteTypeInfo from './components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx'
import SessionStorage from './components/Dashboard/Developer/SessionStorage.jsx'
import AuthContextDebug from './components/Dashboard/Developer/AuthContextDebug.jsx'
import PrintServerContextDebug from './components/Dashboard/Developer/PrintServerContextDebug.jsx'
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.js'
import Users from './components/Dashboard/Management/Users.jsx'
import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx'
import SubJobs from './components/Dashboard/Production/SubJobs.jsx'
import Hosts from './components/Dashboard/Management/Hosts.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js'
import AuthCallback from './components/App/AuthCallback.jsx'
import {
ProductionRoutes,
InventoryRoutes,
ManagementRoutes,
DeveloperRoutes
} from './routes'
const getRouter = () => {
if (
typeof window !== 'undefined' &&
@ -115,135 +75,10 @@ const AppContent = () => {
<PrivateRoute component={() => <Dashboard />} />
}
>
{/* Production Routes */}
<Route
path='production/overview'
element={<ProductionOverview />}
/>
<Route
path='production/printers'
element={<Printers />}
/>
<Route
path='production/printers/control'
element={<ControlPrinter />}
/>
<Route
path='production/printers/info'
element={<PrinterInfo />}
/>
<Route path='production/jobs' element={<Jobs />} />
<Route
path='production/subjobs'
element={<SubJobs />}
/>
<Route
path='production/jobs/info'
element={<JobInfo />}
/>
<Route
path='production/gcodefiles'
element={<GCodeFiles />}
/>
<Route
path='production/gcodefiles/info'
element={<GCodeFileInfo />}
/>
{/* Inventory Routes */}
<Route
path='inventory/filamentstocks'
element={<FilamentStocks />}
/>
<Route
path='inventory/filamentstocks/info'
element={<FilamentStockInfo />}
/>
<Route
path='inventory/partstocks'
element={<PartStocks />}
/>
<Route
path='inventory/stockevents'
element={<StockEvents />}
/>
<Route
path='inventory/stockaudits'
element={<StockAudits />}
/>
<Route
path='inventory/stockaudits/info'
element={<StockAuditInfo />}
/>
{/* Management Routes */}
<Route
path='management/filaments'
element={<Filaments />}
/>
<Route
path='management/filaments/info'
element={<FilamentInfo />}
/>
<Route path='management/parts' element={<Parts />} />
<Route
path='management/parts/info'
element={<PartInfo />}
/>
<Route
path='management/products'
element={<Products />}
/>
<Route
path='management/products/info'
element={<ProductInfo />}
/>
<Route
path='management/vendors'
element={<Vendors />}
/>
<Route path='management/hosts' element={<Hosts />} />
<Route
path='management/users/info'
element={<UserInfo />}
/>
<Route
path='management/vendors/info'
element={<VendorInfo />}
/>
<Route
path='management/materials'
element={<Materials />}
/>
<Route
path='management/notetypes'
element={<NoteTypes />}
/>
<Route path='management/users' element={<Users />} />
<Route
path='management/notetypes/info'
element={<NoteTypeInfo />}
/>
<Route
path='management/settings'
element={<Settings />}
/>
<Route
path='management/auditlogs'
element={<AuditLogs />}
/>
<Route
path='developer/sessionstorage'
element={<SessionStorage />}
/>
<Route
path='developer/authcontextdebug'
element={<AuthContextDebug />}
/>
<Route
path='developer/printservercontextdebug'
element={<PrintServerContextDebug />}
/>
{ProductionRoutes}
{InventoryRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>
<Route
path='*'

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M13.734 57.726h62.96c9.856 0 14.795-6.142 13.535-16.562l-3.392-28.218C85.843 4.532 81.097 0 73.389 0h-56.35C9.33 0 4.579 4.532 3.585 12.946L.194 41.164c-1.261 10.42 3.678 16.562 13.54 16.562m0-6.122c-5.47 0-8.202-3.888-7.404-10.554l3.315-27.376c.597-4.891 3.173-7.552 7.394-7.552h56.35c4.221 0 6.791 2.661 7.368 7.552l3.309 27.376c.83 6.691-1.907 10.554-7.372 10.554z" style="fill-rule:nonzero" transform="translate(0 11.571)scale(.70779)"/><path d="M24.535 37.579c1.291 0 2.316-.613 2.316-1.49v-8.638c3.169-.937 5.499-3.927 5.499-7.436 0-4.301-3.514-7.821-7.815-7.821-4.327 0-7.827 3.545-7.827 7.821 0 3.535 2.336 6.504 5.536 7.487v8.587c0 .877 1.005 1.49 2.291 1.49m.006 6.861c6.461 0 10.983-3.479 10.983-8.382 0-3.397-2.182-6.087-5.613-7.445v4.398c1.026.747 1.635 1.799 1.635 3.047 0 2.844-2.869 4.833-7.005 4.833-4.142 0-7.043-1.989-7.043-4.833 0-1.274.629-2.331 1.687-3.079v-4.36c-3.478 1.32-5.66 4.042-5.66 7.439 0 4.903 4.554 8.382 11.016 8.382m18.255-17.511c2.605 0 4.675-2.065 4.675-4.643a4.65 4.65 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.649 4.675 4.64 4.64 0 0 0 4.649 4.643m13.331 0c2.574 0 4.675-2.065 4.675-4.643a4.67 4.67 0 0 0-4.675-4.675 4.654 4.654 0 0 0-4.675 4.675c0 2.578 2.077 4.643 4.675 4.643m13.258 0c2.579 0 4.675-2.065 4.675-4.643a4.666 4.666 0 0 0-4.675-4.675 4.673 4.673 0 0 0-4.675 4.675c0 2.578 2.107 4.643 4.675 4.643M47.112 40.193a4.63 4.63 0 0 0 4.65-4.644 4.645 4.645 0 0 0-4.65-4.675 4.657 4.657 0 0 0-4.675 4.675c0 2.579 2.082 4.644 4.675 4.644m13.306 0c2.599 0 4.675-2.065 4.675-4.644a4.653 4.653 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.649 4.675 4.634 4.634 0 0 0 4.649 4.644m13.289 0c2.599 0 4.675-2.065 4.675-4.644a4.653 4.653 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.65 4.675 4.635 4.635 0 0 0 4.65 4.644" style="fill-rule:nonzero" transform="translate(0 11.571)scale(.70779)"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M13.734 57.726h62.96c9.856 0 14.795-6.142 13.535-16.562l-3.392-28.218C85.843 4.532 81.097 0 73.389 0h-56.35C9.33 0 4.579 4.532 3.585 12.946L.194 41.164c-1.261 10.42 3.678 16.562 13.54 16.562m0-6.122c-5.47 0-8.202-3.888-7.404-10.554l3.315-27.376c.597-4.891 3.173-7.552 7.394-7.552h56.35c4.221 0 6.791 2.661 7.368 7.552l3.309 27.376c.83 6.691-1.907 10.554-7.372 10.554z" style="fill-rule:nonzero" transform="translate(0 11.571)scale(.7078)"/><path d="M24.535 37.579c1.291 0 2.316-.613 2.316-1.49v-8.638c3.169-.937 5.499-3.927 5.499-7.436 0-4.301-3.514-7.821-7.815-7.821-4.327 0-7.827 3.545-7.827 7.821 0 3.535 2.336 6.504 5.536 7.487v8.587c0 .877 1.005 1.49 2.291 1.49m.006 6.861c6.461 0 10.983-3.479 10.983-8.382 0-3.397-2.182-6.087-5.613-7.445v4.398c1.026.747 1.635 1.799 1.635 3.047 0 2.844-2.869 4.833-7.005 4.833-4.142 0-7.043-1.989-7.043-4.833 0-1.274.629-2.331 1.687-3.079v-4.36c-3.478 1.32-5.66 4.042-5.66 7.439 0 4.903 4.554 8.382 11.016 8.382m18.255-17.511c2.605 0 4.675-2.065 4.675-4.643a4.65 4.65 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.649 4.675 4.64 4.64 0 0 0 4.649 4.643m13.331 0c2.574 0 4.675-2.065 4.675-4.643a4.67 4.67 0 0 0-4.675-4.675 4.654 4.654 0 0 0-4.675 4.675c0 2.578 2.077 4.643 4.675 4.643m13.258 0c2.579 0 4.675-2.065 4.675-4.643a4.666 4.666 0 0 0-4.675-4.675 4.673 4.673 0 0 0-4.675 4.675c0 2.578 2.107 4.643 4.675 4.643M47.112 40.193a4.63 4.63 0 0 0 4.65-4.644 4.645 4.645 0 0 0-4.65-4.675 4.657 4.657 0 0 0-4.675 4.675c0 2.579 2.082 4.644 4.675 4.644m13.306 0c2.599 0 4.675-2.065 4.675-4.644a4.653 4.653 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.649 4.675 4.634 4.634 0 0 0 4.649 4.644m13.289 0c2.599 0 4.675-2.065 4.675-4.644a4.653 4.653 0 0 0-4.675-4.675 4.65 4.65 0 0 0-4.65 4.675 4.635 4.635 0 0 0 4.65 4.644" style="fill-rule:nonzero" transform="translate(0 11.571)scale(.7078)"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M4.098 71.551c5.422 5.426 11.763 5.489 17.102.161 3.719-3.719 7.626-12.355 10.53-15.945l6.498 6.53c2.612 2.643 5.814 2.654 8.358.099l4.099-4.13c2.555-2.586 2.538-5.689-.1-8.326L25.74 25.058c-2.643-2.649-5.777-2.674-8.363-.088l-4.093 4.119c-2.561 2.56-2.581 5.708.074 8.357l6.519 6.499c-3.559 2.904-12.195 6.791-15.909 10.504-5.328 5.339-5.296 11.706.13 17.102m14.21-39.074 2.43-2.384c.706-.7 1.474-.731 2.18-.026L45.596 52.72c.68.706.68 1.479-.051 2.211l-2.353 2.378c-.712.763-1.525.788-2.242.026l-7.503-7.528c-1.274-1.28-2.965-1.155-4.413.255-2.437 2.401-6.679 12.615-11.665 17.564-2.859 2.884-6.31 2.879-9.283-.063-2.916-2.948-2.953-6.435-.068-9.294 4.985-4.943 15.194-9.191 17.6-11.634 1.379-1.468 1.53-3.159.25-4.413l-7.56-7.54c-.711-.705-.711-1.493 0-2.205m-5.655 34.304c2.052 0 3.709-1.682 3.709-3.729a3.703 3.703 0 0 0-3.709-3.703c-2.046 0-3.728 1.657-3.728 3.703a3.745 3.745 0 0 0 3.728 3.729m37.082-13.117 15.103-15.097c3.62-3.626 3.547-7.991-.168-11.711L40.105 2.254c-3.551-3.551-9.537-2.107-10.76 3.28-3.019 13.052-3.048 14.119-7.942 20.958l4.128 4.117c5.806-7.465 6.139-11.005 9.128-22.015.356-1.391 1.499-1.752 2.438-.839l23.204 23.178c1.297 1.297 1.291 2.783.099 3.98L45.695 49.624zm-5.027-18.24c1.65 1.656 10.781-3.407 13.932-7.375l-6.87-6.844c-.594 5.531-3.469 9.531-6.969 13.031-.406.406-.375.912-.093 1.188" style="fill-rule:nonzero" transform="translate(6.02 2.76)scale(.7697)"/></svg>

After

Width:  |  Height:  |  Size: 1.5 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.7697,0,0,0.7697,6.02054,2.76086)">
<path d="M4.098,71.551C9.52,76.977 15.861,77.04 21.2,71.712C24.919,67.993 28.826,59.357 31.73,55.767L38.228,62.297C40.84,64.94 44.042,64.951 46.586,62.396L50.685,58.266C53.24,55.68 53.223,52.577 50.585,49.94L25.74,25.058C23.097,22.409 19.963,22.384 17.377,24.97L13.284,29.089C10.723,31.649 10.703,34.797 13.358,37.446L19.877,43.945C16.318,46.849 7.682,50.736 3.968,54.449C-1.36,59.788 -1.328,66.155 4.098,71.551ZM18.308,32.477L20.738,30.093C21.444,29.393 22.212,29.362 22.918,30.067L45.596,52.72C46.276,53.426 46.276,54.199 45.545,54.931L43.192,57.309C42.48,58.072 41.667,58.097 40.95,57.335L33.447,49.807C32.173,48.527 30.482,48.652 29.034,50.062C26.597,52.463 22.355,62.677 17.369,67.626C14.51,70.51 11.059,70.505 8.086,67.563C5.17,64.615 5.133,61.128 8.018,58.269C13.003,53.326 23.212,49.078 25.618,46.635C26.997,45.167 27.148,43.476 25.868,42.222L18.308,34.682C17.597,33.977 17.597,33.189 18.308,32.477ZM12.653,66.781C14.705,66.781 16.362,65.099 16.362,63.052C16.362,61.006 14.705,59.349 12.653,59.349C10.607,59.349 8.925,61.006 8.925,63.052C8.925,65.099 10.607,66.781 12.653,66.781ZM49.735,53.664L64.838,38.567C68.458,34.941 68.385,30.576 64.67,26.856L40.105,2.254C36.554,-1.297 30.568,0.147 29.345,5.534C26.326,18.586 26.297,19.653 21.403,26.492L25.531,30.609C31.337,23.144 31.67,19.604 34.659,8.594C35.015,7.203 36.158,6.842 37.097,7.755L60.301,30.933C61.598,32.23 61.592,33.716 60.4,34.913L45.695,49.624L49.735,53.664ZM44.708,35.424C46.358,37.08 55.489,32.017 58.64,28.049L51.77,21.205C51.176,26.736 48.301,30.736 44.801,34.236C44.395,34.642 44.426,35.148 44.708,35.424Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M15.46 13.515c0-6.725 3.475-10.252 10.149-10.252h11.744c3.596 0 6.407.934 8.744 3.328l15.584 15.857c2.468 2.53 3.322 5.153 3.322 9.261v25.879c0 6.719-3.475 10.251-10.149 10.251h-6.939v-6.122h6.405c3.036 0 4.561-1.604 4.561-4.51V30.221H45.094c-3.841 0-5.878-2.011-5.878-5.878V9.386H26.118c-3.037 0-4.536 1.629-4.536 4.509v5.572a24 24 0 0 0-.543-.006H15.46zM44.326 23.57c0 1.071.443 1.54 1.515 1.54h11.446L44.326 11.945z" style="fill-rule:nonzero" transform="translate(7.224 .331)scale(.76231)"/><path d="M10.149 67.641h33.289c6.699 0 10.148-3.506 10.148-10.231V29.058c0-4.341-.558-6.333-3.278-9.104L33.894 3.284C31.282.616 29.111 0 25.212 0H10.149C3.481 0 0 3.532 0 10.257V57.41c0 6.751 3.455 10.231 10.149 10.231m.488-6.122c-3.016 0-4.515-1.578-4.515-4.49V10.637c0-2.885 1.499-4.515 4.541-4.515h13.321v17.139c0 4.472 2.183 6.634 6.635 6.634h16.845v27.134c0 2.912-1.499 4.49-4.535 4.49zm20.561-37.023c-1.295 0-1.814-.551-1.814-1.84V6.973l17.229 17.523z" style="fill-rule:nonzero" transform="translate(7.224 12.833)scale(.7121)"/><path d="M37.787 51.93c0-1.06-.86-1.921-1.92-1.921h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.862 1.921-1.923m.001-6.297c0-1.06-.86-1.922-1.92-1.922h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.861 1.921-1.923m.001-6.397c0-1.06-.86-1.922-1.92-1.922h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.861 1.921-1.923m-16.139-6.473c0-1.06-.861-1.922-1.922-1.922H16.74a1.923 1.923 0 0 0 0 3.845h2.986c1.06 0 1.922-.861 1.922-1.923"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.75929,0,0,0.75929,7.36648,3.81256)">
<g transform="matrix(1.00398,0,0,1.00398,-0.188198,-4.58479)">
<path d="M15.46,13.515C15.46,6.79 18.935,3.263 25.609,3.263L37.353,3.263C40.949,3.263 43.76,4.197 46.097,6.591L61.681,22.448C64.149,24.978 65.003,27.601 65.003,31.709L65.003,57.588C65.003,64.307 61.528,67.839 54.854,67.839L47.915,67.839L47.915,61.717L54.32,61.717C57.356,61.717 58.881,60.113 58.881,57.207L58.881,30.221L45.094,30.221C41.253,30.221 39.216,28.21 39.216,24.343L39.216,9.386L26.118,9.386C23.081,9.386 21.582,11.015 21.582,13.895L21.582,19.467C21.407,19.463 21.225,19.461 21.039,19.461L15.46,19.461L15.46,13.515ZM44.326,23.57C44.326,24.641 44.769,25.11 45.841,25.11L57.287,25.11L44.326,11.945L44.326,23.57Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.937843,0,0,0.937843,-0.188198,11.8804)">
<path d="M10.149,67.641L43.438,67.641C50.137,67.641 53.586,64.135 53.586,57.41L53.586,29.058C53.586,24.717 53.028,22.725 50.308,19.954L33.894,3.284C31.282,0.616 29.111,0 25.212,0L10.149,0C3.481,0 0,3.532 0,10.257L0,57.41C0,64.161 3.455,67.641 10.149,67.641ZM10.637,61.519C7.621,61.519 6.122,59.941 6.122,57.029L6.122,10.637C6.122,7.752 7.621,6.122 10.663,6.122L23.984,6.122L23.984,23.261C23.984,27.733 26.167,29.895 30.619,29.895L47.464,29.895L47.464,57.029C47.464,59.941 45.965,61.519 42.929,61.519L10.637,61.519ZM31.198,24.496C29.903,24.496 29.384,23.945 29.384,22.656L29.384,6.973L46.613,24.496L31.198,24.496Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.937843,0,0,0.937843,-0.188198,12.4057)">
<g transform="matrix(4.7745,0,0,1.46137,-127.467,5.67668)">
<path d="M35.687,33.303C35.687,32.284 35.434,31.456 35.122,31.456L29.496,31.456C29.184,31.456 28.931,32.284 28.931,33.303C28.931,34.323 29.184,35.151 29.496,35.151L35.122,35.151C35.434,35.151 35.687,34.323 35.687,33.303Z"/>
</g>
<g transform="matrix(4.7745,0,0,1.46137,-127.467,-3.16531)">
<path d="M35.687,33.303C35.687,32.284 35.434,31.456 35.122,31.456L29.496,31.456C29.184,31.456 28.931,32.284 28.931,33.303C28.931,34.323 29.184,35.151 29.496,35.151L35.122,35.151C35.434,35.151 35.687,34.323 35.687,33.303Z"/>
</g>
<g transform="matrix(4.7745,0,0,1.46137,-127.467,-12.1487)">
<path d="M35.687,33.303C35.687,32.284 35.434,31.456 35.122,31.456L29.496,31.456C29.184,31.456 28.931,32.284 28.931,33.303C28.931,34.323 29.184,35.151 29.496,35.151L35.122,35.151C35.434,35.151 35.687,34.323 35.687,33.303Z"/>
</g>
<g transform="matrix(1.41967,0,0,1.46137,-30.4087,-21.2388)">
<path d="M35.687,33.303C35.687,32.284 34.835,31.456 33.786,31.456L30.832,31.456C29.783,31.456 28.931,32.284 28.931,33.303C28.931,34.323 29.783,35.151 30.832,35.151L33.786,35.151C34.835,35.151 35.687,34.323 35.687,33.303Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M58.753 9.703v1.679h-5.704V9.269c0-2.366-1.2-3.535-3.555-3.535H21.921c-2.329 0-3.555 1.169-3.555 3.535v2.113h-5.704V9.703c0-6.347 3.417-9.234 9.265-9.234h27.56c6.111 0 9.266 2.887 9.266 9.234m12.662 11.358v28.755c0 6.375-3.432 9.68-9.782 9.68h-3.245v-5.535h3.251c2.565 0 3.918-1.347 3.918-3.917V20.839c0-2.57-1.353-3.928-3.918-3.928H9.801c-2.59 0-3.917 1.358-3.917 3.928v29.205c0 2.57 1.327 3.917 3.917 3.917h3.226v5.535h-3.22C3.432 59.496 0 56.191 0 49.816V21.061c0-6.349 3.694-9.679 9.807-9.679h51.826c6.35 0 9.782 3.33 9.782 9.679m-12.861 3.172c0 2.235-1.843 4.052-4.027 4.052a4.057 4.057 0 0 1-4.053-4.052c0-2.184 1.818-4.027 4.053-4.027 2.184 0 4.027 1.843 4.027 4.027" style="fill-rule:nonzero" transform="translate(4 4.73)scale(.78415)"/><path d="M19.642 69.086h32.131c4.43 0 6.615-2.032 6.615-6.621V37.831c0-4.583-2.185-6.615-6.615-6.615H19.642c-4.27 0-6.615 2.032-6.615 6.615v24.634c0 4.589 2.186 6.621 6.615 6.621m1.316-5.393c-1.494 0-2.27-.745-2.27-2.27v-22.57c0-1.524.776-2.238 2.27-2.238h29.525c1.519 0 2.244.714 2.244 2.238v22.57c0 1.525-.725 2.27-2.244 2.27zm4.428-16.733h20.711c1.221 0 2.124-.928 2.124-2.154 0-1.164-.903-2.061-2.124-2.061H25.386c-1.232 0-2.155.897-2.155 2.061 0 1.226.928 2.154 2.155 2.154m0 10.636h20.711c1.221 0 2.124-.922 2.124-2.092 0-1.2-.903-2.128-2.124-2.128H25.386c-1.227 0-2.155.928-2.155 2.128 0 1.17.923 2.092 2.155 2.092" style="fill-rule:nonzero" transform="translate(4 4.73)scale(.78415)"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
<?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.784149,0,0,0.784149,2.8024,2.29292)">
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
<path d="M58.753,9.703L58.753,11.382L53.049,11.382L53.049,9.269C53.049,6.903 51.849,5.734 49.494,5.734L21.921,5.734C19.592,5.734 18.366,6.903 18.366,9.269L18.366,11.382L12.662,11.382L12.662,9.703C12.662,3.356 16.079,0.469 21.927,0.469L49.487,0.469C55.598,0.469 58.753,3.356 58.753,9.703Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
<path d="M71.415,21.061L71.415,49.816C71.415,56.191 67.983,59.496 61.633,59.496L58.388,59.496L58.388,53.961L61.639,53.961C64.204,53.961 65.557,52.614 65.557,50.044L65.557,20.839C65.557,18.269 64.204,16.911 61.639,16.911L9.801,16.911C7.211,16.911 5.884,18.269 5.884,20.839L5.884,50.044C5.884,52.614 7.211,53.961 9.801,53.961L13.027,53.961L13.027,59.496L9.807,59.496C3.432,59.496 0,56.191 0,49.816L0,21.061C0,14.712 3.694,11.382 9.807,11.382L61.633,11.382C67.983,11.382 71.415,14.712 71.415,21.061ZM58.554,24.233C58.554,26.468 56.711,28.285 54.527,28.285C52.292,28.285 50.474,26.468 50.474,24.233C50.474,22.049 52.292,20.206 54.527,20.206C56.711,20.206 58.554,22.049 58.554,24.233Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,1.52726,3.10699)">
<path d="M19.642,69.086L51.773,69.086C56.203,69.086 58.388,67.054 58.388,62.465L58.388,37.831C58.388,33.248 56.203,31.216 51.773,31.216L19.642,31.216C15.372,31.216 13.027,33.248 13.027,37.831L13.027,62.465C13.027,67.054 15.213,69.086 19.642,69.086ZM20.958,63.693C19.464,63.693 18.688,62.948 18.688,61.423L18.688,38.853C18.688,37.329 19.464,36.615 20.958,36.615L50.483,36.615C52.002,36.615 52.727,37.329 52.727,38.853L52.727,61.423C52.727,62.948 52.002,63.693 50.483,63.693L20.958,63.693ZM25.386,46.96L46.097,46.96C47.318,46.96 48.221,46.032 48.221,44.806C48.221,43.642 47.318,42.745 46.097,42.745L25.386,42.745C24.154,42.745 23.231,43.642 23.231,44.806C23.231,46.032 24.159,46.96 25.386,46.96ZM25.386,57.596L46.097,57.596C47.318,57.596 48.221,56.674 48.221,55.504C48.221,54.304 47.318,53.376 46.097,53.376L25.386,53.376C24.159,53.376 23.231,54.304 23.231,55.504C23.231,56.674 24.154,57.596 25.386,57.596Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M52.109 26.338V23.46h-10.51c-2.928 0-4.481-1.534-4.481-4.481V7.577h-9.985c-2.314 0-3.457 1.242-3.457 3.437v4.248a18 18 0 0 0-.414-.005h-4.253v-4.533c0-5.126 2.649-7.814 7.736-7.814h8.953c2.742 0 4.884.711 6.666 2.536l11.88 12.088c1.881 1.929 2.532 3.928 2.532 7.06v1.744zM41.014 18.39c0 .816.338 1.174 1.155 1.174h8.725l-9.88-10.037z" style="fill-rule:nonzero"/><path d="M26.506 61.09H14.451c-4.767 0-7.227-2.478-7.227-7.285V20.228c0-4.789 2.478-7.304 7.227-7.304h10.726c2.776 0 4.322.438 6.182 2.338l10.906 11.076h-5.81l-8.307-8.449v10.824a7.1 7.1 0 0 0-1.81 4.738v.216c-1.363-.701-2.036-2.084-2.036-4.179V17.283h-9.485c-2.167 0-3.234 1.161-3.234 3.215v33.036c0 2.073 1.067 3.197 3.215 3.197h11.54v2.818q.002.795.168 1.541" style="fill-rule:nonzero"/><path d="M26.338 50.1H16.74a1.923 1.923 0 0 0 0 3.844h9.6zm0-6.297H16.74a1.923 1.923 0 0 0 0 3.845h9.6zm0-6.397H16.74a1.923 1.923 0 0 0 0 3.845h9.6zm-4.691-4.551c0-1.06-.861-1.922-1.922-1.922H16.74a1.923 1.923 0 0 0 0 3.845h2.986c1.06 0 1.922-.862 1.922-1.923"/><path d="m25.075 47.705-1.007-1.007-1.894-1.796-2.246-2.465.056 2.381.033 2.347c.032.471-.16.965-.496 1.3-.675.676-1.668.694-2.332.029-.384-.383-.524-.801-.488-1.331l.35-8.005c.031-.659.186-1.097.546-1.458.361-.36.799-.516 1.458-.546l8.005-.35c.53-.036.948.104 1.331.487.665.665.657 1.648-.031 2.336a1.76 1.76 0 0 1-1.298.493l-2.349-.03-2.379-.059 2.465 2.247 1.425 1.326 1.945 1.945 1.327 1.425 2.246 2.465-.059-2.379-.03-2.349a1.76 1.76 0 0 1 .493-1.298c.688-.688 1.671-.696 2.336-.031.383.383.523.801.487 1.331l-.35 8.005c-.03.659-.186 1.097-.546 1.458s-.799.515-1.458.546l-8.005.35c-.53.036-.947-.104-1.331-.488-.665-.664-.647-1.657.029-2.332a1.72 1.72 0 0 1 1.3-.496l2.347.033 2.381.056-2.465-2.246z" style="fill-rule:nonzero" transform="matrix(-1.06473 0 0 1.06473 74.043 -2.257)"/><path d="M11.031 59.75h37.688c7.14 0 11.031-3.859 11.031-10.953V10.969C59.75 3.891 55.859 0 48.719 0H11.031C3.906 0 0 3.891 0 10.969v37.828C0 55.891 3.906 59.75 11.031 59.75m.875-8.062c-2.515 0-3.843-1.219-3.843-3.875V11.969c0-2.656 1.328-3.891 3.843-3.891h35.938c2.5 0 3.844 1.235 3.844 3.891v35.844c0 2.656-1.344 3.875-3.844 3.875z" style="fill-rule:nonzero" transform="translate(29 29)scale(.58577)"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.75929,0,0,0.75929,7.36648,3.90301)">
<g transform="matrix(1.31702,0,0,1.31702,-9.70181,-5.14035)">
<path d="M52.109,26.338L52.109,23.46L41.599,23.46C38.671,23.46 37.118,21.926 37.118,18.979L37.118,7.577L27.133,7.577C24.819,7.577 23.676,8.819 23.676,11.014L23.676,15.262C23.543,15.259 23.404,15.257 23.262,15.257L19.009,15.257L19.009,10.724C19.009,5.598 21.658,2.91 26.745,2.91L35.698,2.91C38.44,2.91 40.582,3.621 42.364,5.446L54.244,17.534C56.125,19.463 56.776,21.462 56.776,24.594L56.776,26.338L52.109,26.338ZM41.014,18.39C41.014,19.206 41.352,19.564 42.169,19.564L50.894,19.564L41.014,9.527L41.014,18.39Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.31702,0,0,1.31702,-9.70181,-5.14035)">
<path d="M26.506,61.09L14.451,61.09C9.684,61.09 7.224,58.612 7.224,53.805L7.224,20.228C7.224,15.439 9.702,12.924 14.451,12.924L25.177,12.924C27.953,12.924 29.499,13.362 31.359,15.262L42.265,26.338L36.455,26.338L28.148,17.889L28.148,28.713C27.023,29.971 26.338,31.632 26.338,33.451L26.338,33.667C24.975,32.966 24.302,31.583 24.302,29.488L24.302,17.283L14.817,17.283C12.65,17.283 11.583,18.444 11.583,20.498L11.583,53.534C11.583,55.607 12.65,56.731 14.798,56.731L26.338,56.731L26.338,59.549C26.338,60.078 26.396,60.594 26.506,61.09Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.937843,0,0,0.937843,-0.188198,12.4057)">
<g transform="matrix(1.40431,0,0,1.40431,-10.1441,-18.7089)">
<path d="M26.338,50.099L16.739,50.099C15.678,50.099 14.817,50.961 14.817,52.022C14.817,53.083 15.678,53.944 16.739,53.944L26.338,53.944L26.338,50.099Z"/>
</g>
<g transform="matrix(1.40431,0,0,1.40431,-10.1441,-18.7089)">
<path d="M26.338,43.803L16.739,43.803C15.678,43.803 14.817,44.664 14.817,45.725C14.817,46.786 15.678,47.648 16.739,47.648L26.338,47.648L26.338,43.803Z"/>
</g>
<g transform="matrix(1.40431,0,0,1.40431,-10.1441,-18.7089)">
<path d="M26.338,37.406L16.739,37.406C15.678,37.406 14.817,38.267 14.817,39.328C14.817,40.389 15.678,41.251 16.739,41.251L26.338,41.251L26.338,37.406Z"/>
</g>
<g transform="matrix(1.41967,0,0,1.46137,-30.4087,-21.2388)">
<path d="M35.687,33.303C35.687,32.284 34.835,31.456 33.786,31.456L30.832,31.456C29.783,31.456 28.931,32.284 28.931,33.303C28.931,34.323 29.783,35.151 30.832,35.151L33.786,35.151C34.835,35.151 35.687,34.323 35.687,33.303Z"/>
</g>
</g>
<g transform="matrix(-1.40227,0,0,1.40227,87.814,-8.11344)">
<path d="M25.075,47.705C24.894,47.524 24.249,46.879 24.068,46.698L22.174,44.902L19.928,42.437L19.984,44.818L20.017,47.165C20.049,47.636 19.857,48.13 19.521,48.465C18.846,49.141 17.853,49.159 17.189,48.494C16.805,48.111 16.665,47.693 16.701,47.163L17.051,39.158C17.082,38.499 17.237,38.061 17.597,37.7C17.958,37.34 18.396,37.184 19.055,37.154L27.06,36.804C27.59,36.768 28.008,36.908 28.391,37.291C29.056,37.956 29.048,38.939 28.36,39.627C28.037,39.95 27.538,40.146 27.062,40.12L24.713,40.09L22.334,40.031L24.799,42.278L26.224,43.604L28.169,45.549L29.496,46.974L31.742,49.439L31.683,47.06L31.653,44.711C31.627,44.235 31.823,43.736 32.146,43.413C32.834,42.725 33.817,42.717 34.482,43.382C34.865,43.765 35.005,44.183 34.969,44.713L34.619,52.718C34.589,53.377 34.433,53.815 34.073,54.176C33.712,54.537 33.274,54.691 32.615,54.722L24.61,55.072C24.08,55.108 23.663,54.968 23.279,54.584C22.614,53.92 22.632,52.927 23.308,52.252C23.643,51.916 24.137,51.724 24.608,51.756L26.955,51.789L29.336,51.845L26.871,49.599L25.075,47.705Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.771471,0,0,0.771471,28.4918,33.0532)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M47.11 47.181h-1.17a2.342 2.342 0 0 0 0 4.683h1.17a2.343 2.343 0 0 0 2.341-2.341 2.343 2.343 0 0 0-2.341-2.342m4.74-2.243a3 3 0 0 1-.338.548 2.343 2.343 0 0 0 3.698 2.873q.523-.672.885-1.443a2.342 2.342 0 0 0-4.245-1.978m.244-7.137v1.17a2.344 2.344 0 0 0 2.342 2.34 2.343 2.343 0 0 0 2.34-2.342V37.8a2.341 2.341 0 1 0-4.683.002m-.006-8.195.001 1.17a2.343 2.343 0 0 0 2.343 2.34 2.343 2.343 0 0 0 2.34-2.342v-1.17a2.342 2.342 0 0 0-4.684.002m-.418-7.282c.093.236.161.476.213.736a2.343 2.343 0 0 0 4.59-.932 9 9 0 0 0-.442-1.51 2.342 2.342 0 0 0-4.361 1.706m-32.62-5.133v.008q-.001.078.004.155l.001.03.006.048v.014c.053.584.284 1.005.581 1.31a2.34 2.34 0 0 0 1.749.785s2.341-.162 2.341-2.356V16.03a2.342 2.342 0 0 0-4.683 0zm28.155-.568.81.845a2.343 2.343 0 0 0 3.312.067 2.343 2.343 0 0 0 .067-3.31l-.81-.845a2.342 2.342 0 0 0-3.379 3.243m-5.657-5.908q.406.42.784.821a2.342 2.342 0 0 0 3.405-3.214c-.267-.282-.545-.576-.832-.871a2.343 2.343 0 0 0-3.312-.047 2.343 2.343 0 0 0-.045 3.311M23.83 9.66q.088-.308.243-.593a2.342 2.342 0 1 0-4.105-2.252 7.5 7.5 0 0 0-.643 1.568A2.341 2.341 0 0 0 23.83 9.66M37.143 3h-.874a2.342 2.342 0 0 0 0 4.683h.874q.056 0 .111.004a2.34 2.34 0 1 0 .37-4.667q-.24-.02-.481-.02m-9.07 4.683h1.171a2.343 2.343 0 0 0 2.342-2.342A2.343 2.343 0 0 0 29.244 3h-1.17a2.342 2.342 0 0 0 0 4.683M48.471 19.87h-1.17a2.342 2.342 0 0 0 0 4.684h1.17a2.342 2.342 0 0 0 0-4.683m-6.548-.195a2.4 2.4 0 0 1-.4-.415 2.343 2.343 0 0 0-3.275-.482 2.343 2.343 0 0 0-.482 3.276 7 7 0 0 0 1.22 1.267 2.342 2.342 0 0 0 2.937-3.646m-.553-5.897v-1.171a2.343 2.343 0 0 0-2.342-2.341 2.344 2.344 0 0 0-2.341 2.341v1.17a2.342 2.342 0 0 0 4.683 0"/><path d="M10.149 67.641h33.289c6.699 0 10.148-3.506 10.148-10.231V29.058c0-4.341-.558-6.333-3.278-9.104L33.894 3.284C31.282.616 29.111 0 25.212 0H10.149C3.481 0 0 3.532 0 10.257V57.41c0 6.751 3.455 10.231 10.149 10.231m.488-6.122c-3.016 0-4.515-1.578-4.515-4.49V10.637c0-2.885 1.499-4.515 4.541-4.515h13.321v17.139c0 4.472 2.183 6.634 6.635 6.634h16.845v27.134c0 2.912-1.499 4.49-4.535 4.49zm20.561-37.023c-1.295 0-1.814-.551-1.814-1.84V6.973l17.229 17.523z" style="fill-rule:nonzero" transform="translate(7.224 12.833)scale(.7121)"/><path d="M37.787 51.93c0-1.06-.86-1.921-1.92-1.921h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.862 1.921-1.923m.001-6.297c0-1.06-.86-1.922-1.92-1.922h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.861 1.921-1.923m.001-6.397c0-1.06-.86-1.922-1.92-1.922h-19.13a1.922 1.922 0 0 0 0 3.845h19.128c1.06 0 1.921-.861 1.921-1.923m-16.139-6.473c0-1.06-.861-1.922-1.922-1.922H16.74a1.923 1.923 0 0 0 0 3.845h2.986c1.06 0 1.922-.861 1.922-1.923"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M39.064 13.281a16 16 0 0 0-1.552 2.283 16 16 0 0 0-1.102 2.424H11.319c-2.038 0-3.189 1.143-3.189 3.185V42.78c0 2.014 1.151 3.185 3.189 3.185h17.908c-.335 1.012-.407 2.18-.02 3.274l.532 1.47H10.622C5.731 50.709 3 48.064 3 43.165V20.784c0-4.878 2.731-7.503 7.622-7.503zm21.781 21.78v8.104c0 4.899-2.731 7.544-7.644 7.544h-5.695a4.8 4.8 0 0 0-.008-2.542l-.362-1.277 3.085-.865q.1-.029.197-.06h2.108c2.037 0 3.189-1.171 3.189-3.185v-5.062c1.92-.572 3.643-1.468 5.13-2.657" style="fill-rule:nonzero" transform="translate(-1.5)"/><path d="M18.888 77.914c1.551 1.303 3.64 1.459 5.099.032l9.646-9.657c1.437-1.448 1.375-3.661-.031-5.099l-4.446-4.435 6.674-6.653c1.408-1.406 1.385-3.65-.021-5.099l-6.024-6.046c8.385-4.254 13.079-11.304 13.079-19.577C42.864 9.551 33.292 0 21.443 0 9.51 0 0 9.498 0 21.38c0 8.455 4.793 16.002 12.406 19.504v29.212c0 1.161.372 2.517 1.378 3.399zm2.555-5.961-3.36-3.328V36.464C11.107 34.977 5.994 28.773 5.994 21.38c0-8.491 6.895-15.355 15.449-15.355 8.533 0 15.387 6.864 15.387 15.355 0 7.34-5.135 13.607-12.9 15.254v7.047l5.867 5.918-6.228 6.134v5.982l4.05 3.978zm0-51.321c2.771 0 5.042-2.282 5.042-5.064s-2.271-5.022-5.042-5.022c-2.814 0-5.043 2.231-5.043 5.022 0 2.782 2.25 5.064 5.043 5.064" style="fill-rule:nonzero" transform="rotate(29.234 11.74 88.091)scale(.60562)"/><path d="M34.933 27.241a4.697 4.697 0 0 0 4.694-4.694 4.697 4.697 0 0 0-4.694-4.694 4.7 4.7 0 0 0-4.695 4.694 4.7 4.7 0 0 0 4.695 4.694m-14.642 0c2.565 0 4.669-2.104 4.669-4.694s-2.13-4.694-4.669-4.694a4.697 4.697 0 0 0-4.694 4.694 4.697 4.697 0 0 0 4.694 4.694" style="fill-rule:nonzero" transform="translate(1.5 13.281)scale(.8287)"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-1.5,7.10543e-15)">
<path d="M39.064,13.281C38.491,13.982 37.971,14.744 37.512,15.564C37.071,16.351 36.704,17.161 36.41,17.988L11.319,17.988C9.281,17.988 8.13,19.131 8.13,21.173L8.13,42.78C8.13,44.794 9.281,45.965 11.319,45.965L29.227,45.965C28.892,46.977 28.82,48.145 29.207,49.239L29.739,50.709L10.622,50.709C5.731,50.709 3,48.064 3,43.165L3,20.784C3,15.906 5.731,13.281 10.622,13.281L39.064,13.281ZM60.845,35.061L60.845,43.165C60.845,48.064 58.114,50.709 53.201,50.709L47.506,50.709C47.725,49.911 47.734,49.038 47.498,48.167L47.136,46.89L50.221,46.025C50.287,46.006 50.353,45.986 50.418,45.965L52.526,45.965C54.563,45.965 55.715,44.794 55.715,42.78L55.715,37.718C57.635,37.146 59.358,36.25 60.845,35.061Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.528487,0.29577,-0.29577,0.528487,46.0168,5.48596)">
<path d="M18.888,77.914C20.439,79.217 22.528,79.373 23.987,77.946L33.633,68.289C35.07,66.841 35.008,64.628 33.602,63.19L29.156,58.755L35.83,52.102C37.238,50.696 37.215,48.452 35.809,47.003L29.785,40.957C38.17,36.703 42.864,29.653 42.864,21.38C42.864,9.551 33.292,0 21.443,0C9.51,0 0,9.498 0,21.38C0,29.835 4.793,37.382 12.406,40.884L12.406,70.096C12.406,71.257 12.778,72.613 13.784,73.495L18.888,77.914ZM21.443,71.953L18.083,68.625L18.083,36.464C11.107,34.977 5.994,28.773 5.994,21.38C5.994,12.889 12.889,6.025 21.443,6.025C29.976,6.025 36.83,12.889 36.83,21.38C36.83,28.72 31.695,34.987 23.93,36.634L23.93,43.681L29.797,49.599L23.569,55.733L23.569,61.715L27.619,65.693L21.443,71.953ZM21.443,20.632C24.214,20.632 26.485,18.35 26.485,15.568C26.485,12.787 24.214,10.546 21.443,10.546C18.629,10.546 16.4,12.777 16.4,15.568C16.4,18.35 18.65,20.632 21.443,20.632Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.828691,0,0,0.828691,3,13.2814)">
<path d="M34.933,27.241C37.523,27.241 39.627,25.137 39.627,22.547C39.627,19.957 37.523,17.853 34.933,17.853C32.343,17.853 30.238,19.957 30.238,22.547C30.238,25.137 32.343,27.241 34.933,27.241Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.828691,0,0,0.828691,3,13.2814)">
<path d="M20.291,27.241C22.856,27.241 24.96,25.137 24.96,22.547C24.96,19.957 22.83,17.853 20.291,17.853C17.701,17.853 15.597,19.957 15.597,22.547C15.597,25.137 17.701,27.241 20.291,27.241Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -223,3 +223,7 @@ body {
.objectTableDescritions >.ant-descriptions-view .ant-descriptions-row >.ant-descriptions-item-label {
width: 35%;
}
.farmcontrol-splitter > .ant-splitter-bar {
margin: 8px
}

View File

@ -19,7 +19,7 @@ import FilamentStockDisplay from '../../common/FilamentStockDisplay'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import { LoadingOutlined } from '@ant-design/icons'
import PrinterState from '../../common/PrinterState'
import PrinterState from '../../common/StateDisplay'
const { Title } = Typography

View File

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

View File

@ -0,0 +1,201 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
const log = loglevel.getLogger('DocumentPrinterInfo')
log.setLevel(config.logLevel)
const DocumentPrinterInfo = () => {
const location = useLocation()
const documentPrinterId = new URLSearchParams(location.search).get(
'documentPrinterId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentPrinterInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
return (
<EditObjectForm
id={documentPrinterId}
type='documentPrinter'
style={{ height: '100%' }}
>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => {
// Define actions for ActionHandler
const actions = {
reload: () => {
fetchObject()
return true
},
edit: () => {
startEditing()
return false
},
cancelEdit: () => {
cancelEditing()
return true
},
finishEdit: () => {
handleUpdate()
return true
}
}
return (
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<Flex
gap='large'
vertical='true'
style={{
height: 'calc(var(--unit-100vh) - 155px)',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='documentPrinter'
id={documentPrinterId}
disabled={loading}
/>
<ViewButton
disabled={loading}
items={[
{
key: 'info',
label: 'DocumentPrinter Information'
},
{ key: 'stocks', label: 'DocumentPrinter Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
handleUpdate={() => {
callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Document Printer Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='documentPrinter'
objectData={objectData}
/>
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
collapseKey='notes'
>
<Card>
<NotesPanel
_id={documentPrinterId}
type='documentPrinter'
/>
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': documentPrinterId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
)}
</ActionHandler>
)
}}
</EditObjectForm>
)
}
export default DocumentPrinterInfo

View File

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

View File

@ -0,0 +1,195 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
const log = loglevel.getLogger('DocumentSizeInfo')
log.setLevel(config.logLevel)
const DocumentSizeInfo = () => {
const location = useLocation()
const documentSizeId = new URLSearchParams(location.search).get(
'documentSizeId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentSizeInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
return (
<EditObjectForm
id={documentSizeId}
type='documentSize'
style={{ height: '100%' }}
>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => {
// Define actions for ActionHandler
const actions = {
reload: () => {
fetchObject()
return true
},
edit: () => {
startEditing()
return false
},
cancelEdit: () => {
cancelEditing()
return true
},
finishEdit: () => {
handleUpdate()
return true
}
}
return (
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<Flex
gap='large'
vertical='true'
style={{
height: 'calc(var(--unit-100vh) - 155px)',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='documentSize'
id={documentSizeId}
disabled={loading}
/>
<ViewButton
disabled={loading}
items={[
{ key: 'info', label: 'DocumentSize Information' },
{ key: 'stocks', label: 'DocumentSize Stocks' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
handleUpdate={() => {
callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Document Size Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='documentSize'
objectData={objectData}
/>
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
collapseKey='notes'
>
<Card>
<NotesPanel _id={documentSizeId} type='documentSize' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': documentSizeId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
)}
</ActionHandler>
)
}}
</EditObjectForm>
)
}
export default DocumentSizeInfo

View File

@ -0,0 +1,103 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { useMediaQuery } from 'react-responsive'
import { Typography, Flex, Steps, Divider } from 'antd'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
const NewDocumentSize = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return (
<NewObjectForm type={'documentSize'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='documentSize'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='documentSize'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<Flex gap='middle'>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && (
<Divider type='vertical' style={{ height: 'unset' }} />
)}
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Title level={2} style={{ margin: 0 }}>
New Document Size
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
)
}}
</NewObjectForm>
)
}
NewDocumentSize.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewDocumentSize

View File

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

View File

@ -0,0 +1,171 @@
import React, { useState, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import TemplateEditor from '../../common/TemplateEditor.jsx'
const log = loglevel.getLogger('DocumentTemplateDesign')
log.setLevel(config.logLevel)
const DocumentTemplateDesign = () => {
const location = useLocation()
const editFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const documentTemplateId = new URLSearchParams(location.search).get(
'documentTemplateId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentTemplateDesign',
{
preview: true,
editor: true,
notes: true,
auditLogs: true
}
)
const [editFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false
})
const actions = {
reload: () => {
editFormRef?.current.handleFetchObject()
return true
},
edit: () => {
editFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
editFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
editFormRef?.current.handleUpdate()
return true
}
}
return (
<Flex
gap='large'
vertical='true'
style={{
height: 'calc(var(--unit-100vh) - 155px)',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='documentTemplate'
id={documentTemplateId}
disabled={editFormState.loading}
visibleActions={{ edit: false }}
/>
<ViewButton
disabled={editFormState.loading}
items={[
{
key: 'preview',
label: 'Preview'
},
{
key: 'editor',
label: 'Editor'
},
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={editFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={editFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={editFormState.editLoading}
formValid={editFormState.formValid}
disabled={editFormState.lock?.locked || editFormState.loading}
loading={editFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={editFormState.loading}
ref={actionHandlerRef}
>
<EditObjectForm
id={documentTemplateId}
type='documentTemplate'
style={{ height: '100%' }}
ref={editFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<TemplateEditor
objectData={objectData}
loading={loading}
style={{ height: '72vh' }}
collapseState={collapseState}
isEditing={isEditing}
/>
)
}}
</EditObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={documentTemplateId} type='documentTemplate' />
</Card>
</InfoCollapse>
</Flex>
</div>
</Flex>
)
}
export default DocumentTemplateDesign

View File

@ -0,0 +1,202 @@
import React, { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
const log = loglevel.getLogger('DocumentTemplateInfo')
log.setLevel(config.logLevel)
const DocumentTemplateInfo = () => {
const location = useLocation()
const editFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const documentTemplateId = new URLSearchParams(location.search).get(
'documentTemplateId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentTemplateInfo',
{
info: true,
stocks: true,
notes: true,
auditLogs: true
}
)
const [editFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false
})
const actions = {
reload: () => {
editFormRef?.current.handleFetchObject()
return true
},
edit: () => {
editFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
editFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
editFormRef?.current.handleUpdate()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{
height: 'calc(var(--unit-100vh) - 155px)',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='documentTemplate'
id={documentTemplateId}
disabled={editFormState.loading}
/>
<ViewButton
disabled={editFormState.loading}
items={[
{ key: 'info', label: 'DocumentTemplate Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={editFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={editFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={editFormState.editLoading}
formValid={editFormState.formValid}
disabled={editFormState.lock?.locked || editFormState.loading}
loading={editFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={editFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='DocumentTemplate Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<EditObjectForm
id={documentTemplateId}
type='documentTemplate'
style={{ height: '100%' }}
ref={editFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='documentTemplate'
objectData={objectData}
visibleProperties={{
content: false,
testObject: false
}}
/>
)
}}
</EditObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={documentTemplateId} type='documentTemplate' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{editFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': documentTemplateId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
</>
)
}
export default DocumentTemplateInfo

View File

@ -0,0 +1,121 @@
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { useMediaQuery } from 'react-responsive'
import { Typography, Flex, Steps, Divider } from 'antd'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
const NewDocumentTemplate = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return (
<NewObjectForm
type={'documentTemplate'}
defaultValues={{ active: true, global: false }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='documentTemplate'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='documentTemplate'
column={1}
bordered={false}
isEditing={true}
required={false}
visibleProperties={{ content: false, testObject: false }}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='documentTemplate'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<Flex gap='middle'>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && (
<Divider type='vertical' style={{ height: 'unset' }} />
)}
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Title level={2} style={{ margin: 0 }}>
New Document Template
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
)
}}
</NewObjectForm>
)
}
NewDocumentTemplate.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewDocumentTemplate

View File

@ -1,6 +1,6 @@
import React from 'react'
import React, { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
@ -19,12 +19,15 @@ import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import HostOTP from './HostOtp.jsx'
const log = loglevel.getLogger('HostInfo')
log.setLevel(config.logLevel)
const HostInfo = () => {
const location = useLocation()
const editFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const hostId = new URLSearchParams(location.search).get('hostId')
const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
info: true,
@ -33,43 +36,40 @@ const HostInfo = () => {
auditLogs: true
})
return (
<EditObjectForm id={hostId} type='host' style={{ height: '100%' }}>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => {
// Define actions for ActionHandler
const [hostOTPOpen, setHostOTPOpen] = useState(false)
const [editFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false
})
const actions = {
reload: () => {
fetchObject()
editFormRef?.current.handleFetchObject()
return true
},
hostOTP: () => {
setHostOTPOpen(true)
return true
},
edit: () => {
startEditing()
editFormRef?.current.startEditing()
return false
},
cancelEdit: () => {
cancelEditing()
editFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
handleUpdate()
editFormRef?.current.handleUpdate()
return true
}
}
return (
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<>
<Flex
gap='large'
vertical='true'
@ -84,10 +84,10 @@ const HostInfo = () => {
<ObjectActions
type='host'
id={hostId}
disabled={loading}
disabled={editFormState.loading}
/>
<ViewButton
disabled={loading}
disabled={editFormState.loading}
items={[
{ key: 'info', label: 'Host Information' },
{ key: 'notes', label: 'Notes' },
@ -97,39 +97,54 @@ const HostInfo = () => {
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={lock} />
<LockIndicator lock={editFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
isEditing={editFormState.isEditing}
handleUpdate={() => {
callAction('finishEdit')
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
actionHandlerRef.current.callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
editLoading={editFormState.editLoading}
formValid={editFormState.formValid}
disabled={editFormState.lock?.locked || editFormState.loading}
loading={editFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={editFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Host Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<EditObjectForm
id={hostId}
type='host'
style={{ height: '100%' }}
ref={editFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
@ -137,15 +152,17 @@ const HostInfo = () => {
type='host'
objectData={objectData}
/>
)
}}
</EditObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
@ -162,7 +179,7 @@ const HostInfo = () => {
}
collapseKey='auditLogs'
>
{loading ? (
{editFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
@ -175,11 +192,19 @@ const HostInfo = () => {
</Flex>
</div>
</Flex>
)}
</ActionHandler>
)
<Modal
open={hostOTPOpen}
destroyOnHidden={true}
width={650}
onCancel={() => {
setHostOTPOpen(false)
}}
</EditObjectForm>
footer={false}
>
<HostOTP id={hostId} />
</Modal>
</>
)
}

View File

@ -0,0 +1,138 @@
import PropTypes from 'prop-types'
import React, { useContext, useEffect, useState, useRef } from 'react'
import { Input, Result, Typography, Flex, Progress, Button } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { ApiServerContext } from '../../context/ApiServerContext'
import CopyButton from '../../common/CopyButton'
import OTPIcon from '../../../Icons/OTPIcon'
const { Text } = Typography
const HostOTP = ({ id }) => {
const { fetchHostOTP, sendObjectAction } = useContext(ApiServerContext)
const [hostObject, setHostObject] = useState(null)
const [loading, setLoading] = useState(true)
const [timeRemaining, setTimeRemaining] = useState(0)
const [totalTime, setTotalTime] = useState(0)
const [initialized, setInitialized] = useState(false)
const intervalRef = useRef(null)
const fetchNewOTP = () => {
setLoading(true)
setHostObject(null) // Reset to show loading
fetchHostOTP(id, (hostOTPObject) => {
setHostObject(hostOTPObject)
setLoading(false)
if (hostOTPObject?.otpExpiresAt) {
const now = Date.now()
const expiresAt = new Date(hostOTPObject.otpExpiresAt).getTime()
const remaining = Math.max(0, expiresAt - now)
setTimeRemaining(remaining)
setTotalTime(remaining)
}
})
}
const sendTestAction = () => {
console.log('Sending test action...')
sendObjectAction(id, 'host', { method: 'testMethod' }, (result) => {
console.log('Got callback', result)
})
}
useEffect(() => {
if (hostObject === null && initialized == false) {
setInitialized(true)
fetchNewOTP()
}
}, [id])
useEffect(() => {
if (hostObject && timeRemaining > 0) {
intervalRef.current = setInterval(() => {
setTimeRemaining((prev) => {
const newTime = prev - 1000
if (newTime <= 0) {
// OTP expired, fetch a new one
fetchNewOTP()
return 0
}
return newTime
})
}, 1000)
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}
}, [hostObject, timeRemaining])
// Clean up interval on unmount
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [])
const progressPercent =
totalTime > 0 ? Math.max(0, (timeRemaining / totalTime) * 100) : 0
return (
<Flex vertical align='center'>
<Button onClick={sendTestAction}>Test Action</Button>
<Result
title={'Connect a Host.'}
subTitle={<Text>Enter the following one time passcode.</Text>}
icon={<OTPIcon />}
>
<Flex justify='center'>
<Flex gap={'small'} align='center' justify='center'>
<CopyButton
size='default'
text={hostObject?.otp}
disabled={loading}
/>
<div>
<Input.OTP
disabled={loading}
size='large'
value={hostObject?.otp}
onChange={(e) => e.preventDefault()} // prevent typing
onKeyDown={(e) => e.preventDefault()} // prevent key input
onPaste={(e) => e.preventDefault()} // prevent pasting
/>
</div>
<div style={{ margin: '0 6px 0 8px', paddingBottom: '5px' }}>
{loading ? (
<Text>
<LoadingOutlined />
</Text>
) : (
<Progress
type='circle'
showInfo={false}
strokeColor='#32D74B'
size={14}
strokeWidth={14}
percent={progressPercent}
/>
)}
</div>
</Flex>
</Flex>
</Result>
</Flex>
)
}
HostOTP.propTypes = {
id: PropTypes.string.isRequired
}
export default HostOTP

View File

@ -12,6 +12,10 @@ import AuditLogIcon from '../../Icons/AuditLogIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import PersonIcon from '../../Icons/PersonIcon'
import HostIcon from '../../Icons/HostIcon'
import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon'
import DocumentTemplateIcon from '../../Icons/DocumentTemplateIcon'
import DocumentIcon from '../../Icons/DocumentIcon'
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
const items = [
{
@ -46,11 +50,37 @@ const items = [
},
{ type: 'divider' },
{
key: 'notetypes',
key: 'noteTypes',
icon: <NoteTypeIcon />,
label: 'Note Types',
path: '/dashboard/management/notetypes'
},
{
key: 'documents',
icon: <DocumentIcon />,
label: 'Documents',
children: [
{
key: 'documentPrinters',
icon: <DocumentPrinterIcon />,
label: 'Document Printers',
path: '/dashboard/management/documentprinters'
},
{
key: 'documentTemplates',
icon: <DocumentTemplateIcon />,
label: 'Document Templates',
path: '/dashboard/management/documenttemplates'
},
{
key: 'documentSizes',
icon: <DocumentSizeIcon />,
label: 'Document Sizes',
path: '/dashboard/management/documentsizes'
}
]
},
{
key: 'hosts',
icon: <HostIcon />,
@ -70,7 +100,7 @@ const items = [
path: '/dashboard/management/settings'
},
{
key: 'auditlogs',
key: 'auditLogs',
icon: <AuditLogIcon />,
label: 'Audit Logs',
path: '/dashboard/management/auditlogs'
@ -96,10 +126,13 @@ const routeKeyMap = {
'/dashboard/management/products': 'products',
'/dashboard/management/vendors': 'vendors',
'/dashboard/management/materials': 'materials',
'/dashboard/management/notetypes': 'notetypes',
'/dashboard/management/notetypes': 'noteTypes',
'/dashboard/management/settings': 'settings',
'/dashboard/management/auditlogs': 'auditlogs',
'/dashboard/management/hosts': 'hosts'
'/dashboard/management/auditlogs': 'auditLogs',
'/dashboard/management/hosts': 'hosts',
'/dashboard/management/documentsizes': 'documentSizes',
'/dashboard/management/documentprinters': 'documentPrinters',
'/dashboard/management/documenttemplates': 'documentTemplates'
}
const ManagementSidebar = (props) => {

View File

@ -28,7 +28,7 @@ import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import PrinterPositionPanel from '../../common/PrinterPositionPanel'
import PrinterMovementPanel from '../../common/PrinterMovementPanel'
import PrinterMiscPanel from '../../common/PrinterMiscPanel'
import PrinterState from '../../common/PrinterState'
import PrinterState from '../../common/StateDisplay'
import { AuthContext } from '../../context/AuthContext'
import PrinterSubJobsTree from '../../common/PrinterJobsTree'
import IdDisplay from '../../common/IdDisplay'

View File

@ -1,81 +1,77 @@
import React from 'react'
import React, { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../common/LockIndicator.jsx'
import PrinterJobsTree from '../../common/PrinterJobsTree'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ActionHandler from '../../common/ActionHandler'
import EditObjectForm from '../../common/EditObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
const log = loglevel.getLogger('PrinterInfo')
log.setLevel(config.logLevel)
const PrinterInfo = () => {
const location = useLocation()
const editFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const printerId = new URLSearchParams(location.search).get('printerId')
const [collapseState, updateCollapseState] = useCollapseState('PrinterInfo', {
info: true,
jobs: true,
stocks: true,
notes: true,
auditLogsParent: true,
auditLogsOwner: true
auditLogs: true
})
const [editFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false
})
return (
<EditObjectForm
id={printerId}
type='printer'
style={{ height: 'calc(var(--unit-100vh) - 155px)', minHeight: 0 }}
>
{({
loading,
isEditing,
startEditing,
cancelEditing,
handleUpdate,
formValid,
objectData,
editLoading,
lock,
fetchObject
}) => {
// Define actions for ActionHandler
const actions = {
reload: () => {
fetchObject()
editFormRef?.current.handleFetchObject()
return true
},
edit: () => {
startEditing()
editFormRef?.current.startEditing()
console.log('CALLING START EDITING')
return false
},
cancelEdit: () => {
cancelEditing()
editFormRef?.current.cancelEditing()
return true
},
finishEdit: () => {
handleUpdate()
editFormRef?.current.handleUpdate()
return true
}
}
return (
<ActionHandler actions={actions} loading={loading}>
{({ callAction }) => (
<>
<Flex
gap='large'
vertical='true'
style={{ height: '100%', minHeight: 0 }}
style={{
height: 'calc(var(--unit-100vh) - 155px)',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
@ -83,60 +79,67 @@ const PrinterInfo = () => {
<ObjectActions
type='printer'
id={printerId}
disabled={loading}
disabled={editFormState.loading}
/>
<ViewButton
disabled={loading}
disabled={editFormState.loading}
items={[
{ key: 'info', label: 'Printer Information' },
{ key: 'jobs', label: 'Printer Jobs' },
{ key: 'notes', label: 'Notes' },
{
key: 'auditLogsParent',
label: 'Audit Logs (By Parent)'
},
{
key: 'auditLogsOwner',
label: 'Audit Logs (By Owner)'
}
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
</Space>
<LockIndicator lock={lock} />
<LockIndicator lock={editFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={isEditing}
isEditing={editFormState.isEditing}
handleUpdate={() => {
callAction('finishEdit')
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
callAction('cancelEdit')
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
callAction('edit')
actionHandlerRef.current.callAction('edit')
}}
editLoading={editLoading}
formValid={formValid}
disabled={lock?.locked || loading}
loading={editLoading}
editLoading={editFormState.editLoading}
formValid={editFormState.formValid}
disabled={editFormState.lock?.locked || editFormState.loading}
loading={editFormState.editLoading}
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={editFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Printer Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<EditObjectForm
id={printerId}
type='printer'
style={{ height: '100%' }}
ref={editFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
@ -144,30 +147,17 @@ const PrinterInfo = () => {
type='printer'
objectData={objectData}
/>
)
}}
</EditObjectForm>
</InfoCollapse>
<InfoCollapse
title='Printer Jobs'
icon={<PrinterIcon />}
active={collapseState.jobs}
onToggle={(expanded) =>
updateCollapseState('jobs', expanded)
}
collapseKey='jobs'
>
<PrinterJobsTree
subJobs={objectData?.subJobs}
loading={loading}
/>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) =>
updateCollapseState('notes', expanded)
}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
@ -176,15 +166,15 @@ const PrinterInfo = () => {
</InfoCollapse>
<InfoCollapse
title='Audit Logs (By Parent)'
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogsParent}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogsParent', expanded)
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{loading ? (
{editFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
@ -194,33 +184,10 @@ const PrinterInfo = () => {
/>
)}
</InfoCollapse>
<InfoCollapse
title='Audit Logs (By Owner)'
icon={<AuditLogIcon />}
active={collapseState.auditLogsOwner}
onToggle={(expanded) =>
updateCollapseState('auditLogsOwner', expanded)
}
collapseKey='auditLogs'
>
{loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'owner._id': printerId }}
visibleColumns={{ _id: false, 'owner._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</div>
</Flex>
)}
</ActionHandler>
)
}}
</EditObjectForm>
</>
)
}

View File

@ -1,15 +1,19 @@
import React, { useEffect, useRef } from 'react'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
const ActionHandler = ({
const ActionHandler = forwardRef(
(
{
children,
actions = {},
actionParam = 'action',
clearAfterExecute = true,
onAction,
loading = true
}) => {
},
ref
) => {
const location = useLocation()
const navigate = useNavigate()
const action = new URLSearchParams(location.search).get(actionParam)
@ -36,6 +40,7 @@ const ActionHandler = ({
) {
// Execute the action
const result = actions[action]()
// Mark this action as executed
lastExecutedAction.current = action
// Call optional callback
@ -66,9 +71,16 @@ const ActionHandler = ({
navigate
])
useImperativeHandle(ref, () => ({
callAction
}))
// Return null as this is a utility component
return <>{children({ callAction })}</>
return children
}
)
ActionHandler.displayName = 'ActionHandler'
ActionHandler.propTypes = {
children: PropTypes.func,

View File

@ -1,213 +0,0 @@
import React, { forwardRef, useState } from 'react'
import { Typography, Space, Descriptions, Badge, Table } from 'antd'
import PropTypes from 'prop-types'
import IdDisplay from './IdDisplay'
import { AuditOutlined, LoadingOutlined } from '@ant-design/icons'
import TimeDisplay from '../common/TimeDisplay'
import BoolDisplay from './BoolDisplay'
import StateTag from './StateTag'
const { Text } = Typography
const formatPropertyName = (name) => {
return name
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase())
}
const isObjectId = (value) => {
return typeof value === 'string' && /^[0-9a-fA-F]{24}$/.test(value)
}
const formatValue = (value, propertyName) => {
if (value === null || value === undefined || value === '') {
return (
<div style={{ maxWidth: 20 }}>
<Text type='secondary'>n/a</Text>
</div>
)
}
// Handle colors specifically
if (propertyName === 'color') {
return <Badge color={value} text={value} />
}
if (propertyName === 'state' && typeof value === 'object' && value.type) {
return <StateTag state={value.type} />
}
// Check if the value is a timestamp (ISO date string)
if (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
) {
return <TimeDisplay dateTime={value} />
}
if (typeof value === 'boolean' || value === true || value === false) {
return <BoolDisplay value={value} yesNo={true} />
}
if (isObjectId(value)) {
return (
<IdDisplay
id={value}
type={propertyName.toLowerCase().replaceAll('current', '')}
longId={false}
showHyperlink={true}
/>
)
}
if (typeof value === 'object') {
return <Text>{JSON.stringify(value)}</Text>
}
return <Text>{value}</Text>
}
const AuditLogTable = forwardRef(
(
{ items, loading = false, showTargetColumn = true, showOwnerColumn = true },
ref
) => {
const [sortedInfo, setSortedInfo] = useState({
columnKey: 'createdAt',
order: 'descend'
})
const handleChange = (pagination, filters, sorter) => {
setSortedInfo(sorter)
}
const columns = [
{
title: '',
key: 'icon',
width: 50,
render: () => <AuditOutlined />
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'auditlog'} longId={false} />
),
sorter: (a, b) => a._id.localeCompare(b._id)
}
]
if (showOwnerColumn) {
columns.push(
{
title: 'Owner Name',
dataIndex: ['owner', 'name'],
key: 'name',
width: 200,
sorter: (a, b) =>
(a.owner?.name || '').localeCompare(b.owner?.name || '')
},
{
title: 'Owner',
key: 'owner',
width: 180,
render: (record) => (
<IdDisplay
id={record.owner._id}
type={record.ownerModel.toLowerCase()}
longId={false}
showHyperlink={true}
/>
)
}
)
}
if (showTargetColumn) {
columns.push({
title: 'Target',
key: 'target',
width: 180,
render: (record) => (
<IdDisplay
id={record.target}
type={record.targetModel.toLowerCase()}
longId={false}
showHyperlink={true}
/>
)
})
}
columns.push({
title: 'Properties',
dataIndex: 'type',
key: 'type',
width: 550,
render: (_, record) => {
const oldValue = record.oldValue || {}
const newValue = record.newValue || {}
return (
<Descriptions size='small' column={1}>
{Object.keys(newValue).map((key) => (
<Descriptions.Item key={key} label={formatPropertyName(key)}>
<Space>
{formatValue(oldValue[key], key)}
<Text type='secondary'></Text>
{formatValue(newValue[key], key)}
</Space>
</Descriptions.Item>
))}
</Descriptions>
)
}
})
columns.push({
title: 'Timestamp',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
defaultSortOrder: 'descend',
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
}
})
return (
<Table
ref={ref}
columns={columns}
dataSource={items}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
pagination={false}
scroll={{ x: 'max-content' }}
onChange={handleChange}
sortDirections={['ascend', 'descend']}
sortOrder={
sortedInfo.columnKey === 'createdAt' ? sortedInfo.order : null
}
/>
)
}
)
AuditLogTable.displayName = 'AuditLogTable'
AuditLogTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
loading: PropTypes.bool,
showTargetColumn: PropTypes.bool,
showOwnerColumn: PropTypes.bool
}
export default AuditLogTable

View File

@ -0,0 +1,192 @@
import React, { useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import CodeMirror from '@uiw/react-codemirror'
import { javascript } from '@codemirror/lang-javascript'
import { python } from '@codemirror/lang-python'
import { json } from '@codemirror/lang-json'
import { css } from '@codemirror/lang-css'
import { html } from '@codemirror/lang-html'
import { markdown } from '@codemirror/lang-markdown'
import { sql } from '@codemirror/lang-sql'
import { java } from '@codemirror/lang-java'
import { cpp } from '@codemirror/lang-cpp'
import { rust } from '@codemirror/lang-rust'
import { go } from '@codemirror/lang-go'
import { php } from '@codemirror/lang-php'
import { yaml } from '@codemirror/lang-yaml'
import { xml } from '@codemirror/lang-xml'
import { oneDark } from '@codemirror/theme-one-dark'
import { useThemeContext } from '../context/ThemeContext'
import { Button, Modal, Typography } from 'antd'
const { Link } = Typography
export default function CodeBlockEditor({
code = '',
language = 'javascript',
style = {},
className = '',
readOnly = false,
onChange = null,
height = 'auto',
showLineNumbers = true,
disabled = false,
minimal = false
}) {
const { isDarkMode } = useThemeContext()
const [codeMirrorOpen, setCodeMirrorOpen] = useState(false)
var editorCode = code
if (typeof code == 'object' && language == 'json') {
editorCode = JSON.stringify(code, null, 2)
}
console.log(editorCode)
// Map language to CodeMirror extension
const languageExtension = useMemo(() => {
switch (language.toLowerCase()) {
case 'javascript':
case 'js':
return javascript()
case 'python':
case 'py':
return python()
case 'json':
return json()
case 'xml':
return xml()
case 'ejs':
return xml()
case 'html':
return html()
case 'css':
return css()
case 'markdown':
case 'md':
return markdown()
case 'sql':
return sql()
case 'java':
return java()
case 'cpp':
case 'c++':
case 'c':
return cpp()
case 'rust':
case 'rs':
return rust()
case 'go':
return go()
case 'php':
return php()
case 'yaml':
case 'yml':
return yaml()
default:
return javascript() // Default fallback
}
}, [language])
const handleOnChange = (value) => {
if (typeof code == 'object' && language == 'json') {
console.log('Parsing object', JSON.parse(value))
onChange(JSON.parse(value))
} else {
onChange(value)
}
}
const codeMirror = (
<div
style={{
border: '1px solid #85858541',
...style
}}
className={className}
>
<CodeMirror
value={editorCode}
height={height}
theme={isDarkMode ? oneDark : 'light'}
extensions={[languageExtension]}
readOnly={readOnly || disabled}
onChange={handleOnChange}
basicSetup={{
lineNumbers: showLineNumbers,
foldGutter: true,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
syntaxHighlighting: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: !readOnly,
rectangularSelection: false,
crosshairCursor: false,
highlightActiveLine: !readOnly,
highlightSelectionMatches: false,
closeBracketsKeymap: false,
searchKeymap: false,
foldKeymap: false,
completionKeymap: !readOnly,
lintKeymap: false
}}
style={{
fontSize: '14px',
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace'
}}
/>
</div>
)
return (
<>
{minimal ? (
<div style={{ maxWidth: '300px' }}>
<Link
onClick={() => {
setCodeMirrorOpen(true)
}}
>
<pre>{editorCode.slice(-64).trim()}...</pre>
</Link>
<Modal
closeIcon={false}
open={codeMirrorOpen}
width={800}
footer={
<Button
onClick={() => {
setCodeMirrorOpen(false)
}}
>
Close
</Button>
}
>
{codeMirror}
</Modal>
</div>
) : (
codeMirror
)}
</>
)
}
CodeBlockEditor.propTypes = {
code: PropTypes.string.isRequired,
language: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string)
]),
style: PropTypes.object,
className: PropTypes.string,
readOnly: PropTypes.bool,
onChange: PropTypes.func,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
showLineNumbers: PropTypes.bool,
disabled: PropTypes.bool,
minimal: PropTypes.bool
}

View File

@ -3,7 +3,12 @@ import PropTypes from 'prop-types'
import { Button, Tooltip, message } from 'antd'
import CopyIcon from '../../Icons/CopyIcon'
const CopyButton = ({ text, tooltip = 'Copy', size = 'small' }) => {
const CopyButton = ({
text,
tooltip = 'Copy',
size = 'small',
type = 'text'
}) => {
const [messageApi, contextHolder] = message.useMessage()
const doCopy = (copyText) => {
@ -49,7 +54,7 @@ const CopyButton = ({ text, tooltip = 'Copy', size = 'small' }) => {
style={{ minWidth: 25 }}
width={20}
size={size}
type={'text'}
type={type}
onClick={() => doCopy(text)}
/>
</Tooltip>
@ -61,7 +66,8 @@ CopyButton.propTypes = {
text: PropTypes.string.isRequired,
style: PropTypes.object,
tooltip: PropTypes.string,
size: PropTypes.string
size: PropTypes.string,
type: PropTypes.string
}
export default CopyButton

View File

@ -4,6 +4,7 @@ import { Breadcrumb, Button, Flex, Space } from 'antd'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
import { getModelByName } from '../../../database/ObjectModels'
const breadcrumbNameMap = {
production: 'Production',
@ -11,30 +12,8 @@ const breadcrumbNameMap = {
management: 'Management',
developer: 'Developer',
overview: 'Overview',
printers: 'Printers',
hosts: 'Hosts',
control: 'Control',
info: 'Info',
jobs: 'Jobs',
subjobs: 'Sub Jobs',
gcodefiles: 'G Code Files',
filaments: 'Filaments',
parts: 'Parts',
products: 'Products',
vendors: 'Vendors',
materials: 'Materials',
notetypes: 'Note Types',
users: 'Users',
settings: 'Settings',
auditlogs: 'Audit Logs',
filamentstocks: 'Filament Stocks',
partstocks: 'Part Stocks',
productstocks: 'Products',
stockevents: 'Stock Events',
stockaudits: 'Stock Audits',
sessionstorage: 'Session Storage',
authcontextdebug: 'Auth Context Debug',
printservercontextdebug: 'Print Server Context Debug'
design: 'Design'
}
const mainSections = ['production', 'inventory', 'management', 'developer']
@ -44,15 +23,39 @@ const DashboardBreadcrumb = () => {
const navigate = useNavigate()
const pathSnippets = location.pathname.split('/').filter((i) => i)
function segmentToModel(segment) {
if (segment) {
// If segment ends with 's', remove it and get the model
if (segment.endsWith('s')) {
const singularSegment = segment.slice(0, -1)
return getModelByName(singularSegment, true)
}
// Otherwise, get the model as is
return getModelByName(segment, true)
}
return null
}
const breadcrumbItems = pathSnippets.map((segment, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
if (segment !== 'dashboard') {
const isMainSection = mainSections.includes(segment)
const name = breadcrumbNameMap[segment] || segment
const model = segmentToModel(segment)
const modelLabelPlural = model?.label ? `${model.label}s` : null
const name = breadcrumbNameMap[segment] || modelLabelPlural || segment
if (isMainSection) {
return {
title: isMainSection ? (
<span style={{ padding: '0 12px' }}>{name}</span>
) : (
title: (
<span style={{ padding: '0 12px' }} key={segment}>
{name}
</span>
),
key: segment
}
}
return {
title: (
<Link to={url} style={{ padding: '0 12px' }}>
{name}
</Link>

View File

@ -18,7 +18,6 @@ import {
LoadingOutlined
} from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext'
import { PrintServerContext } from '../context/PrintServerContext'
import { SpotlightContext } from '../context/SpotlightContext'
import { ApiServerContext } from '../context/ApiServerContext'
import { useNavigate, useLocation } from 'react-router-dom'
@ -36,7 +35,6 @@ import BellIcon from '../../Icons/BellIcon'
import SearchIcon from '../../Icons/SearchIcon'
import SettingsIcon from '../../Icons/SettingsIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import PrinterIcon from '../../Icons/PrinterIcon'
import { ElectronContext } from '../context/ElectronContext'
import DashboardWindowButtons from './DashboardWindowButtons'
@ -44,9 +42,7 @@ const DashboardNavigation = () => {
const { logout, userProfile } = useContext(AuthContext)
const { showSpotlight } = useContext(SpotlightContext)
const { toggleNotificationCenter, unreadCount } = useContext(ApiServerContext)
const { printServer } = useContext(PrintServerContext)
const { apiServer } = useContext(ApiServerContext)
const [printServerState, setPrintServerState] = useState('disconnected')
const { connecting, connected } = useContext(ApiServerContext)
const [apiServerState, setApiServerState] = useState('disconnected')
const navigate = useNavigate()
const location = useLocation()
@ -62,24 +58,15 @@ const DashboardNavigation = () => {
}, [location.pathname])
useEffect(() => {
if (printServer?.connecting) {
setPrintServerState('connecting')
} else if (printServer?.connected) {
setPrintServerState('connected')
} else {
setPrintServerState('disconnected')
}
}, [printServer?.connecting, printServer?.connected])
useEffect(() => {
if (apiServer?.connecting) {
if (connecting == true) {
setApiServerState('connecting')
} else if (apiServer?.connected) {
} else if (connected == true) {
setApiServerState('connected')
} else {
setApiServerState('disconnected')
}
}, [apiServer?.connecting, apiServer?.connected])
console.log('Connecting/connected', connecting, connected)
}, [connecting, connected])
const mainMenuItems = [
{
@ -191,33 +178,6 @@ const DashboardNavigation = () => {
{process.env.NODE_ENV === 'development' && (
<Space>
{printServerState === 'connected' ? (
<Tooltip title='Connected to print server' arrow={false}>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<PrinterIcon />}
/>
</Tooltip>
) : null}
{printServerState === 'connecting' ? (
<Tooltip title='Connecting to print erver...' arrow={false}>
<Tag
color='default'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{printServerState === 'disconnected' ? (
<Tooltip title='Disconnected from print server' arrow={false}>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<PrinterIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connected' ? (
<Tooltip title='Connected to api server' arrow={false}>
<Tag
@ -230,7 +190,7 @@ const DashboardNavigation = () => {
{apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tag
color='default'
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>

View File

@ -38,18 +38,35 @@ const DashboardSidebar = ({
if (onCollapse) onCollapse(newCollapsed)
}
// Add onClick to each item
const _items = items.map((item) => {
if (item?.type == 'divider') {
// Recursive function to map items and their children
const mapItemsRecursively = (items) => {
return items.map((item) => {
if (item?.type === 'divider') {
return item
}
return {
const mappedItem = {
key: item.key,
icon: item.icon,
label: item.label,
onClick: () => navigate(item.path)
label: item.label
}
// Add onClick for items with paths (leaf nodes)
if (item.path) {
mappedItem.onClick = () => navigate(item.path)
}
// Recursively map children if they exist
if (item?.children && Array.isArray(item.children)) {
mappedItem.children = mapItemsRecursively(item.children)
}
return mappedItem
})
}
// Map items recursively
const _items = mapItemsRecursively(items)
if (isMobile) {
return (

View File

@ -1,4 +1,11 @@
import React, { useState, useEffect, useContext, useCallback } from 'react'
import React, {
useState,
useEffect,
useContext,
useCallback,
forwardRef,
useImperativeHandle
} from 'react'
import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
@ -18,7 +25,8 @@ import merge from 'lodash/merge'
* loading, isEditing, startEditing, cancelEditing, handleUpdate, form, formValid, objectData, setIsEditing, setObjectData
* }) => ReactNode
*/
const EditObjectForm = ({ id, type, style, children }) => {
const EditObjectForm = forwardRef(
({ id, type, style, children, onEdit, onStateChange }, ref) => {
const [objectData, setObjectData] = useState(null)
const [serverObjectData, setServerObjectData] = useState(null)
const [fetchLoading, setFetchLoading] = useState(true)
@ -41,16 +49,22 @@ const EditObjectForm = ({ id, type, style, children }) => {
fetchObjectLock,
showError,
connected,
subscribeToObject,
subscribeToLock
subscribeToObjectUpdates,
subscribeToObjectLock
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// Validate form on change
useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => setFormValid(true))
.catch(() => setFormValid(false))
.then(() => {
setFormValid(true)
onStateChange({ formValid: true })
})
.catch(() => {
setFormValid(false)
onStateChange({ formValid: true })
})
}, [form, formUpdateValues])
// Cleanup on unmount
@ -65,13 +79,16 @@ const EditObjectForm = ({ id, type, style, children }) => {
const handleFetchObject = useCallback(async () => {
try {
setFetchLoading(true)
onStateChange({ loading: true })
const data = await fetchObject(id, type)
const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent)
onStateChange({ lock: lockEvent })
setObjectData(data)
setServerObjectData(data)
form.setFieldsValue(data)
setFetchLoading(false)
onStateChange({ loading: false })
} catch (err) {
messageApi.error('Failed to fetch object info')
showError(
@ -88,7 +105,10 @@ const EditObjectForm = ({ id, type, style, children }) => {
// Update event handler
const updateLockEventHandler = useCallback((value) => {
setLock((prev) => ({ ...prev, ...value }))
setLock((prev) => {
onStateChange({ lock: { ...prev, ...value } })
return { ...prev, ...value }
})
}, [])
useEffect(() => {
@ -100,22 +120,26 @@ const EditObjectForm = ({ id, type, style, children }) => {
useEffect(() => {
if (id && connected) {
const objectUnsubscribe = subscribeToObject(
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
id,
type,
updateObjectEventHandler
)
const lockUnsubscribe = subscribeToLock(id, type, updateLockEventHandler)
const lockUnsubscribe = subscribeToObjectLock(
id,
type,
updateLockEventHandler
)
return () => {
if (objectUnsubscribe) objectUnsubscribe()
if (objectUpdatesUnsubscribe) objectUpdatesUnsubscribe()
if (lockUnsubscribe) lockUnsubscribe()
}
}
}, [
id,
type,
subscribeToObject,
subscribeToLock,
subscribeToObjectUpdates,
subscribeToObjectLock,
updateObjectEventHandler,
connected,
updateLockEventHandler
@ -123,6 +147,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
const startEditing = () => {
setIsEditing(true)
onStateChange({ isEditing: true })
lockObject(id, type)
}
@ -132,6 +157,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
setObjectData(serverObjectData)
}
setIsEditing(false)
onStateChange({ isEditing: false })
unlockObject(id, type)
}
@ -139,9 +165,11 @@ const EditObjectForm = ({ id, type, style, children }) => {
try {
const value = await form.validateFields()
setEditLoading(true)
onStateChange({ editLoading: true })
await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
setIsEditing(false)
onStateChange({ isEditing: false })
messageApi.success('Information updated successfully')
} catch (err) {
if (err.errorFields) {
@ -155,6 +183,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
} finally {
handleFetchObject()
setEditLoading(false)
onStateChange({ editLoading: false })
}
}
@ -180,6 +209,20 @@ const EditObjectForm = ({ id, type, style, children }) => {
}
}
useImperativeHandle(ref, () => ({
startEditing,
cancelEditing,
handleUpdate,
handleDelete,
confirmDelete,
handleFetchObject,
editLoading,
fetchLoading,
isEditing,
objectData,
lock
}))
return (
<>
<DeleteObjectModal
@ -195,7 +238,12 @@ const EditObjectForm = ({ id, type, style, children }) => {
layout='vertical'
style={style}
onValuesChange={(values) => {
setObjectData((prev) => ({ ...prev, ...values }))
if (onEdit != undefined) {
onEdit(values)
}
setObjectData((prev) => {
return { ...prev, ...values }
})
}}
>
{contextHolder}
@ -219,12 +267,17 @@ const EditObjectForm = ({ id, type, style, children }) => {
</>
)
}
)
EditObjectForm.displayName = 'EditObjectForm'
EditObjectForm.propTypes = {
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired,
style: PropTypes.object
style: PropTypes.object,
onEdit: PropTypes.func,
onStateChange: PropTypes.func
}
export default EditObjectForm

View File

@ -8,7 +8,7 @@ import {
} from 'react'
import * as THREE from 'three'
function GCodePreviewUI(props, ref, initialGCode) {
function GCodePreviewUI(props, ref) {
const {
topLayerColor = '',
lastSegmentColor = '',

View File

@ -4,6 +4,36 @@ import { getModelByName } from '../../../database/ObjectModels'
import PropTypes from 'prop-types'
import { useNavigate, useLocation } from 'react-router-dom'
// Recursively filter actions based on visibleActions
function filterActionsByVisibility(actions, visibleActions) {
if (!visibleActions) return actions
return actions.filter((action) => {
if (action.type === 'divider') {
return true // Always show dividers
}
const actionKey = action.key || action.name
const isVisible = visibleActions[actionKey] !== false
// If this action has children, filter them recursively
if (action.children && Array.isArray(action.children)) {
const filteredChildren = filterActionsByVisibility(
action.children,
visibleActions
)
action.children = filteredChildren
// Show parent if it has visible children or if it's explicitly visible
return (
isVisible &&
(filteredChildren.length > 0 || visibleActions[actionKey] === true)
)
}
return isVisible
})
}
// Recursively map actions to AntD Dropdown items
function mapActionsToMenuItems(actions, currentUrlWithActions, id) {
return actions.map((action) => {
@ -41,6 +71,7 @@ const ObjectActions = ({
id,
disabled = false,
buttonProps = {},
visibleActions = {},
...dropdownProps
}) => {
const model = getModelByName(type)
@ -54,7 +85,13 @@ const ObjectActions = ({
location.search
)
const filteredActions = actions.filter(
// First filter by visibility, then by current URL
const visibilityFilteredActions = filterActionsByVisibility(
actions,
visibleActions
)
const filteredActions = visibilityFilteredActions.filter(
(action) =>
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
@ -98,7 +135,8 @@ ObjectActions.propTypes = {
id: PropTypes.string.isRequired,
disabled: PropTypes.bool,
buttonProps: PropTypes.object,
buttonLabel: PropTypes.string
buttonLabel: PropTypes.string,
visibleActions: PropTypes.object
}
export default ObjectActions

View File

@ -20,14 +20,10 @@ import CountrySelect from './CountrySelect'
import TagsDisplay from './TagsDisplay'
import TagsInput from './TagsInput'
import BoolDisplay from './BoolDisplay'
import PrinterState from './PrinterState'
import SubJobState from './SubJobState'
import JobState from './JobState'
import ColorSelector from './ColorSelector'
import SecretDisplay from './SecretDisplay'
import EyeIcon from '../../Icons/EyeIcon'
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
import FilamentStockState from './FilamentStockState'
import { getPropertyValue } from '../../../database/ObjectModels'
import PropertyChanges from './PropertyChanges'
import NetGrossDisplay from './NetGrossDisplay'
@ -38,6 +34,10 @@ import OperationDisplay from './OperationDisplay'
import MarkdownDisplay from './MarkdownDisplay'
import ObjectSelect from './ObjectSelect'
import ObjectDisplay from './ObjectDisplay'
import ObjectTypeSelect from './ObjectTypeSelect'
import ObjectTypeDisplay from './ObjectTypeDisplay'
import CodeBlockEditor from './CodeBlockEditor'
import StateDisplay from './StateDisplay'
const { Text } = Typography
@ -65,11 +65,16 @@ const ObjectProperty = ({
name,
label,
showLabel = false,
masterFilter = {},
language = '',
objectData = null,
objectType = 'unknown',
readOnly = false,
disabled = false,
empty = false,
initial = false,
height = 'auto',
minimal = false,
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
@ -84,6 +89,10 @@ const ObjectProperty = ({
disabled = disabled(objectData)
}
if (empty && typeof empty == 'function' && objectData) {
empty = empty(objectData)
}
if (difference && typeof difference == 'function' && objectData) {
difference = difference(objectData)
}
@ -114,6 +123,9 @@ const ObjectProperty = ({
}
const renderProperty = () => {
if (empty == true) {
return <Text type='secondary'>n/a</Text>
}
if (!isEditing || (readOnly && !initial)) {
switch (type) {
case 'netGross':
@ -256,6 +268,24 @@ const ObjectProperty = ({
</Text>
)
}
case 'codeBlock':
if (value != null && value != '') {
return (
<CodeBlockEditor
code={value}
language={language}
height={height}
readOnly={true}
minimal={minimal}
/>
)
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'markdown':
if (value != null && value != '') {
return <MarkdownDisplay content={value} />
@ -297,27 +327,23 @@ const ObjectProperty = ({
)
}
}
case 'objectType': {
if (value) {
return <ObjectTypeDisplay objectType={value} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'objectList': {
return <ObjectList value={value} objectType={objectType} />
}
case 'state': {
if (value && value?.type) {
switch (objectType) {
case 'printer':
return <PrinterState state={value} {...rest} />
case 'job':
return <JobState state={value} {...rest} />
case 'subJob':
return <SubJobState state={value} {...rest} />
case 'filamentStock':
return <FilamentStockState state={value} {...rest} />
default:
return (
<Text type='secondary' {...textParams}>
No Object Type Specified
</Text>
)
}
return <StateDisplay {...rest} state={value} />
} else {
return (
<Text type='secondary' {...textParams}>
@ -566,6 +592,18 @@ const ObjectProperty = ({
/>
</Form.Item>
)
case 'codeBlock':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<CodeBlockEditor
code={value}
language={language}
disabled={disabled}
height={height}
minimal={minimal}
/>
</Form.Item>
)
case 'material':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
@ -590,13 +628,23 @@ const ObjectProperty = ({
case 'object':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectSelect type={objectType} />
<ObjectSelect
type={objectType}
disabled={disabled}
masterFilter={masterFilter}
/>
</Form.Item>
)
case 'objectType':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectTypeSelect disabled={disabled} />
</Form.Item>
)
case 'objectList':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectSelect type={objectType} multiple />
<ObjectSelect type={objectType} multiple disabled={disabled} />
</Form.Item>
)
case 'tags':
@ -624,8 +672,10 @@ ObjectProperty.propTypes = {
value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
isEditing: PropTypes.bool,
formItemProps: PropTypes.object,
masterFilter: PropTypes.object,
required: PropTypes.bool,
name: PropTypes.string,
language: PropTypes.string,
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
min: PropTypes.number,
@ -635,8 +685,10 @@ ObjectProperty.propTypes = {
objectType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
readOnly: PropTypes.bool,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
empty: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
difference: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
objectData: PropTypes.object
objectData: PropTypes.object,
height: PropTypes.string
}
export default ObjectProperty

View File

@ -9,6 +9,7 @@ import PropTypes from 'prop-types'
import { TreeSelect, Space, Button, Input } from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels'
import merge from 'lodash/merge'
@ -20,10 +21,13 @@ const ObjectSelect = ({
multiple = false,
treeSelectProps = {},
filter = {},
masterFilter = {},
value,
disabled = false,
...rest
}) => {
const { fetchObjectsByProperty } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// --- State ---
const [treeData, setTreeData] = useState([])
const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
@ -40,9 +44,14 @@ const ObjectSelect = ({
try {
const data = await fetchObjectsByProperty(type, {
properties: properties,
filter: customFilter
filter: customFilter,
masterFilter
})
if (Array.isArray(data)) {
setObjectPropertiesTree((prev) => merge([], prev, data))
} else {
setObjectPropertiesTree((prev) => merge({}, prev, data))
}
setInitialLoading(false)
setError(false)
return data
@ -97,7 +106,7 @@ const ObjectSelect = ({
value: key,
key: parentKeys.concat(key).join(':'),
property,
parentKeys: parentKeys.concat(key),
parentKeys: parentKeys.concat(key || ':'),
filterPath: newFilterPath,
selectable: false,
children: buildTreeData(
@ -131,9 +140,15 @@ const ObjectSelect = ({
// Fetch children for this node
const data = await handleFetchObjectsProperties(customFilter)
if (!data) return
// Build new children
// Extract only the children for the specific node that was expanded
let nodeSpecificData = data
if (typeof data === 'object' && !Array.isArray(data)) {
// If the API returns an object with multiple keys, get only the data for this node
nodeSpecificData = data[node.value] || {}
}
// Build new children only for this specific node
const children = buildTreeData(
data,
nodeSpecificData,
properties.indexOf(node.property) + 1,
node.parentKeys || [],
(node.filterPath || []).concat({
@ -141,7 +156,7 @@ const ObjectSelect = ({
value: node.value
})
)
// Update treeData with new children for this node
// Update treeData with new children for this node only
setTreeData((prevTreeData) => {
// Helper to recursively update the correct node
const updateNode = (nodes) =>
@ -195,11 +210,12 @@ const ObjectSelect = ({
// Build a new filter from value's properties that are in the properties list
const valueFilter = { ...filter }
properties.forEach((prop) => {
console.log('prop', prop)
if (Object.prototype.hasOwnProperty.call(value, prop)) {
const filterValue = value[prop]
if (filterValue?.name) {
valueFilter[prop] = filterValue.name
} else if (Array.isArray(filterValue)) {
valueFilter[prop] = filterValue.join(',')
} else {
valueFilter[prop] = filterValue
}
@ -211,7 +227,7 @@ const ObjectSelect = ({
setInitialized(true)
return
}
if (!initialized) {
if (!initialized && token != null) {
handleFetchObjectsProperties()
setInitialized(true)
}
@ -254,6 +270,7 @@ const ObjectSelect = ({
{...rest}
value={treeSelectValue}
onChange={onTreeSelectChange}
disabled={disabled}
/>
)
}
@ -261,13 +278,15 @@ const ObjectSelect = ({
ObjectSelect.propTypes = {
properties: PropTypes.arrayOf(PropTypes.string).isRequired,
filter: PropTypes.object,
masterFilter: PropTypes.object,
useFilter: PropTypes.bool,
value: PropTypes.any,
onChange: PropTypes.func,
showSearch: PropTypes.bool,
multiple: PropTypes.bool,
treeSelectProps: PropTypes.object,
type: PropTypes.string.isRequired
type: PropTypes.string.isRequired,
disabled: PropTypes.bool
}
export default ObjectSelect

View File

@ -57,8 +57,12 @@ const ObjectTable = forwardRef(
ref
) => {
const { token } = useContext(AuthContext)
const { fetchObjects, connected, subscribeToObject, subscribeToType } =
useContext(ApiServerContext)
const {
fetchObjects,
connected,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
var adjustedScrollHeight = scrollHeight
@ -286,14 +290,9 @@ const ObjectTable = forwardRef(
)
}, [])
const newEventHandler = useCallback(
(newData) => {
if (newData.type == type) {
const newEventHandler = useCallback(() => {
reload()
}
},
[reload, type]
)
}, [reload, type])
// Subscribe to real-time updates for all items
useEffect(() => {
@ -305,7 +304,7 @@ const ObjectTable = forwardRef(
if (page?.items && page?.items?.length > 0) {
page.items.forEach((item) => {
if (!item.isSkeleton) {
const unsubscribe = subscribeToObject(
const unsubscribe = subscribeToObjectUpdates(
item._id,
type,
updateEventHandler
@ -325,18 +324,18 @@ const ObjectTable = forwardRef(
})
}
}
}, [pages, type, subscribeToObject, updateEventHandler, connected])
}, [pages, type, subscribeToObjectUpdates, updateEventHandler, connected])
useEffect(() => {
if (connected == true) {
const unsubscribe = subscribeToType(type, newEventHandler)
const unsubscribe = subscribeToObjectTypeUpdates(type, newEventHandler)
return () => {
if (unsubscribe) {
unsubscribe()
}
}
}
}, [type, subscribeToType, connected, newEventHandler])
}, [type, subscribeToObjectTypeUpdates, connected, newEventHandler])
const updateData = useCallback(
(_id, updatedData) => {

View File

@ -0,0 +1,47 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Tag, Typography } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
const { Text } = Typography
const ObjectTypeDisplay = ({
objectType,
showIcon = true,
showLabel = true,
style,
color = 'default'
}) => {
if (!objectType) {
return <Text type='secondary'>n/a</Text>
}
const model = getModelByName(objectType)
if (!model || model.name === 'unknown') {
return <Text type='secondary'>Unknown type</Text>
}
const Icon = model.icon
return (
<Tag
color={color}
style={{ margin: 0, ...style }}
icon={showIcon && Icon ? <Icon /> : undefined}
>
{showLabel && model.label && model.label}
</Tag>
)
}
ObjectTypeDisplay.propTypes = {
objectType: PropTypes.string,
showIcon: PropTypes.bool,
showLabel: PropTypes.bool,
showPrefix: PropTypes.bool,
style: PropTypes.object,
color: PropTypes.string
}
export default ObjectTypeDisplay

View File

@ -0,0 +1,56 @@
import React from 'react'
import { Select } from 'antd'
import PropTypes from 'prop-types'
import { objectModels } from '../../../database/ObjectModels'
import ObjectTypeDisplay from './ObjectTypeDisplay'
const ObjectTypeSelect = ({
value,
onChange,
style,
placeholder = 'Select object type...',
showSearch = true,
allowClear = true,
disabled = false
}) => {
// Create options from object models
const options = objectModels
.sort((a, b) => a.label.localeCompare(b.label))
.map((model) => ({
value: model.name,
label: <ObjectTypeDisplay objectType={model.name} />
}))
console.log('VALUE', value)
return (
<Select
showSearch={showSearch}
style={{ width: '100%', ...style }}
placeholder={placeholder}
optionFilterProp='children'
value={value}
onChange={onChange}
allowClear={allowClear}
disabled={disabled}
filterOption={(input, option) =>
option.label.props.children[1].props.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
options={options}
/>
)
}
ObjectTypeSelect.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
style: PropTypes.object,
placeholder: PropTypes.string,
showSearch: PropTypes.bool,
allowClear: PropTypes.bool,
disabled: PropTypes.bool
}
export default ObjectTypeSelect

View File

@ -3,15 +3,14 @@ import { Card, Tree, Spin, Space, Button, message } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import React, { useState, useEffect, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import SubJobState from './SubJobState'
import { PrintServerContext } from '../context/PrintServerContext'
import axios from 'axios'
import JobState from './JobState'
import JobIcon from '../../Icons/JobIcon'
import SubJobIcon from '../../Icons/SubJobIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import config from '../../../config'
import StateDisplay from './StateDisplay'
const PrinterJobsTree = ({
subJobs: initialSubJobs,
@ -62,7 +61,7 @@ const PrinterJobsTree = ({
<Space size={5}>
<JobIcon />
{'Job'}
<JobState state={job?.state} />
<StateDisplay state={job?.state} />
</Space>
),
key: `job-${job._id}`,
@ -71,8 +70,8 @@ const PrinterJobsTree = ({
<Space>
<SubJobIcon />
{'Sub Job'}
<SubJobState
subJob={subJob}
<StateDisplay
state={subJob?.state}
showProgress={false}
showControls={false}
/>

View File

@ -57,6 +57,7 @@ const PropertyChanges = ({ type, value }) => {
<ObjectProperty
{...changeProperty}
longId={false}
minimal={true}
objectData={value?.old}
/>
) : null}
@ -69,6 +70,7 @@ const PropertyChanges = ({ type, value }) => {
<ObjectProperty
{...changeProperty}
longId={false}
minimal={true}
objectData={value?.new}
/>
) : null}

View File

@ -4,7 +4,7 @@ import { Progress, Flex, Space } from 'antd'
import React from 'react'
import StateTag from './StateTag'
const PrinterState = ({ state, showProgress = true, showState = true }) => {
const StateDisplay = ({ state, showProgress = true, showState = true }) => {
const currentState = state || {
type: 'unknown',
progress: 0
@ -17,9 +17,7 @@ const PrinterState = ({ state, showProgress = true, showState = true }) => {
<StateTag state={currentState.type} />
</Space>
)}
{showProgress &&
(currentState.type === 'printing' ||
currentState.type === 'deploying') ? (
{showProgress && currentState?.progress && currentState?.progress > 0 ? (
<Progress
percent={Math.round(currentState.progress * 100)}
status='active'
@ -30,10 +28,10 @@ const PrinterState = ({ state, showProgress = true, showState = true }) => {
)
}
PrinterState.propTypes = {
StateDisplay.propTypes = {
state: PropTypes.object,
showProgress: PropTypes.bool,
showState: PropTypes.bool
}
export default PrinterState
export default StateDisplay

View File

@ -1,147 +0,0 @@
import PropTypes from 'prop-types'
import { Progress, Flex, Button, Space, Tooltip } from 'antd' // eslint-disable-line
import { CaretLeftOutlined } from '@ant-design/icons' // eslint-disable-line
import React, { useContext } from 'react'
import { PrintServerContext } from '../context/PrintServerContext'
import IdDisplay from './IdDisplay'
import StateTag from './StateTag'
import XMarkIcon from '../../Icons/XMarkIcon'
import PauseIcon from '../../Icons/PauseIcon'
import BinIcon from '../../Icons/BinIcon'
import config from '../../../config'
import loglevel from 'loglevel'
const logger = loglevel.getLogger('SubJobState')
logger.setLevel(config.logLevel)
const SubJobState = ({
state,
showStatus = true,
showId = true,
showProgress = true,
showControls = true //eslint-disable-line
}) => {
const { printServer } = useContext(PrintServerContext)
const currentState = state || {
type: 'unknown',
progress: 0
}
return (
<Flex gap='small' align={'center'}>
{showId && state?._id && (
<IdDisplay
id={state._id}
showCopy={false}
type='subjob'
longId={false}
/>
)}
{showStatus && (
<Space>
<StateTag state={currentState?.type} />
</Space>
)}
{showProgress &&
(currentState.type === 'printing' ||
currentState.type === 'processing') ? (
<Progress
percent={Math.round(currentState.progress * 100)}
status='active'
style={{ width: '150px', marginBottom: '2px' }}
/>
) : null}
{showControls &&
(currentState.type === 'printing' || currentState.type === 'paused') &&
state?.printer ? (
<Space.Compact>
<Tooltip
title={currentState.type === 'printing' ? 'Pause' : 'Resume'}
arrow={false}
>
<Button
onClick={() => {
if (currentState.type === 'printing') {
printServer.emit('printer.print.pause', {
printerId: state.printer
})
} else {
printServer.emit('printer.print.resume', {
printerId: state.printer
})
}
}}
style={{ height: '22px' }}
type='text'
icon={
currentState.type === 'printing' ? (
<PauseIcon
style={{ fontSize: '12px', marginBottom: '3px' }}
/>
) : (
<CaretLeftOutlined
style={{ fontSize: '10px', marginBottom: '3px' }}
/>
)
}
></Button>
</Tooltip>
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
printServer.emit('printer.print.cancel', {
printerId: state.printer
})
}}
type='text'
style={{ height: '22px' }}
icon={
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
</Space.Compact>
) : null}
{showControls && currentState.type === 'queued' && state?._id ? (
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
printServer.emit('server.job_queue.cancel', {
subJobId: state._id
})
}}
style={{ height: '22px' }}
type='text'
icon={
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
) : null}
{showControls && currentState.type === 'draft' ? (
<Space>
<Tooltip title='Delete' arrow={false}>
<Button
onClick={() => {
logger.debug('Hello')
}}
type='text'
style={{ height: 'unset' }}
icon={
<BinIcon style={{ fontSize: '14px', marginBottom: '2px' }} />
}
/>
</Tooltip>
</Space>
) : null}
</Flex>
)
}
SubJobState.propTypes = {
state: PropTypes.object,
showProgress: PropTypes.bool,
showControls: PropTypes.bool,
showId: PropTypes.bool,
showStatus: PropTypes.bool
}
export default SubJobState

View File

@ -0,0 +1,211 @@
import React, { useState, useContext, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { Flex, Alert, Card, Spin, Splitter, Button, Modal, Input } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PlusIcon from '../../Icons/PlusIcon.jsx'
import MinusIcon from '../../Icons/MinusIcon.jsx'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon.jsx'
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx'
import { ApiServerContext } from '../context/ApiServerContext.js'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
const TemplateEditor = ({
objectData,
loading,
collapseState,
isEditing,
style
}) => {
const iframeRef = useRef(null)
const { fetchTemplatePreview } = useContext(ApiServerContext)
const [testObjectOpen, setTestObjectOpen] = useState(false)
const [previewMessage, setPreviewMessage] = useState('No issues found.')
const [previewError, setPreviewError] = useState(false)
const [previewContent, setPreviewContent] = useState('')
const [reloadLoading, setReloadLoading] = useState(false)
const [previewScale, setPreviewScale] = useState(1)
const updatePreviewContent = (html) => {
if (iframeRef.current) {
// Save current scroll position
const scrollY = iframeRef.current.contentWindow.scrollY
const scrollX = iframeRef.current.contentWindow.scrollX
// Update srcDoc
setPreviewContent(html)
// Restore scroll position after iframe loads new content
const handleLoad = () => {
iframeRef.current.contentWindow.scrollTo(scrollX, scrollY)
iframeRef.current.removeEventListener('load', handleLoad)
}
iframeRef.current.addEventListener('load', handleLoad)
}
}
function reloadPreview(content, testObject = {}, scale = 1) {
fetchTemplatePreview(
objectData._id,
content,
testObject,
scale,
(result) => {
setReloadLoading(false)
if (result?.error) {
setPreviewError(true)
setPreviewMessage(result.error)
} else {
setPreviewError(false)
updatePreviewContent(result.html)
setPreviewMessage('No issues found.')
}
}
)
}
// Move useEffect to component level and use state to track objectData changes
useEffect(() => {
if (objectData) {
console.log('PreviewScale', previewScale)
reloadPreview(objectData.content, objectData.testObject, previewScale)
}
}, [objectData, previewScale])
return (
<>
<Splitter className={'farmcontrol-splitter'}>
{collapseState.preview == true && (
<Splitter.Panel style={{ height: '100%' }}>
<Spin
spinning={loading || reloadLoading}
indicator={<LoadingOutlined />}
>
<Card style={style} styles={{ body: { height: '100%' } }}>
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Flex gap={'small'}>
{objectData?.objectType ? (
<ObjectProperty
objectType={objectData?.objectType}
name={'testObject'}
isEditing={true}
objectData={objectData}
disabled={!isEditing || objectData?.global}
type={'object'}
/>
) : (
<div style={{ flexGrow: 1 }}>
<Input disabled={true} />
</div>
)}
<Button
icon={<InfoCircleIcon />}
onClick={() => {
setTestObjectOpen(true)
}}
/>
<Button
icon={<PlusIcon />}
onClick={() => {
setPreviewScale((prev) => prev + 0.1)
}}
/>
<Button
icon={<MinusIcon />}
onClick={() => {
setPreviewScale((prev) => prev - 0.1)
}}
/>
</Flex>
<iframe
ref={iframeRef}
srcDoc={previewContent}
frameBorder='0'
style={{
width: '100%',
flexGrow: 1,
border: '1px solid #85858541',
overflow: 'auto'
}}
/>
</Flex>
</Card>
</Spin>
</Splitter.Panel>
)}
{collapseState.editor == true && (
<Splitter.Panel>
<Spin spinning={loading} indicator={<LoadingOutlined />}>
<Card style={style} styles={{ body: { height: '100%' } }}>
<Flex vertical gap={'middle'} style={{ height: '100%' }}>
<Alert
message={previewMessage}
showIcon
style={{ padding: '4px 8px' }}
icon={
previewError ? (
<ExclamationOctagonIcon />
) : (
<CheckCircleIcon />
)
}
type={previewError ? 'error' : 'success'}
/>
<div
style={{
overflowY: 'scroll',
height: '100%',
flex: '1 1 auto'
}}
>
<ObjectProperty
name={'content'}
height='100%'
type='codeBlock'
language={'xml'}
objectData={objectData}
isEditing={isEditing}
/>
</div>
</Flex>
</Card>
</Spin>
</Splitter.Panel>
)}
</Splitter>
<Modal
open={testObjectOpen}
closeIcon={null}
footer={
<Button
onClick={() => {
setTestObjectOpen(false)
}}
>
Close
</Button>
}
>
<ObjectProperty
type={'codeBlock'}
name='testObject'
language='json'
objectData={objectData}
isEditing={true}
/>
</Modal>
</>
)
}
TemplateEditor.propTypes = {
loading: PropTypes.bool,
objectData: PropTypes.object,
collapseState: PropTypes.object,
isEditing: PropTypes.bool,
style: PropTypes.object
}
export default TemplateEditor

View File

@ -77,20 +77,23 @@ const ApiServerProvider = ({ children }) => {
const newSocket = io(config.apiServerUrl, {
reconnectionAttempts: 3,
timeout: 3000,
auth: { token: token }
auth: { type: 'user' }
})
setConnecting(true)
newSocket.on('connect', () => {
logger.debug('Api Server connected')
newSocket.emit('authenticate', { token: token }, (result) => {
console.log('Auth result', result)
setConnecting(false)
setConnected(true)
setError(null)
})
})
newSocket.on('notify_object_update', notifyObjectUpdate)
newSocket.on('notify_object_new', notifyObjectNew)
newSocket.on('objectUpdate', handleObjectUpdate)
newSocket.on('objectNew', handleObjectNew)
newSocket.on('notify_lock_update', notifyLockUpdate)
newSocket.on('disconnect', () => {
@ -181,34 +184,41 @@ const ApiServerProvider = ({ children }) => {
}
}
const notifyObjectUpdate = async (object) => {
logger.debug('Notifying object update:', object)
const objectId = object._id || object.id
const handleObjectUpdate = async (data) => {
logger.debug('Notifying object update:', data)
const id = data._id
const objectType = data.objectType
if (objectId && subscribedCallbacksRef.current.has(objectId)) {
const callbacks = subscribedCallbacksRef.current.get(objectId)
const callbacksRefKey = `${objectType}:${id}`
if (
id &&
objectType &&
subscribedCallbacksRef.current.has(callbacksRefKey)
) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for object:`,
objectId
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(object)
callback(data.object)
} catch (error) {
logger.error('Error in object update callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${objectId}, subscribed callbacks:`,
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const notifyObjectNew = async (object) => {
logger.debug('Notifying object new:', object)
const objectType = object.type || 'unknown'
const handleObjectNew = async (data) => {
logger.debug('Notifying object new:', data)
const objectType = data.objectType || 'unknown'
if (objectType && subscribedCallbacksRef.current.has(objectType)) {
const callbacks = subscribedCallbacksRef.current.get(objectType)
@ -218,7 +228,7 @@ const ApiServerProvider = ({ children }) => {
)
callbacks.forEach((callback) => {
try {
callback(object)
callback(data.object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
@ -231,7 +241,7 @@ const ApiServerProvider = ({ children }) => {
}
}
const offUpdateEvent = useCallback((id, type, callback) => {
const offObjectUpdatesEvent = useCallback((id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(id)) {
@ -240,7 +250,7 @@ const ApiServerProvider = ({ children }) => {
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(id)
socketRef.current.emit('unsubscribe', { id: id, type: type })
socketRef.current.emit('unsubscribe', { id: id, type: objectType })
} else {
subscribedCallbacksRef.current.set(id, callbacks)
}
@ -248,62 +258,82 @@ const ApiServerProvider = ({ children }) => {
}
}, [])
const offTypeEvent = useCallback((type, callback) => {
const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(type)) {
if (subscribedCallbacksRef.current.has(objectType)) {
const callbacks = subscribedCallbacksRef.current
.get(type)
.get(objectType)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(type)
socketRef.current.emit('unsubscribe', { type: type })
subscribedCallbacksRef.current.delete(objectType)
socketRef.current.emit('unsubscribe', { objectType: objectType })
} else {
subscribedCallbacksRef.current.set(type, callbacks)
subscribedCallbacksRef.current.set(objectType, callbacks)
}
}
}
}, [])
const subscribeToObject = useCallback(
(id, type, callback) => {
const subscribeToObjectUpdates = useCallback(
(id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}`
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(id)) {
subscribedCallbacksRef.current.set(id, [])
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
subscribedCallbacksRef.current.get(id).push(callback)
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
socketRef.current.emit('subscribe', { id: id, type: type })
// Return cleanup function
return () => offUpdateEvent(id, type, callback)
}
socketRef.current.emit(
'subscribeToObjectUpdate',
{
_id: id,
objectType: objectType
},
[offUpdateEvent]
(result) => {
if (result.success) {
logger.info('Subscribed to id:', id, 'objectType:', objectType)
}
}
)
const subscribeToType = useCallback(
(type, callback) => {
logger.debug('Subscribing to type:', type)
// Return cleanup function
return () => offObjectUpdatesEvent(id, objectType, callback)
}
},
[offObjectUpdatesEvent]
)
const subscribeToObjectTypeUpdates = useCallback(
(objectType, callback) => {
logger.debug('Subscribing to type updates:', objectType)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(type)) {
subscribedCallbacksRef.current.set(type, [])
if (!subscribedCallbacksRef.current.has(objectType)) {
subscribedCallbacksRef.current.set(objectType, [])
}
subscribedCallbacksRef.current.get(type).push(callback)
subscribedCallbacksRef.current.get(objectType).push(callback)
logger.debug(
`Added callback for type ${type}, total callbacks: ${subscribedCallbacksRef.current.get(type).length}`
`Added callback for type ${objectType}, total callbacks: ${subscribedCallbacksRef.current.get(objectType).length}`
)
socketRef.current.emit('subscribe', { type: type })
logger.debug('Registered update event listener for object:', type)
socketRef.current.emit(
'subscribeToObjectTypeUpdate',
{ objectType: objectType },
(result) => {
if (result.success) {
logger.info('Subscribed to objectType:', objectType)
}
}
)
logger.debug('Registered update event listener for object:', objectType)
// Return cleanup function
return () => offTypeEvent(type, callback)
return () => offObjectTypeUpdatesEvent(objectType, callback)
}
},
[offTypeEvent]
[offObjectTypeUpdatesEvent]
)
const offLockEvent = useCallback((id, callback) => {
@ -324,7 +354,7 @@ const ApiServerProvider = ({ children }) => {
}
}, [])
const subscribeToLock = useCallback(
const subscribeToObjectLock = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to lock for object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
@ -453,7 +483,7 @@ const ApiServerProvider = ({ children }) => {
// Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => {
const { filter = {}, properties = [] } = params
const { filter = {}, properties = [], masterFilter = {} } = params
logger.debug('Fetching property object data from:', type, {
properties,
@ -466,7 +496,8 @@ const ApiServerProvider = ({ children }) => {
{
params: {
...filter,
properties: properties.join(',') // Convert array to comma-separated string
properties: properties.join(','), // Convert array to comma-separated string
masterFilter: JSON.stringify(masterFilter)
},
headers: {
Accept: 'application/json',
@ -636,6 +667,56 @@ const ApiServerProvider = ({ children }) => {
}
}
const fetchTemplatePreview = async (
id,
content,
testObject,
scale,
callback
) => {
logger.debug('Fetching preview...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'previewTemplate',
{
_id: id,
content: content,
testObject: testObject,
scale: scale
},
callback
)
}
}
const fetchHostOTP = async (id, callback) => {
logger.debug('Fetching host OTP...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'generateHostOtp',
{
_id: id
},
callback
)
}
}
const sendObjectAction = async (id, objectType, action, callback) => {
logger.debug('Sending object action...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'objectAction',
{
_id: id,
objectType: objectType,
action: action
},
callback
)
}
}
return (
<ApiServerContext.Provider
value={{
@ -649,10 +730,9 @@ const ApiServerProvider = ({ children }) => {
updateObject,
createObject,
deleteObject,
subscribeToObject,
subscribeToType,
subscribeToLock,
offUpdateEvent,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates,
subscribeToObjectLock,
fetchObject,
fetchObjects,
fetchObjectsByProperty,
@ -660,7 +740,10 @@ const ApiServerProvider = ({ children }) => {
fetchLoading,
showError,
fetchObjectContent,
fetchNotes
fetchTemplatePreview,
fetchNotes,
fetchHostOTP,
sendObjectAction
}}
>
{contextHolder}

View File

@ -54,13 +54,17 @@ const AuthProvider = ({ children }) => {
// Read token from session storage if present
useEffect(() => {
const storedToken = sessionStorage.getItem('authToken')
const storedUser = sessionStorage.getItem('user')
const storedExpiresAt = sessionStorage.getItem('authExpiresAt')
if (storedToken && storedExpiresAt) {
console.log('stored user', storedUser, storedToken)
if (storedToken && storedExpiresAt && storedUser) {
setToken(storedToken)
setUserProfile(storedUser)
setExpiresAt(storedExpiresAt)
setAuthenticated(true)
} else {
setAuthenticated(false)
setUserProfile(null)
setShowUnauthorizedModal(true)
}
setRetreivedTokenFromSession(true)
@ -73,6 +77,7 @@ const AuthProvider = ({ children }) => {
setUserProfile(null)
sessionStorage.removeItem('authToken')
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
}, [])
@ -112,6 +117,16 @@ const AuthProvider = ({ children }) => {
setUserProfile(response.data)
sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
console.log('SETTING USER', {
...response.data,
// eslint-disable-next-line
access_token: 'redacted',
// eslint-disable-next-line
refresh_token: 'redacted',
// eslint-disable-next-line
id_token: 'redacted'
})
sessionStorage.setItem('user', response.data)
const searchParams = new URLSearchParams(location.search)
searchParams.delete('authCode')
const newSearch = searchParams.toString()
@ -154,6 +169,7 @@ const AuthProvider = ({ children }) => {
setUserProfile(response.data)
sessionStorage.setItem('authToken', response.data.access_token)
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
sessionStorage.setItem('user', response.data)
} else {
setAuthenticated(false)
setAuthError('Failed to authenticate user.')

View File

@ -14,8 +14,7 @@ import axios from 'axios'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
import PrinterState from '../common/PrinterState'
import JobState from '../common/JobState'
import StateDisplay from '../common/StateDisplay'
import IdDisplay from '../common/IdDisplay'
import config from '../../../config'
@ -24,8 +23,6 @@ import {
getModelByPrefix
} from '../../../database/ObjectModels'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import FilamentStockState from '../common/FilamentStockState'
import SubJobState from '../common/SubJobState'
const SpotlightContext = createContext()
@ -469,38 +466,14 @@ const SpotlightProvider = ({ children }) => {
{item.name}
</Text>
) : null}
{model.type == 'printer' ? (
<PrinterState
printer={item}
{item?.state ? (
<StateDisplay
state={item.state}
showName={false}
showProgress={false}
showId={false}
/>
) : null}
{model.type == 'job' ? (
<JobState
job={item}
showQuantity={false}
showProgress={false}
showId={false}
/>
) : null}
{model.type == 'subjob' ? (
<SubJobState
subJob={item}
showProgress={false}
showId={false}
/>
) : null}
{model.type == 'filamentstock' ? (
<Flex gap={'small'}>
<FilamentStockState
filamentStock={item}
showId={false}
showProgress={false}
/>
</Flex>
) : null}
<IdDisplay id={item._id} type={type} longId={false} />
</Flex>
<Flex gap={'small'}>

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/designicon.min.svg'
const DesignIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default DesignIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/documenticon.min.svg'
const DocumentIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default DocumentIcon

View File

@ -0,0 +1,9 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/documentprintericon.min.svg'
const DocumentPrinterIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default DocumentPrinterIcon

View File

@ -0,0 +1,9 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/documentsizeicon.min.svg'
const DocumentSizeIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default DocumentSizeIcon

View File

@ -0,0 +1,9 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/documenttemplateicon.min.svg'
const DocumentTemplateIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default DocumentTemplateIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/otpicon.min.svg'
const OTPIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default OTPIcon

View File

@ -18,6 +18,9 @@ import { AuditLog } from './models/AuditLog'
import { User } from './models/User'
import { NoteType } from './models/NoteType'
import { Note } from './models/Note'
import { DocumentSize } from './models/DocumentSize.js'
import { DocumentTemplate } from './models/DocumentTemplate.js'
import { DocumentPrinter } from './models/DocumentPrinter.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
@ -40,7 +43,10 @@ export const objectModels = [
AuditLog,
User,
NoteType,
Note
Note,
DocumentSize,
DocumentTemplate,
DocumentPrinter
]
// Re-export individual models for direct access
@ -64,12 +70,21 @@ export {
AuditLog,
User,
NoteType,
Note
Note,
DocumentSize,
DocumentTemplate,
DocumentPrinter
}
export function getModelByName(name) {
export function getModelByName(name, ignoreCase = false) {
function formatName(formattedName) {
if (ignoreCase == true) {
formattedName = formattedName.toUpperCase()
}
return formattedName
}
return (
objectModels.find((meta) => meta.name === name) || {
objectModels.find((meta) => formatName(meta.name) === formatName(name)) || {
name: 'unknown',
label: 'Unknown',
prefix: 'UNK',

View File

@ -0,0 +1,125 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon'
export const DocumentPrinter = {
name: 'documentPrinter',
label: 'Document Printer',
prefix: 'DPR',
icon: DocumentPrinterIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=edit`
}
],
columns: [
'name',
'_id',
'documentSize',
'documentSize._id',
'createdAt',
'updatedAt'
],
filters: ['name', '_id'],
sorters: ['name', 'documentSize', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'documentPrinter',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text',
columnWidth: 200,
columnFixed: 'left'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'documentSize',
label: 'Document Size',
required: true,
type: 'object',
objectType: 'documentSize'
},
{
name: 'documentSize._id',
label: 'Document Size ID',
type: 'id',
objectType: 'documentSize',
showCopy: true,
showHyperlink: true
},
{
name: 'active',
label: 'Active',
required: true,
type: 'bool'
},
{
name: 'tags',
label: 'Tags',
required: false,
type: 'tags'
},
{ name: 'global', label: 'Global', required: false, type: 'bool' },
{
name: 'parent',
label: 'Parent',
required: false,
type: 'object',
objectType: 'documentPrinter',
disabled: (documentPrinter) => {
if (documentPrinter.global == true) {
documentPrinter.parent = null
}
return documentPrinter.global
}
},
{
name: 'parent._id',
label: 'Parent ID',
required: false,
type: 'id',
objectType: 'documentPrinter'
}
]
}

View File

@ -0,0 +1,86 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon'
export const DocumentSize = {
name: 'documentSize',
label: 'Document Size',
prefix: 'DSZ',
icon: DocumentSizeIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`
}
],
columns: ['name', '_id', 'width', 'height', 'createdAt', 'updatedAt'],
filters: ['name', '_id', 'width', 'height'],
sorters: ['name', 'width', 'height', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'documentSize',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text',
columnWidth: 200,
columnFixed: 'left'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'width',
label: 'Width',
required: true,
columnWidth: 150,
type: 'number',
suffix: 'mm'
},
{
name: 'height',
label: 'Height',
required: true,
columnWidth: 150,
type: 'number',
suffix: 'mm'
}
]
}

View File

@ -0,0 +1,189 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import DesignIcon from '../../components/Icons/DesignIcon'
import DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon'
export const DocumentTemplate = {
name: 'documentTemplate',
label: 'Document Template',
prefix: 'DTP',
icon: DocumentTemplateIcon,
actions: [
{
name: 'design',
label: 'Design',
row: true,
icon: DesignIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/design?documentTemplateId=${_id}`
},
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=edit`
}
],
columns: [
'name',
'_id',
'active',
'global',
'objectType',
'tags',
'parent',
'parent._id',
'documentSize',
'documentSize._id',
'createdAt',
'updatedAt'
],
filters: ['name', '_id', 'active', 'tags', 'objectType'],
group: ['documentSize', 'tags'],
sorters: [
'name',
'parent',
'documentSize',
'createdAt',
'updatedAt',
'active',
'global',
'objectType'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'documentTemplate',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text',
columnWidth: 200,
columnFixed: 'left'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'objectType',
label: 'Object Type',
required: false,
type: 'objectType',
empty: (documentTemplate) => {
return documentTemplate.global
}
},
{
name: 'active',
label: 'Active',
required: true,
type: 'bool',
columnWidth: 150
},
{
name: 'tags',
label: 'Tags',
required: false,
type: 'tags'
},
{
name: 'global',
label: 'Global',
required: true,
type: 'bool',
columnWidth: 150
},
{
name: 'documentSize',
label: 'Document Size',
required: true,
type: 'object',
objectType: 'documentSize'
},
{
name: 'documentSize._id',
label: 'Document Size ID',
type: 'id',
objectType: 'documentSize',
showCopy: true,
showHyperlink: true
},
{
name: 'parent',
label: 'Parent',
required: false,
type: 'object',
masterFilter: { global: true, active: true },
objectType: 'documentTemplate',
empty: (documentTemplate) => {
return documentTemplate.global
}
},
{
name: 'parent._id',
label: 'Parent ID',
required: false,
type: 'id',
showHyperlink: true,
objectType: 'documentTemplate',
empty: (documentTemplate) => {
return documentTemplate.global
}
},
{
name: 'documentPrinters',
label: 'Document Printers',
required: false,
type: 'objectList',
objectType: 'documentPrinter'
},
{
name: 'content',
label: 'Content',
required: false,
type: 'codeBlock',
language: 'ejs'
},
{
name: 'testObject',
label: 'Test Object',
required: false,
type: 'codeBlock',
language: 'json'
}
]
}

View File

@ -47,7 +47,15 @@ export const Filament = {
'updatedAt'
],
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'],
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
sorters: [
'name',
'createdAt',
'type',
'vendor',
'cost',
'updatedAt',
'createdAt'
],
group: ['diameter', 'type', 'vendor'],
properties: [
{

View File

@ -2,6 +2,7 @@ import HostIcon from '../../components/Icons/HostIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import OTPIcon from '../../components/Icons/OTPIcon'
export const Host = {
name: 'host',
@ -15,7 +16,7 @@ export const Host = {
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}`
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}`
},
{
@ -23,14 +24,21 @@ export const Host = {
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/production/hosts/info?hostId=${_id}&action=reload`
`/dashboard/management/hosts/info?hostId=${_id}&action=reload`
},
{
name: 'connect',
label: 'Connect',
icon: OTPIcon,
url: (_id) =>
`/dashboard/management/hosts/info?hostId=${_id}&action=hostOTP`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}&action=edit`
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}&action=edit`
}
],
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
@ -61,30 +69,95 @@ export const Host = {
},
{
name: 'state',
label: 'Status',
label: 'State',
type: 'state',
objectType: 'host',
showName: false,
readOnly: true
},
{
name: 'host',
label: 'Host',
type: 'text',
name: 'active',
label: 'Active',
type: 'bool',
required: true
},
{
name: 'online',
label: 'Online',
type: 'bool',
readOnly: true
},
{
name: 'deviceInfo.os',
label: 'Operating System',
type: 'text',
required: false,
readOnly: true,
value: (objectData) => {
if (
objectData.deviceInfo?.os?.type &&
objectData.deviceInfo?.os?.release &&
objectData.deviceInfo?.os?.arch
) {
return `${objectData.deviceInfo.os.type} ${objectData.deviceInfo.os.release} (${objectData.deviceInfo.os.arch})`
}
}
},
{
name: 'deviceInfo.os.hostname',
label: 'Hostname',
type: 'text',
required: false,
readOnly: true
},
{
name: 'deviceInfo.cpu.model',
label: 'CPU Model',
type: 'text',
required: false,
readOnly: true
},
{
name: 'deviceInfo.cpu',
label: 'CPU Info',
type: 'text',
required: false,
readOnly: true,
value: (objectData) => {
if (
objectData.deviceInfo?.cpu?.cores &&
objectData.deviceInfo?.cpu?.speedMHz
) {
return `Cores: ${objectData.deviceInfo.cpu.cores}, Speed: ${objectData.deviceInfo.cpu.speedMHz} MHz`
}
}
},
{
name: 'deviceInfo.user.username',
label: 'User',
type: 'text',
required: false,
readOnly: true
},
{
name: 'deviceInfo.user.homedir',
label: 'User Home',
type: 'text',
required: false,
readOnly: true
},
{
name: 'deviceInfo.process.nodeVersion',
label: 'NodeJS Version',
type: 'text',
required: false,
readOnly: true
},
{
name: 'tags',
label: 'Tags',
type: 'tags',
required: false
},
{
name: 'operatingSystem',
label: 'Operating System',
type: 'text',
required: false,
readOnly: true
}
]
}

View File

@ -92,8 +92,23 @@ export const Printer = {
readOnly: true
},
{
name: 'moonraker.host',
name: 'host',
label: 'Host',
type: 'object',
objectType: 'host',
required: true
},
{
name: 'host._id',
label: 'Host ID',
type: 'id',
objectType: 'host',
showHyperlink: true,
readOnly: true
},
{
name: 'moonraker.host',
label: 'Hostname',
type: 'text',
required: true
},

View File

@ -0,0 +1,25 @@
import React from 'react'
import { Route } from 'react-router-dom'
import SessionStorage from '../components/Dashboard/Developer/SessionStorage.jsx'
import AuthContextDebug from '../components/Dashboard/Developer/AuthContextDebug.jsx'
import PrintServerContextDebug from '../components/Dashboard/Developer/PrintServerContextDebug.jsx'
const DeveloperRoutes = [
<Route
key='sessionstorage'
path='developer/sessionstorage'
element={<SessionStorage />}
/>,
<Route
key='authcontextdebug'
path='developer/authcontextdebug'
element={<AuthContextDebug />}
/>,
<Route
key='printservercontextdebug'
path='developer/printservercontextdebug'
element={<PrintServerContextDebug />}
/>
]
export default DeveloperRoutes

View File

@ -0,0 +1,44 @@
import React from 'react'
import { Route } from 'react-router-dom'
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
const InventoryRoutes = [
<Route
key='filamentstocks'
path='inventory/filamentstocks'
element={<FilamentStocks />}
/>,
<Route
key='filamentstocks-info'
path='inventory/filamentstocks/info'
element={<FilamentStockInfo />}
/>,
<Route
key='partstocks'
path='inventory/partstocks'
element={<PartStocks />}
/>,
<Route
key='stockevents'
path='inventory/stockevents'
element={<StockEvents />}
/>,
<Route
key='stockaudits'
path='inventory/stockaudits'
element={<StockAudits />}
/>,
<Route
key='stockaudits-info'
path='inventory/stockaudits/info'
element={<StockAuditInfo />}
/>
]
export default InventoryRoutes

View File

@ -0,0 +1,112 @@
import React from 'react'
import { Route } from 'react-router-dom'
import Filaments from '../components/Dashboard/Management/Filaments'
import FilamentInfo from '../components/Dashboard/Management/Filaments/FilamentInfo.jsx'
import Parts from '../components/Dashboard/Management/Parts.jsx'
import PartInfo from '../components/Dashboard/Management/Parts/PartInfo.jsx'
import Products from '../components/Dashboard/Management/Products.jsx'
import ProductInfo from '../components/Dashboard/Management/Products/ProductInfo.jsx'
import Vendors from '../components/Dashboard/Management/Vendors'
import VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo'
import Materials from '../components/Dashboard/Management/Materials'
import Settings from '../components/Dashboard/Management/Settings'
import AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx'
import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx'
import NoteTypeInfo from '../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx'
import Users from '../components/Dashboard/Management/Users.jsx'
import UserInfo from '../components/Dashboard/Management/Users/UserInfo.jsx'
import Hosts from '../components/Dashboard/Management/Hosts.jsx'
import HostInfo from '../components/Dashboard/Management/Hosts/HostInfo.jsx'
import DocumentSizes from '../components/Dashboard/Management/DocumentSizes.jsx'
import DocumentSizeInfo from '../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx'
import DocumentTemplates from '../components/Dashboard/Management/DocumentTemplates.jsx'
import DocumentTemplateInfo from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx'
import DocumentPrinters from '../components/Dashboard/Management/DocumentPrinters.jsx'
import DocumentPrinterInfo from '../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx'
import DocumentTemplateDesign from '../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx'
const ManagementRoutes = [
<Route key='filaments' path='management/filaments' element={<Filaments />} />,
<Route
key='filaments-info'
path='management/filaments/info'
element={<FilamentInfo />}
/>,
<Route key='parts' path='management/parts' element={<Parts />} />,
<Route
key='parts-info'
path='management/parts/info'
element={<PartInfo />}
/>,
<Route key='products' path='management/products' element={<Products />} />,
<Route
key='products-info'
path='management/products/info'
element={<ProductInfo />}
/>,
<Route key='vendors' path='management/vendors' element={<Vendors />} />,
<Route key='hosts' path='management/hosts' element={<Hosts />} />,
<Route
key='hosts-info'
path='management/hosts/info'
element={<HostInfo />}
/>,
<Route
key='users-info'
path='management/users/info'
element={<UserInfo />}
/>,
<Route
key='vendors-info'
path='management/vendors/info'
element={<VendorInfo />}
/>,
<Route key='materials' path='management/materials' element={<Materials />} />,
<Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />,
<Route
key='notetypes-info'
path='management/notetypes/info'
element={<NoteTypeInfo />}
/>,
<Route
key='documentsizes'
path='management/documentsizes'
element={<DocumentSizes />}
/>,
<Route
key='documentsizes-info'
path='management/documentsizes/info'
element={<DocumentSizeInfo />}
/>,
<Route
key='documenttemplates'
path='management/documenttemplates'
element={<DocumentTemplates />}
/>,
<Route
key='documenttemplates-info'
path='management/documenttemplates/info'
element={<DocumentTemplateInfo />}
/>,
<Route
key='documentprinters'
path='management/documentprinters'
element={<DocumentPrinters />}
/>,
<Route
key='documentprinters-info'
path='management/documentprinters/info'
element={<DocumentPrinterInfo />}
/>,
<Route
key='documenttemplates-design'
path='management/documenttemplates/design'
element={<DocumentTemplateDesign />}
/>,
<Route key='users' path='management/users' element={<Users />} />,
<Route key='settings' path='management/settings' element={<Settings />} />,
<Route key='auditlogs' path='management/auditlogs' element={<AuditLogs />} />
]
export default ManagementRoutes

View File

@ -0,0 +1,46 @@
import React from 'react'
import { Route } from 'react-router-dom'
import ProductionOverview from '../components/Dashboard/Production/ProductionOverview'
import Printers from '../components/Dashboard/Production/Printers'
import ControlPrinter from '../components/Dashboard/Production/Printers/ControlPrinter.jsx'
import PrinterInfo from '../components/Dashboard/Production/Printers/PrinterInfo.jsx'
import Jobs from '../components/Dashboard/Production/Jobs.jsx'
import JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx'
import SubJobs from '../components/Dashboard/Production/SubJobs.jsx'
import GCodeFiles from '../components/Dashboard/Production/GCodeFiles'
import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
const ProductionRoutes = [
<Route
key='production-overview'
path='production/overview'
element={<ProductionOverview />}
/>,
<Route key='printers' path='production/printers' element={<Printers />} />,
<Route
key='printers-control'
path='production/printers/control'
element={<ControlPrinter />}
/>,
<Route
key='printers-info'
path='production/printers/info'
element={<PrinterInfo />}
/>,
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
<Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />,
<Route
key='gcodefiles'
path='production/gcodefiles'
element={<GCodeFiles />}
/>,
<Route
key='gcodefiles-info'
path='production/gcodefiles/info'
element={<GCodeFileInfo />}
/>
]
export default ProductionRoutes

4
src/routes/index.js Normal file
View File

@ -0,0 +1,4 @@
export { default as ProductionRoutes } from './ProductionRoutes'
export { default as InventoryRoutes } from './InventoryRoutes'
export { default as ManagementRoutes } from './ManagementRoutes'
export { default as DeveloperRoutes } from './DeveloperRoutes'

5473
yarn.lock

File diff suppressed because it is too large Load Diff