diff --git a/src/assets/icons/downloadicon.afdesign b/src/assets/icons/downloadicon.afdesign new file mode 100644 index 0000000..19c091a Binary files /dev/null and b/src/assets/icons/downloadicon.afdesign differ diff --git a/src/assets/icons/downloadicon.min.svg b/src/assets/icons/downloadicon.min.svg new file mode 100644 index 0000000..ef5cfa7 --- /dev/null +++ b/src/assets/icons/downloadicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/downloadicon.svg b/src/assets/icons/downloadicon.svg new file mode 100644 index 0000000..f6e0904 --- /dev/null +++ b/src/assets/icons/downloadicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/Dashboard/Management/AuditLogs.jsx b/src/components/Dashboard/Management/AuditLogs.jsx index e19882d..cd66fbd 100644 --- a/src/components/Dashboard/Management/AuditLogs.jsx +++ b/src/components/Dashboard/Management/AuditLogs.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useRef } from 'react' +import React, { useRef } from 'react' import { Button, Flex, @@ -12,14 +12,12 @@ import { Badge } from 'antd' -import { AuthContext } from '../context/AuthContext' import IdDisplay from '../common/IdDisplay' import ReloadIcon from '../../Icons/ReloadIcon' import useColumnVisibility from '../hooks/useColumnVisibility' import TimeDisplay from '../common/TimeDisplay' import ObjectTable from '../common/ObjectTable' -import config from '../../../config' import AuditLogIcon from '../../Icons/AuditLogIcon' import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' @@ -253,8 +251,6 @@ const AuditLogs = () => { columns ) - const { authenticated } = useContext(AuthContext) - const actionItems = { items: [ { @@ -294,10 +290,6 @@ const AuditLogs = () => { ) } - const visibleColumns = columns.filter( - (col) => !col.key || columnVisibility[col.key] - ) - return ( <> @@ -318,9 +310,8 @@ const AuditLogs = () => { diff --git a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx index 57a662b..000e79b 100644 --- a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx +++ b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx @@ -1,12 +1,10 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { Space, Flex, Card } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import loglevel from 'loglevel' import config from '../../../../config' -import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -17,10 +15,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import EditObjectForm from '../../common/EditObjectForm' import EditButtons from '../../common/EditButtons' import LockIndicator from './LockIndicator' -import { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels' +import ActionHandler from '../../common/ActionHandler' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' const log = loglevel.getLogger('FilamentInfo') log.setLevel(config.logLevel) @@ -51,111 +49,139 @@ const FilamentInfo = () => { 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 ( + + {({ callAction }) => ( + + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() + key='info' + > + } + isEditing={isEditing} + type='filament' + objectData={objectData} + /> + + + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) } - } - }} - > - - - - - - - - - - + key='notes' + > + + + + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - items={getModelProperties('filament').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
- - )} + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} +
+ ) + }} ) } diff --git a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx index 211e85e..267918f 100644 --- a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx +++ b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx @@ -1,10 +1,8 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown } from 'antd' +import { Space, Flex } from 'antd' import { LoadingOutlined } from '@ant-design/icons' -import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' import ViewButton from '../../common/ViewButton' @@ -13,10 +11,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import EditObjectForm from '../../common/EditObjectForm' import EditButtons from '../../common/EditButtons' import LockIndicator from '../Filaments/LockIndicator' -import { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels.js' +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 NoteTypeInfo = () => { const location = useLocation() @@ -32,7 +30,7 @@ const NoteTypeInfo = () => { return ( {({ @@ -46,99 +44,121 @@ const NoteTypeInfo = () => { editLoading, lock, fetchObject - }) => ( - - - - - - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() - } - } - }} - > - - - - - - - - - - + }) => { + // Define actions for ActionHandler + const actions = { + reload: () => { + fetchObject() + return true + }, + edit: () => { + startEditing() + return false + }, + cancelEdit: () => { + cancelEditing() + return true + }, + finishEdit: () => { + handleUpdate() + return true + } + } -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' + return ( + + {({ callAction }) => ( + - } - isEditing={isEditing} - type='noteType' - items={getModelProperties('noteType').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
-
- )} +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + key='info' + > + } + isEditing={isEditing} + type='noteType' + objectData={objectData} + /> + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} + + ) + }} ) } diff --git a/src/components/Dashboard/Management/Parts.jsx b/src/components/Dashboard/Management/Parts.jsx index d690b1a..456c443 100644 --- a/src/components/Dashboard/Management/Parts.jsx +++ b/src/components/Dashboard/Management/Parts.jsx @@ -1,11 +1,8 @@ // src/gcodefiles.js -import React, { useState, useContext, useRef } from 'react' +import React, { useState, useRef } from 'react' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd' - -import { AuthContext } from '../context/AuthContext' - import ObjectTable from '../common/ObjectTable' import NewProduct from './Products/NewProduct' @@ -20,14 +17,12 @@ import useViewMode from '../hooks/useViewMode' import ColumnViewButton from '../common/ColumnViewButton' -const Parts = () => { +const Parts = (filter) => { const [messageApi, contextHolder] = message.useMessage() const [newProductOpen, setNewProductOpen] = useState(false) const tableRef = useRef() - const { authenticated } = useContext(AuthContext) const [viewMode, setViewMode] = useViewMode('part') - const [columnVisibility, setColumnVisibility] = useColumnVisibility('part') const actionItems = { @@ -82,8 +77,8 @@ const Parts = () => { ref={tableRef} visibleColumns={columnVisibility} type='part' - authenticated={authenticated} cards={viewMode === 'cards'} + filter={filter} />
{ - const [partData, setPartData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) const location = useLocation() - const [messageApi, contextHolder] = message.useMessage() const partId = new URLSearchParams(location.search).get('partId') - const [marginOrPrice, setMarginOrPrice] = useState(false) - const [useGlobalPricing, setUseGlobalPricing] = useState(true) + + const { handleDownloadContent } = useContext(ApiServerContext) + const [collapseState, updateCollapseState] = useCollapseState('PartInfo', { info: true, - preview: true, + parts: true, notes: true, auditLogs: true }) - const [partForm] = Form.useForm() - const [partFormValues, setPartFormValues] = useState({}) - - // Add a ref to store the object URL - const objectUrlRef = useRef(null) - // Add a ref to store the array buffer - const arrayBufferRef = useRef(null) - - const [isEditing, setIsEditing] = useState(false) - const [fetchLoading, setFetchLoading] = useState(true) - - const [partFileObjectId, setPartFileObjectId] = useState(null) - const [stlLoadError, setStlLoadError] = useState(null) - - useEffect(() => { - async function fetchData() { - await fetchPartDetails() - setTimeout(async () => { - await fetchPartContent() - }, 1000) - } - if (partId) { - fetchData() - } - }, [partId]) - - useEffect(() => { - if (partData) { - partForm.setFieldsValue({ - name: partData.name || '', - price: partData.price || null, - margin: partData.margin || null, - marginOrPrice: partData.marginOrPrice, - useGlobalPricing: partData.useGlobalPricing, - createdAt: partData.createdAt || null, - updatedAt: partData.updatedAt || null - }) - setPartFormValues(partData) - } - }, [partData, partForm]) - - useEffect(() => { - setMarginOrPrice(partFormValues.marginOrPrice) - setUseGlobalPricing(partFormValues.useGlobalPricing) - }, [partFormValues]) - - const fetchPartDetails = async () => { - try { - setFetchLoading(true) - const response = await axios.get(`${config.backendUrl}/parts/${partId}`, { - headers: { - Accept: 'application/json' - }, - withCredentials: true - }) - setPartData(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch part details') - logger.debug(err) - messageApi.error('Failed to fetch part details') - } finally { - setFetchLoading(false) - } - } - - const fetchPartContent = async () => { - if (fetchLoading == true) { - return - } - try { - setFetchLoading(true) - // Cleanup previous object URL if it exists - if (objectUrlRef.current) { - URL.revokeObjectURL(objectUrlRef.current) - objectUrlRef.current = null - } - const response = await axios.get( - `${config.backendUrl}/parts/${partId}/content`, - { - withCredentials: true, - responseType: 'blob' - } - ) - - // Check file size before processing - const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB - if (response.data.size > MAX_FILE_SIZE) { - throw new Error( - `File size exceeds ${MAX_FILE_SIZE / (1024 * 1024)}MB limit` - ) - } - - // Convert blob to array buffer for better memory management - const arrayBuffer = await response.data.arrayBuffer() - - // Store array buffer in ref for later cleanup - arrayBufferRef.current = arrayBuffer - - // Create a new blob from the array buffer - const blob = new Blob([arrayBuffer], { type: response.data.type }) - - try { - // Create and store object URL - const objectUrl = URL.createObjectURL(blob) - objectUrlRef.current = objectUrl - - // Update state with the new object URL - setPartFileObjectId(objectUrl) - setStlLoadError(null) - setError(null) - } catch (allocErr) { - setStlLoadError( - 'Failed to load STL file: Array buffer allocation failed' - ) - console.error('STL allocation error:', allocErr) - } - } catch (err) { - setError('Failed to fetch part content') - logger.debug(err) - messageApi.error('Failed to fetch part content') - } finally { - setFetchLoading(false) - } - } - - const startEditing = () => { - updateCollapseState('info', true) - setIsEditing(true) - } - - const cancelEditing = () => { - // Reset form values to original data - if (partData) { - partForm.setFieldsValue({ - name: partData.name || '', - price: partData.price || null, - margin: partData.margin || null, - marginOrPrice: partData.marginOrPrice, - useGlobalPricing: partData.useGlobalPricing - }) - } - setIsEditing(false) - } - - const updateInfo = async () => { - try { - const values = await partForm.validateFields() - setLoading(true) - - await axios.put(`${config.backendUrl}/parts/${partId}`, values, { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - }) - - // Update the local state with the new values - setPartData({ ...partData, ...values }) - setIsEditing(false) - messageApi.success('Part information updated successfully') - } catch (err) { - if (err.errorFields) { - // This is a form validation error - return - } - console.error('Failed to update part information:', err) - messageApi.error('Failed to update part information') - } finally { - await fetchPartDetails() - setLoading(false) - } - } - - const actionItems = { - items: [ - { - label: 'Reload Part', - key: 'reload', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchPartDetails() - } - } - } - - const getViewDropdownItems = () => { - const sections = [ - { key: 'info', label: 'Part Information' }, - { key: 'preview', label: 'Part Preview' }, - { key: 'notes', label: 'Notes' }, - { key: 'auditLogs', label: 'Audit Logs' } - ] - - return ( - - - {sections.map((section) => ( - { - updateCollapseState(section.key, e.target.checked) - }} - > - {section.label} - - ))} - - - ) - } - - if (error) { - return ( - -

{error || 'Part not found'}

- -
- ) - } - return ( - <> - {contextHolder} - - - - - - - - - - - - {isEditing ? ( - <> - - - ) : ( -
- - - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' + + {({ + loading, + isEditing, + startEditing, + cancelEditing, + handleUpdate, + formValid, + objectData, + editLoading, + lock, + fetchObject + }) => { + const actions = { + reload: () => { + fetchObject() + return true + }, + edit: () => { + startEditing() + return false + }, + cancelEdit: () => { + cancelEditing() + return true + }, + finishEdit: () => { + handleUpdate() + return true + }, + download: () => { + if (partId) { + handleDownloadContent(partId, 'part', `${objectData.name}.stl`) + return true + } + } + } + return ( + + {({ callAction }) => ( + - - - - Part Information - - - } - key='1' - > -
- setPartFormValues((prevValues) => ({ - ...prevValues, - ...changedValues - })) - } - initialValues={{ - name: partData?.name || '', - version: partData?.version || '', - tags: partData?.tags || [] - }} - > - } - spinning={fetchLoading} + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + key='info' > - - - {partData?.id ? ( - - ) : ( - n/a - )} - - - {partData?.createdAt ? ( - - ) : ( - n/a - )} - + + - - {isEditing ? ( - - - - ) : partData?.name ? ( - {partData.name} - ) : ( - n/a - )} - + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) + } + key='notes' + > + + + + - - {partData?.updatedAt ? ( - - ) : ( - n/a - )} - - - - {partData?.product?.name ? ( - {partData.product.name} - ) : ( - n/a - )} - - - {partData?.product?._id ? ( - - ) : ( - n/a - )} - - - {isEditing && useGlobalPricing == false ? ( - - {marginOrPrice == false ? ( - - - - ) : ( - - - - )} - - Price - - - ) : partData?.margin && - marginOrPrice == false && - partData?.useGlobalPricing == false ? ( - {partData.margin + '%'} - ) : partData?.price && - marginOrPrice == true && - partData?.useGlobalPricing == false ? ( - {'£' + partData.price} - ) : ( - n/a - )} - - - {isEditing ? ( - - - - ) : partData ? ( - - ) : ( - n/a - )} - - - {partData?.version ? ( - {partData.version} - ) : ( - n/a - )} - - - {partData?.tags && partData.tags.length > 0 ? ( - partData.tags.map((tag, index) => ( - {tag} - )) - ) : ( - n/a - )} - - - - - - - - - updateCollapseState('preview', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - - - Part Preview - - - } - key='2' - > - - {stlLoadError ? ( -
- - - - {stlLoadError} - - -
- ) : ( - partFileObjectId && ( - - ) - )} -
- - - - - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - - - Notes - - - } - key='notes' - > - - - - - - - - updateCollapseState('auditLogs', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - - - Audit Logs - - - } - key='auditLogs' - > - - - - -
- )} -
- + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} + + ) + }} + ) } diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx index a99de80..4dec3ac 100644 --- a/src/components/Dashboard/Management/Products/ProductInfo.jsx +++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx @@ -1,9 +1,7 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card } from 'antd' -import ReloadIcon from '../../../Icons/ReloadIcon' +import { Space, Flex, Card } from 'antd' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -11,11 +9,14 @@ import ViewButton from '../../common/ViewButton' import EditObjectForm from '../../common/EditObjectForm' import EditButtons from '../../common/EditButtons' import LockIndicator from '../Filaments/LockIndicator' -import PartsTable from '../../common/PartsTable' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ProductIcon from '../../../Icons/ProductIcon.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' const ProductInfo = () => { const location = useLocation() @@ -44,191 +45,153 @@ const ProductInfo = () => { editLoading, lock, fetchObject - }) => ( - - - - - + }) => { + const actions = { + reload: () => { + fetchObject() + return true + }, + edit: () => { + startEditing() + return false + }, + cancelEdit: () => { + cancelEditing() + return true + }, + finishEdit: () => { + handleUpdate() + return true + } + } + return ( + + {({ callAction }) => ( + + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() + key='info' + > + + + + } + active={collapseState.parts} + onToggle={(expanded) => + updateCollapseState('parts', expanded) } - } - }} - > - - - - - - - - - - + key='parts' + > + + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - - + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) + } + key='notes' + > + + + + - } - active={collapseState.parts} - onToggle={(expanded) => updateCollapseState('parts', expanded)} - key='parts' - > - - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
- - )} + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} +
+ ) + }} ) } diff --git a/src/components/Dashboard/Management/Users.jsx b/src/components/Dashboard/Management/Users.jsx index 4c92a28..de8613f 100644 --- a/src/components/Dashboard/Management/Users.jsx +++ b/src/components/Dashboard/Management/Users.jsx @@ -40,9 +40,9 @@ const Users = () => {
diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx index 667598c..135850d 100644 --- a/src/components/Dashboard/Management/Users/UserInfo.jsx +++ b/src/components/Dashboard/Management/Users/UserInfo.jsx @@ -1,10 +1,8 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { Space, Flex, Card } from 'antd' import { LoadingOutlined } from '@ant-design/icons' -import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -15,10 +13,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import EditObjectForm from '../../common/EditObjectForm' import EditButtons from '../../common/EditButtons' import LockIndicator from '../Filaments/LockIndicator' -import { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels.js' +import ActionHandler from '../../common/ActionHandler' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' const UserInfo = () => { const location = useLocation() @@ -46,112 +44,136 @@ const UserInfo = () => { 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 ( + + {({ callAction }) => ( + + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading || true} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() + key='info' + > + } + isEditing={isEditing} + type='user' + objectData={objectData} + /> + + + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) } - } - }} - > - - - - - - - - - - + key='notes' + > + + + + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - type='user' - items={getModelProperties('user').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
- - )} + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} +
+ ) + }} ) } diff --git a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx index 7c7a661..1b2ebe2 100644 --- a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx +++ b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx @@ -1,12 +1,9 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card } from 'antd' -import { LoadingOutlined } from '@ant-design/icons' +import { Space, Flex, Card } from 'antd' import loglevel from 'loglevel' import config from '../../../../config' -import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -17,10 +14,10 @@ import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import EditObjectForm from '../../common/EditObjectForm' import EditButtons from '../../common/EditButtons' import LockIndicator from '../Filaments/LockIndicator' -import { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels' +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('VendorInfo') log.setLevel(config.logLevel) @@ -51,111 +48,135 @@ const VendorInfo = () => { 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 ( + + {({ callAction }) => ( + + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() + key='info' + > + + + + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) } - } - }} - > - - - - - - - - - - + key='notes' + > + + + + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - items={getModelProperties('vendor').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
- - )} + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} +
+ ) + }} ) } diff --git a/src/components/Dashboard/Production/GCodeFiles.jsx b/src/components/Dashboard/Production/GCodeFiles.jsx index 354a2db..49c0e7e 100644 --- a/src/components/Dashboard/Production/GCodeFiles.jsx +++ b/src/components/Dashboard/Production/GCodeFiles.jsx @@ -1,8 +1,7 @@ // src/gcodefiles.js -import React, { useState, useContext, useRef } from 'react' +import React, { useState, useRef } from 'react' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd' -import { AuthContext } from '../context/AuthContext' import NewGCodeFile from './GCodeFiles/NewGCodeFile' import useColumnVisibility from '../hooks/useColumnVisibility' import PlusIcon from '../../Icons/PlusIcon' @@ -23,8 +22,6 @@ const GCodeFiles = () => { const [columnVisibility, setColumnVisibility] = useColumnVisibility('gcodeFile') - const { authenticated } = useContext(AuthContext) - const actionItems = { items: [ { @@ -59,9 +56,9 @@ const GCodeFiles = () => {
@@ -75,8 +72,7 @@ const GCodeFiles = () => {
diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx index c2ca031..1d3f11f 100644 --- a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx @@ -1,10 +1,8 @@ import React, { useContext } from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card, Typography } from 'antd' +import { Space, Flex, Card, Typography } from 'antd' import { LoadingOutlined } from '@ant-design/icons' -import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -18,10 +16,9 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx' import { ApiServerContext } from '../../context/ApiServerContext' -import { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels.js' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' const { Text } = Typography @@ -31,7 +28,7 @@ const GCodeFileInfo = () => { const { handleDownloadContent } = useContext(ApiServerContext) const [collapseState, updateCollapseState] = useCollapseState( - 'GCodeFileInfo', + 'gcodeFileInfo', { info: true, preview: true, @@ -40,183 +37,187 @@ const GCodeFileInfo = () => { } ) - // Define actions that can be triggered via URL - const actions = { - download: () => { - if (gcodeFileId) { - handleDownloadContent( - gcodeFileId, - 'gcodeFile', - `gcodeFile-${gcodeFileId}.gcode` - ) - } - } - } - return ( - <> - - - {({ - loading, - isEditing, - startEditing, - cancelEditing, - handleUpdate, - formValid, - objectData, - editLoading, - lock, - fetchObject - }) => ( - - - - - - }, - { - label: 'Download GCode File', - key: 'download', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() - } else if (key === 'download' && gcodeFileId) { - handleDownloadContent( - gcodeFileId, - 'gcodefile', - `gcodefile-${gcodeFileId}.gcode` - ) - } - } - }} - > - - - - - - - - - - + + {({ + loading, + isEditing, + startEditing, + cancelEditing, + handleUpdate, + formValid, + objectData, + editLoading, + lock, + fetchObject + }) => { + // Define actions that can be triggered via URL, now with access to startEditing + const actions = { + reload: () => { + fetchObject() + return true + }, + edit: () => { + startEditing() + return false + }, + cancelEdit: () => { + cancelEditing() + return true + }, + finishEdit: () => { + handleUpdate() + return true + }, + download: () => { + if (gcodeFileId) { + handleDownloadContent( + gcodeFileId, + 'gcodeFile', + `${objectData.name}.gcode` + ) + return true + } + } + } -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - items={getModelProperties('gcodeFile').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - objectData={objectData} - type='gcodefile' - /> - - - } - active={collapseState.preview} - onToggle={(expanded) => - updateCollapseState('preview', expanded) - } - key='preview' - > - - {objectData?.gcodeFileInfo?.thumbnail ? ( - GCodeFile + {({ callAction }) => ( + + + + + - ) : ( - n/a - )} - - + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + - } - active={collapseState.notes} - onToggle={(expanded) => - updateCollapseState('notes', expanded) - } - key='notes' - > - - - - +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + key='info' + > + } + isEditing={isEditing} + objectData={objectData} + type='gcodeFile' + /> + - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - + } + active={collapseState.preview} + onToggle={(expanded) => + updateCollapseState('preview', expanded) + } + key='preview' + > + + {objectData?.gcodeFileInfo?.thumbnail ? ( + GCodeFile + ) : ( + n/a + )} + + + + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) + } + key='notes' + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
-
-
- )} -
- + )} +
+ ) + }} + ) } diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index 2ee0074..dfb04f1 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -1,313 +1,27 @@ // src/Jobs.js import React, { useState, useContext, useRef } from 'react' -import { useNavigate } from 'react-router-dom' -import { - Button, - Flex, - Space, - Modal, - Dropdown, - message, - notification, - Input, - Typography, - Checkbox, - Popover -} from 'antd' +import { Button, Flex, Space, Modal, Dropdown, message } from 'antd' import { AuthContext } from '../context/AuthContext.js' -import { PrintServerContext } from '../context/PrintServerContext.js' import NewJob from './Jobs/NewJob.jsx' -import JobState from '../common/JobState.jsx' -import SubJobCounter from '../common/SubJobCounter.jsx' -import TimeDisplay from '../common/TimeDisplay.jsx' -import IdDisplay from '../common/IdDisplay.jsx' import useColumnVisibility from '../hooks/useColumnVisibility.js' -import JobIcon from '../../Icons/JobIcon.jsx' -import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx' import PlusIcon from '../../Icons/PlusIcon.jsx' import ReloadIcon from '../../Icons/ReloadIcon.jsx' -import EditIcon from '../../Icons/EditIcon.jsx' -import XMarkIcon from '../../Icons/XMarkIcon.jsx' -import CheckIcon from '../../Icons/CheckIcon.jsx' -import PlayCircleIcon from '../../Icons/PlayCircleIcon.jsx' -import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx' -import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx' -import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx' -import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx' import ObjectTable from '../common/ObjectTable.jsx' import ListIcon from '../../Icons/ListIcon.jsx' import GridIcon from '../../Icons/GridIcon.jsx' import useViewMode from '../hooks/useViewMode.js' - -const { Text } = Typography +import ColumnViewButton from '../common/ColumnViewButton.jsx' const Jobs = () => { const [messageApi, contextHolder] = message.useMessage() - const [notificationApi, notificationContextHolder] = - notification.useNotification() - const navigate = useNavigate() const [newJobOpen, setNewJobOpen] = useState(false) const tableRef = useRef() - const [viewMode, setViewMode] = useViewMode('Jobs') - - const getFilterDropdown = ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - propertyName - }) => { - return ( -
- - - setSelectedKeys(e.target.value ? [e.target.value] : []) - } - onPressEnter={() => confirm()} - style={{ width: 200, display: 'block' }} - /> -
- ) - } - - // Column definitions - const columns = [ - { - title: , - key: 'icon', - width: 40, - fixed: 'left', - render: () => - }, - { - title: 'GCode File Name', - key: 'gcodeFileName', - width: 200, - fixed: 'left', - render: (record) => {record?.gcodeFile?.name}, - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters - }) => - getFilterDropdown({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - propertyName: 'GCode file name' - }), - onFilter: (value, record) => - record.gcodeFile.name.toLowerCase().includes(value.toLowerCase()) - }, - { - title: 'ID', - dataIndex: 'id', - key: 'id', - width: 180, - render: (text) => , - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters - }) => - getFilterDropdown({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - propertyName: 'ID' - }), - onFilter: (value, record) => - record.id.toLowerCase().includes(value.toLowerCase()) - }, - { - title: 'State', - key: 'state', - dataIndex: 'state', - width: 240, - render: (state) => { - return - }, - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters - }) => - getFilterDropdown({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - propertyName: 'state' - }), - onFilter: (value, record) => - record?.state?.type?.toLowerCase().includes(value.toLowerCase()) - }, - { - title: , - key: 'complete', - width: 70, - render: (record) => { - return - } - }, - { - title: , - key: 'queued', - width: 70, - render: (record) => { - return - } - }, - { - title: , - key: 'failed', - width: 70, - render: (record) => { - return - } - }, - { - title: , - key: 'draft', - width: 70, - render: (record) => { - return - } - }, - { - title: 'Created At', - dataIndex: 'createdAt', - key: 'createdAt', - width: 180, - render: (createdAt) => { - if (createdAt) { - return - } else { - return 'n/a' - } - }, - sorter: true - }, - { - title: 'Started At', - dataIndex: 'startedAt', - key: 'startedAt', - width: 180, - render: (startedAt) => { - if (startedAt) { - return - } else { - return 'n/a' - } - }, - sorter: true - }, - { - title: 'Actions', - key: 'actions', - fixed: 'right', - width: 150, - render: (record) => { - return ( - - {record?.state?.type === 'draft' ? ( - -
-
- ) - } - } - ] - + const [viewMode, setViewMode] = useViewMode('job') const { authenticated } = useContext(AuthContext) - const { printServer } = useContext(PrintServerContext) - const [columnVisibility, updateColumnVisibility] = useColumnVisibility( - 'Jobs', - columns - ) - - const handleDeployJob = (jobId) => { - if (printServer) { - messageApi.info(`Print job ${jobId} deployment initiated`) - printServer.emit('server.job_queue.deploy', { jobId }, (response) => { - if (response == false) { - notificationApi.error({ - message: 'Print job deployment failed', - description: 'Please try again later' - }) - } else { - notificationApi.success({ - message: 'Print job deployment initiated', - description: 'Please wait for the print job to start' - }) - } - }) - navigate(`/dashboard/production/jobs/info?jobId=${jobId}`) - } else { - messageApi.error('Socket connection not available') - } - } - - const getJobActionItems = (jobId) => { - return { - items: [ - { - label: 'Info', - key: 'info', - icon: - }, - { - label: 'Edit', - key: 'edit', - icon: - } - ], - onClick: ({ key }) => { - if (key === 'edit') { - showNewJobModal(jobId) - } else if (key === 'info') { - navigate(`/dashboard/production/jobs/info?jobId=${jobId}`) - } - } - } - } + const [columnVisibility, setColumnVisibility] = useColumnVisibility('job') const actionItems = { items: [ @@ -336,33 +50,8 @@ const Jobs = () => { setNewJobOpen(true) } - const getViewDropdownItems = () => { - const columnItems = columns - .filter((col) => col.key && col.title !== '') - .map((col) => ( - { - updateColumnVisibility(col.key, e.target.checked) - }} - > - {col.title} - - )) - - return ( - - - {columnItems} - - - ) - } - return ( <> - {notificationContextHolder} {contextHolder} @@ -370,14 +59,14 @@ const Jobs = () => { - - - +
+ - - - - -
- - - -
+ key='info' + > + } + isEditing={isEditing} + type='job' + objectData={objectData} + /> + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - type='job' - items={getModelProperties('job').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - + } + active={collapseState.subJobs} + onToggle={(expanded) => + updateCollapseState('subJobs', expanded) + } + key='subJobs' + > + + - } - active={collapseState.subJobs} - onToggle={(expanded) => - updateCollapseState('subJobs', expanded) - } - key='subJobs' - > - - + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) + } + key='notes' + > + + + + - } - active={collapseState.notes} - onToggle={(expanded) => - updateCollapseState('notes', expanded) - } - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
- -
- )} - - + )} + + ) + }} + ) } diff --git a/src/components/Dashboard/Production/Printers.jsx b/src/components/Dashboard/Production/Printers.jsx index 518de6f..a61e8a0 100644 --- a/src/components/Dashboard/Production/Printers.jsx +++ b/src/components/Dashboard/Production/Printers.jsx @@ -1,9 +1,7 @@ // src/Printers.js -import React, { useState, useContext, useRef } from 'react' +import React, { useState, useRef } from 'react' import { Button, message, Dropdown, Space, Flex, Modal } from 'antd' - -import { AuthContext } from '../context/AuthContext' import NewPrinter from './Printers/NewPrinter' import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' @@ -17,7 +15,6 @@ import useColumnVisibility from '../hooks/useColumnVisibility' const Printers = () => { const [messageApi, contextHolder] = message.useMessage() - const { authenticated } = useContext(AuthContext) const [newPrinterOpen, setNewPrinterOpen] = useState(false) const tableRef = useRef() @@ -61,9 +58,9 @@ const Printers = () => { @@ -78,8 +75,7 @@ const Printers = () => { diff --git a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx index 60c15c4..99d6fd9 100644 --- a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx +++ b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx @@ -1,9 +1,8 @@ import React from 'react' import { useLocation } from 'react-router-dom' -import { Space, Button, Flex, Dropdown, Card } from 'antd' +import { Space, Flex, Card } from 'antd' import { LoadingOutlined } from '@ant-design/icons' import useCollapseState from '../../hooks/useCollapseState' -import AuditLogTable from '../../common/AuditLogTable' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' @@ -16,10 +15,10 @@ 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 { - getModelProperties, - getPropertyValue -} from '../../../../database/ObjectModels.js' +import ActionHandler from '../../common/ActionHandler' +import ObjectActions from '../../common/ObjectActions.jsx' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' const PrinterInfo = () => { const location = useLocation() @@ -28,7 +27,8 @@ const PrinterInfo = () => { info: true, jobs: true, notes: true, - auditLogs: true + auditLogsParent: true, + auditLogsOwner: true }) return ( @@ -48,126 +48,178 @@ const PrinterInfo = () => { 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 ( + + {({ callAction }) => ( + + + + + + + + + + + { + callAction('finishEdit') + }} + cancelEditing={() => { + callAction('cancelEdit') + }} + startEditing={() => { + callAction('edit') + }} + editLoading={editLoading} + formValid={formValid} + disabled={lock?.locked || loading} + loading={editLoading} + /> + + + +
+ + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) } - ], - onClick: ({ key }) => { - if (key === 'reload') { - fetchObject() + key='info' + > + } + isEditing={isEditing} + type='printer' + objectData={objectData} + /> + + + } + active={collapseState.jobs} + onToggle={(expanded) => + updateCollapseState('jobs', expanded) } - } - }} - > - - - - - - - - - - + key='jobs' + > + + -
- - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - key='info' - > - } - isEditing={isEditing} - type='printer' - items={getModelProperties('printer').map((prop) => ({ - ...prop, - value: getPropertyValue(objectData, prop.name) - }))} - /> - + } + active={collapseState.notes} + onToggle={(expanded) => + updateCollapseState('notes', expanded) + } + key='notes' + > + + + + - } - active={collapseState.jobs} - onToggle={(expanded) => updateCollapseState('jobs', expanded)} - key='jobs' - > - - - - } - active={collapseState.notes} - onToggle={(expanded) => updateCollapseState('notes', expanded)} - key='notes' - > - - - - - - } - active={collapseState.auditLogs} - onToggle={(expanded) => - updateCollapseState('auditLogs', expanded) - } - key='auditLogs' - > - - - -
- - )} + } + active={collapseState.auditLogsParent} + onToggle={(expanded) => + updateCollapseState('auditLogsParent', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + } + active={collapseState.auditLogsOwner} + onToggle={(expanded) => + updateCollapseState('auditLogsOwner', expanded) + } + key='auditLogs' + > + {loading ? ( + + ) : ( + + )} + + +
+
+ )} +
+ ) + }} ) } diff --git a/src/components/Dashboard/common/ActionHandler.jsx b/src/components/Dashboard/common/ActionHandler.jsx index 3a33ef2..1d22524 100644 --- a/src/components/Dashboard/common/ActionHandler.jsx +++ b/src/components/Dashboard/common/ActionHandler.jsx @@ -1,38 +1,61 @@ -import { useEffect } from 'react' +import React, { useEffect, useRef } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import PropTypes from 'prop-types' const ActionHandler = ({ + children, actions = {}, actionParam = 'action', clearAfterExecute = true, - onAction + onAction, + loading = true }) => { const location = useLocation() const navigate = useNavigate() const action = new URLSearchParams(location.search).get(actionParam) + // Ref to track last executed action + const lastExecutedAction = useRef(null) + + // Method to add action as URL param + const callAction = (actionName) => { + const searchParams = new URLSearchParams(location.search) + searchParams.set(actionParam, actionName) + const newSearch = searchParams.toString() + const newPath = location.pathname + (newSearch ? `?${newSearch}` : '') + navigate(newPath, { replace: true }) + } + // Execute action and clear from URL useEffect(() => { - if (action && actions[action]) { + if ( + !loading && + action && + actions[action] && + lastExecutedAction.current !== action + ) { // Execute the action const result = actions[action]() - + // Mark this action as executed + lastExecutedAction.current = action // Call optional callback if (onAction) { onAction(action, result) } - - // Clear action from URL if requested - if (clearAfterExecute) { + // Clear action from URL if requested and result is true + if (clearAfterExecute && result == true) { const searchParams = new URLSearchParams(location.search) searchParams.delete(actionParam) const newSearch = searchParams.toString() const newPath = location.pathname + (newSearch ? `?${newSearch}` : '') navigate(newPath, { replace: true }) } + } else if (!action) { + // Reset lastExecutedAction if no action is present + lastExecutedAction.current = null } }, [ + loading, action, actions, actionParam, @@ -44,14 +67,16 @@ const ActionHandler = ({ ]) // Return null as this is a utility component - return null + return <>{children({ callAction })} } ActionHandler.propTypes = { + children: PropTypes.func, actions: PropTypes.objectOf(PropTypes.func), actionParam: PropTypes.string, clearAfterExecute: PropTypes.bool, - onAction: PropTypes.func + onAction: PropTypes.func, + loading: PropTypes.bool } export default ActionHandler diff --git a/src/components/Dashboard/common/ColumnViewButton.jsx b/src/components/Dashboard/common/ColumnViewButton.jsx index 6e5826f..455c900 100644 --- a/src/components/Dashboard/common/ColumnViewButton.jsx +++ b/src/components/Dashboard/common/ColumnViewButton.jsx @@ -5,9 +5,9 @@ import { getModelByName } from '../../../database/ObjectModels' const ColumnViewButton = ({ type, - loading = false, - collapseState = {}, - updateCollapseState = () => {}, + disabled = false, + visibleState = {}, + updateVisibleState = () => {}, ...buttonProps }) => { // Get the model by name @@ -31,10 +31,10 @@ const ColumnViewButton = ({ return ( ) @@ -42,9 +42,9 @@ const ColumnViewButton = ({ ColumnViewButton.propTypes = { type: PropTypes.string.isRequired, - loading: PropTypes.bool, - collapseState: PropTypes.object, - updateCollapseState: PropTypes.func + disabled: PropTypes.bool, + visibleState: PropTypes.object, + updateVisibleState: PropTypes.func } export default ColumnViewButton diff --git a/src/components/Dashboard/common/EditObjectForm.jsx b/src/components/Dashboard/common/EditObjectForm.jsx index 54b53ae..4f4cda0 100644 --- a/src/components/Dashboard/common/EditObjectForm.jsx +++ b/src/components/Dashboard/common/EditObjectForm.jsx @@ -17,6 +17,7 @@ import PropTypes from 'prop-types' */ const EditObjectForm = ({ id, type, style, children }) => { const [objectData, setObjectData] = useState(null) + const [serverObjectData, setServerObjectData] = useState(null) const [fetchLoading, setFetchLoading] = useState(true) const [editLoading, setEditLoading] = useState(false) const [lock, setLock] = useState({}) @@ -59,12 +60,6 @@ const EditObjectForm = ({ id, type, style, children }) => { } }, [id, type, unlockObject]) - useEffect(() => { - if (objectData) { - form.setFieldsValue(objectData) - } - }, [objectData, form]) - const fetchObject = useCallback(async () => { try { setFetchLoading(true) @@ -72,6 +67,7 @@ const EditObjectForm = ({ id, type, style, children }) => { const lockEvent = await fetchObjectLock(id, type) setLock(lockEvent) setObjectData(data) + setServerObjectData(data) form.setFieldsValue(data) setFetchLoading(false) } catch (err) { @@ -120,8 +116,9 @@ const EditObjectForm = ({ id, type, style, children }) => { } const cancelEditing = () => { - if (objectData) { - form.setFieldsValue(objectData) + if (serverObjectData) { + form.setFieldsValue(serverObjectData) + setObjectData(serverObjectData) } setIsEditing(false) unlockObject(id, type) @@ -151,7 +148,14 @@ const EditObjectForm = ({ id, type, style, children }) => { } return ( -
+ { + setObjectData((prev) => ({ ...prev, ...values })) + }} + > {contextHolder} {children({ loading: fetchLoading, diff --git a/src/components/Dashboard/common/EmailDisplay.jsx b/src/components/Dashboard/common/EmailDisplay.jsx index 809539f..7ed8f91 100644 --- a/src/components/Dashboard/common/EmailDisplay.jsx +++ b/src/components/Dashboard/common/EmailDisplay.jsx @@ -24,9 +24,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => { +
+ ) +} + +ObjectActions.propTypes = { + type: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + disabled: PropTypes.bool, + buttonProps: PropTypes.object, + buttonLabel: PropTypes.string +} + +export default ObjectActions diff --git a/src/components/Dashboard/common/ObjectInfo.jsx b/src/components/Dashboard/common/ObjectInfo.jsx index ba16461..b2a0cd1 100644 --- a/src/components/Dashboard/common/ObjectInfo.jsx +++ b/src/components/Dashboard/common/ObjectInfo.jsx @@ -1,27 +1,37 @@ import React from 'react' import { Spin, Descriptions } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' import PropTypes from 'prop-types' import ObjectProperty from './ObjectProperty' +import { getModelProperties } from '../../../database/ObjectModels' const ObjectInfo = ({ loading = false, - indicator = null, bordered = true, isEditing = false, - items = [] + type = 'unknown', + objectData = null }) => { + const items = getModelProperties(type) + // Map items to Descriptions 'items' prop format const descriptionItems = items.map((item, idx) => { const key = item.name || item.label || idx return { key, label: item.label, - children: + children: ( + + ) } }) return ( - + }> { + if (typeof value == 'function' && objectData) { + value = disabled(objectData) + } + + if (objectType && typeof objectType == 'function' && objectData) { + objectType = objectType(objectData) + } + + if (disabled && typeof disabled == 'function' && objectData) { + disabled = disabled(objectData) + } + + if (!value) { + value = getPropertyValue(objectData, name) + } + // Split the name by "." to handle nested object properties var formItemName = name @@ -65,6 +90,12 @@ const ObjectProperty = ({ formItemName = name ? name.split('.') : undefined } + var textParams = {} + + if (disabled == true) { + textParams = { ...textParams, delete: true, type: 'secondary' } + } + const renderProperty = () => { if (!isEditing || readOnly) { switch (type) { @@ -72,93 +103,154 @@ const ObjectProperty = ({ if (value != null) { return } else { - return n/a + return ( + + n/a + + ) } case 'wsprotocol': switch (value) { case 'ws': - return Websocket + return Websocket case 'wss': - return Websocket Secure + return Websocket Secure default: - return n/a + return ( + + n/a + + ) + } + case 'priceMode': + switch (value) { + case 'margin': + return Margin % + case 'amount': + return £ Amount + default: + return ( + + n/a + + ) } case 'bool': { if (value != null) { return } else { - return n/a + return ( + + n/a + + ) } } case 'dateTime': { if (value != null) { return } else { - return n/a - } - } - case 'currency': { - if (value != null) { - return {`£${value}/kg`} - } else { - return n/a + return ( + + n/a + + ) } } case 'country': { if (value != null) { return } else { - return n/a + return ( + + n/a + + ) } } case 'color': { if (value) { return } else { - return n/a - } - } - case 'weight': { - if (value != null) { - return {`${value}g`} - } else { - return n/a + return ( + + n/a + + ) } } case 'number': { if (value != null) { if (Array.isArray(value)) { - return {value.length} + return ( + + {prefix} + {value.length} + {suffix} + + ) } else { - return {value} + return ( + + {prefix} + {value} + {suffix} + + ) } } else { - return n/a + return ( + + n/a + + ) } } case 'text': if (value != null && value != '') { - return {value} + return ( + + {prefix} + {value} + {suffix} + + ) } else { - return n/a + return ( + + n/a + + ) } case 'email': if (value != null && value != '') { return } else { - return n/a + return ( + + n/a + + ) } case 'url': if (value != null && value != '') { return } else { - return n/a + return ( + + n/a + + ) } case 'object': { if (value && value.name) { return {value.name} } else { - return n/a + return ( + + n/a + + ) } } case 'state': { @@ -173,59 +265,87 @@ const ObjectProperty = ({ case 'filamentStock': return default: - return No Object Type Specified + return ( + + No Object Type Specified + + ) } } else { - return n/a + return ( + + n/a + + ) } } case 'material': { if (value) { - return {value} + return {value} } else { - return n/a + return ( + + n/a + + ) } } case 'id': { if (value) { return } else { - return n/a + return ( + + n/a + + ) } } case 'density': { if (value != null) { - return {`${value} g/cm³`} + return {`${value} g/cm³`} } else { - return n/a + return ( + + n/a + + ) } } case 'mm': { if (value != null) { - return {`${value} mm`} + return {`${value} mm`} } else { - return n/a + return ( + + n/a + + ) } } case 'tags': { if (value != null || value?.length != 0) { return } else { - return n/a + return ( + + n/a + + ) } } - case 'version': { - if (value != null) { - return {`${value} mm`} - } else { - return n/a - } + case 'propertyChanges': { + return } default: { if (value) { - return {value} + return {value} } else { - return n/a + return ( + + n/a + + ) } } } @@ -266,6 +386,7 @@ const ObjectProperty = ({ visible ? : @@ -278,6 +399,7 @@ const ObjectProperty = ({ + + ) case 'bool': return ( - + ) case 'dateTime': @@ -302,24 +437,17 @@ const ObjectProperty = ({ {...mergedFormItemProps} getValueProps={(v) => ({ value: v ? dayjs(v) : null })} > - - - ) - case 'currency': - return ( - - ) case 'country': return ( - + ) case 'color': @@ -330,7 +458,7 @@ const ObjectProperty = ({ valuePropName='value' getValueFromEvent={(v) => v} > - + ) case 'weight': @@ -340,6 +468,7 @@ const ObjectProperty = ({ suffix='g' style={{ width: '100%' }} placeholder={label} + disabled={disabled} /> ) @@ -347,22 +476,36 @@ const ObjectProperty = ({ return ( ) case 'text': return ( - + ) case 'material': return ( - ) case 'id': @@ -370,7 +513,11 @@ const ObjectProperty = ({ if (value) { return } else { - return n/a + return ( + + n/a + + ) } case 'object': switch (objectType) { @@ -405,7 +552,11 @@ const ObjectProperty = ({ ) default: - return n/a + return ( + + n/a + + ) } case 'density': @@ -455,10 +606,15 @@ ObjectProperty.propTypes = { formItemProps: PropTypes.object, required: PropTypes.bool, name: PropTypes.string, - label: PropTypes.string, + prefix: PropTypes.string, + suffix: PropTypes.string, + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, showLabel: PropTypes.bool, objectType: PropTypes.string, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + disabled: PropTypes.bool } export default ObjectProperty diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx index 4f0ffbd..42be76c 100644 --- a/src/components/Dashboard/common/ObjectTable.jsx +++ b/src/components/Dashboard/common/ObjectTable.jsx @@ -38,6 +38,7 @@ import XMarkIcon from '../../Icons/XMarkIcon' import CheckIcon from '../../Icons/CheckIcon' import { useNavigate } from 'react-router-dom' import QuestionCircleIcon from '../../Icons/QuestionCircleIcon' +import { AuthContext } from '../context/AuthContext' const logger = loglevel.getLogger('DasboardTable') logger.setLevel(config.logLevel) @@ -49,13 +50,14 @@ const ObjectTable = forwardRef( pageSize = 25, scrollHeight = 'calc(var(--unit-100vh) - 270px)', onDataChange, - authenticated, initialPage = 1, cards = false, - visibleColumns = {} + visibleColumns = {}, + masterFilter = {} }, ref ) => { + const { authenticated } = useContext(AuthContext) const { fetchTableData } = useContext(ApiServerContext) const isMobile = useMediaQuery({ maxWidth: 768 }) const navigate = useNavigate() @@ -107,7 +109,7 @@ const ObjectTable = forwardRef( const result = await fetchTableData(type, { page: pageNum, limit: pageSize, - filter, + filter: { ...filter, ...masterFilter }, sorter, onDataChange }) @@ -408,17 +410,15 @@ const ObjectTable = forwardRef( ) } } - // Add filter configuration if the property is filterable - if (isFilterable) { + // Add filter configuration if the property is filterable and not in masterFilter + if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) { columnConfig.filterDropdown = ({ setSelectedKeys, selectedKeys, @@ -606,7 +606,8 @@ ObjectTable.propTypes = { initialPage: PropTypes.number, cards: PropTypes.bool, cardRenderer: PropTypes.func, - visibleColumns: PropTypes.object + visibleColumns: PropTypes.object, + masterFilter: PropTypes.object } export default ObjectTable diff --git a/src/components/Dashboard/common/PropertyChanges.jsx b/src/components/Dashboard/common/PropertyChanges.jsx new file mode 100644 index 0000000..3051dc1 --- /dev/null +++ b/src/components/Dashboard/common/PropertyChanges.jsx @@ -0,0 +1,62 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Descriptions, Typography, Space } from 'antd' +import { getModelProperty } from '../../../database/ObjectModels' +import ObjectProperty from './ObjectProperty' +import ArrowRightIcon from '../../Icons/ArrowRightIcon' + +const { Text } = Typography + +const PropertyChanges = ({ type, value }) => { + if (!value || !value.new) { + return n/a + } + console.log('combined', { ...value?.old, ...value?.new }) + return ( + + {Object.keys({ ...value?.old, ...value?.new }).map((key) => { + console.log('tc', type, key) + var changeProperty = getModelProperty(type, key) + console.log('change prop', changeProperty) + + if (changeProperty?.type == 'object') { + changeProperty = { + ...changeProperty, + name: changeProperty.name + '._id', + type: 'id', + showHyperlink: true + } + } + return ( + + + + + + + + + + ) + })} + + ) +} + +PropertyChanges.propTypes = { + type: PropTypes.string.isRequired, + value: PropTypes.shape({ + old: PropTypes.object, + new: PropTypes.object + }) +} + +export default PropertyChanges diff --git a/src/components/Dashboard/common/UrlDisplay.jsx b/src/components/Dashboard/common/UrlDisplay.jsx index 926f1f7..a1bbe0c 100644 --- a/src/components/Dashboard/common/UrlDisplay.jsx +++ b/src/components/Dashboard/common/UrlDisplay.jsx @@ -23,12 +23,14 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => { ) : ( <> - {url} + + {url} + @@ -41,15 +41,15 @@ const ViewButton = ({ } ViewButton.propTypes = { - loading: PropTypes.bool, - properties: PropTypes.arrayOf( + disabled: PropTypes.bool, + items: PropTypes.arrayOf( PropTypes.shape({ key: PropTypes.string.isRequired, label: PropTypes.string.isRequired }) ), - collapseState: PropTypes.object, - updateCollapseState: PropTypes.func + visibleState: PropTypes.object, + updateVisibleState: PropTypes.func } export default ViewButton diff --git a/src/components/Dashboard/context/ApiServerContext.js b/src/components/Dashboard/context/ApiServerContext.js index 44499bb..ca511c2 100644 --- a/src/components/Dashboard/context/ApiServerContext.js +++ b/src/components/Dashboard/context/ApiServerContext.js @@ -331,14 +331,25 @@ const ApiServerProvider = ({ children }) => { fileLink.parentNode.removeChild(fileLink) } catch (error) { logger.error('Failed to download GCode file content:', error) + if (error.response) { - messageApi.error('Error downloading GCode file:', error.response.status) + if (error.response.status === 404) { + showError( + `The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`, + () => handleDownloadContent(id, type, fileName) + ) + } else { + showError( + `Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`, + () => handleDownloadContent(id, type, fileName) + ) + } } else { - messageApi.error( - 'An unexpected error occurred while downloading. Please try again later.' + showError( + 'An unexpected error occurred while downloading. Please check your connection and try again.', + () => handleDownloadContent(id, type, fileName) ) } - throw error } } @@ -378,6 +389,14 @@ const ApiServerProvider = ({ children }) => { centered maskClosable={true} footer={[ + , diff --git a/src/components/Icons/DownloadIcon.jsx b/src/components/Icons/DownloadIcon.jsx new file mode 100644 index 0000000..81c8ca7 --- /dev/null +++ b/src/components/Icons/DownloadIcon.jsx @@ -0,0 +1,7 @@ +import React from 'react' +import Icon from '@ant-design/icons' +import { ReactComponent as CustomIconSvg } from '../../assets/icons/downloadicon.min.svg' + +const DownloadIcon = (props) => + +export default DownloadIcon diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js index 0fb8e26..b986b24 100644 --- a/src/database/ObjectModels.js +++ b/src/database/ObjectModels.js @@ -77,6 +77,16 @@ export function getModelByName(name) { ) } +export function getModelProperty(name, property) { + const model = getModelByName(name) + + if (!model || !model.properties) { + return undefined + } + + return model.properties.find((prop) => prop.name == property) +} + export function getModelProperties(name, propertyList) { const model = getModelByName(name) @@ -132,3 +142,53 @@ export const getPropertyValue = (obj, path) => { return obj[path] } } + +export const evaluateVariable = (expression, data) => { + if (!expression) return false + + // Only treat as an expression if it starts and ends with () + const expr = expression.trim() + if (!(expr.startsWith('(') && expr.endsWith(')'))) return false + + // Remove the outer parentheses + const innerExpr = expr.slice(1, -1) + + // Helper to evaluate a single condition like 'foo == "bar"' or 'foo.bar == 42' or 'foo == true' + const evalCondition = (cond, data) => { + const match = cond.trim().match(/^([a-zA-Z0-9_.]+)\s*==\s*(.+)$/) + if (!match) return false + const [, path, valueRaw] = match + let value + let raw = valueRaw.trim() + // Check for quoted string + if ( + (raw.startsWith('"') && raw.endsWith('"')) || + (raw.startsWith("'") && raw.endsWith("'")) + ) { + value = raw.slice(1, -1) + } else if (raw === 'true') { + value = true + } else if (raw === 'false') { + value = false + } else if (!isNaN(Number(raw))) { + value = Number(raw) + } else { + value = raw + } + // Resolve nested property + const propValue = path + .split('.') + .reduce((acc, key) => (acc ? acc[key] : undefined), data) + return propValue === value + } + + // Split by '||' first (lowest precedence) + const orParts = innerExpr.split(/\|\|/) + for (let orPart of orParts) { + // Each orPart may have '&&' (higher precedence) + const andParts = orPart.split(/&&/) + const andResult = andParts.every((andPart) => evalCondition(andPart, data)) + if (andResult) return true // If any OR group is true, return true + } + return false // None of the OR groups were true +} diff --git a/src/database/models/AuditLog.js b/src/database/models/AuditLog.js index ac868dc..edab88b 100644 --- a/src/database/models/AuditLog.js +++ b/src/database/models/AuditLog.js @@ -1,20 +1,89 @@ import AuditLogIcon from '../../components/Icons/AuditLogIcon' -import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' export const AuditLog = { - name: 'auditlog', + name: 'auditLog', label: 'Audit Log', prefix: 'ADL', icon: AuditLogIcon, - actions: [ + actions: [], + columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'], + filters: ['_id', 'owner._id', 'parent._id'], + sorters: [], + properties: [ { - name: 'info', - label: 'Info', - default: true, - row: true, - icon: InfoCircleIcon, - url: (_id) => `/dashboard/management/auditlogs/info?auditLogId=${_id}` + name: '_id', + label: 'ID', + type: 'id', + objectType: 'auditLog', + columnFixed: 'left', + value: null, + showCopy: true + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + value: null, + readOnly: true + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + value: null, + readOnly: true + }, + { + name: 'owner', + label: 'Owner', + type: 'object', + objectType: (objectData) => { + return objectData.ownerType + }, + columnFixed: 'left', + value: null, + showCopy: true + }, + { + name: 'owner._id', + label: 'Owner ID', + type: 'id', + objectType: (objectData) => { + return objectData.ownerType + }, + columnFixed: 'left', + showHyperlink: true, + showCopy: true + }, + { + name: 'parent', + label: 'Parent', + type: 'object', + objectType: (objectData) => { + return objectData.parentType + }, + value: null, + showCopy: true + }, + { + name: 'parent._id', + label: 'Parent ID', + type: 'id', + objectType: (objectData) => { + return objectData.parentType + }, + showHyperlink: true, + showCopy: true + }, + { + name: 'changes', + label: 'Changes', + columnWidth: 500, + type: 'propertyChanges', + objectType: (objectData) => { + return objectData.parentType + }, + showCopy: true } - ], - url: () => `#` + ] } diff --git a/src/database/models/Filament.js b/src/database/models/Filament.js index 1dae231..310bb08 100644 --- a/src/database/models/Filament.js +++ b/src/database/models/Filament.js @@ -1,5 +1,7 @@ +import EditIcon from '../../components/Icons/EditIcon' import FilamentIcon from '../../components/Icons/FilamentIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' export const Filament = { name: 'filament', @@ -14,6 +16,21 @@ export const Filament = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/filaments/info?filamentId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/filaments/info?filamentId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/management/filaments/info?filamentId=${_id}&action=edit` } ], columns: [ diff --git a/src/database/models/GCodeFile.js b/src/database/models/GCodeFile.js index 96b526b..587d5ff 100644 --- a/src/database/models/GCodeFile.js +++ b/src/database/models/GCodeFile.js @@ -1,5 +1,8 @@ +import DownloadIcon from '../../components/Icons/DownloadIcon' +import EditIcon from '../../components/Icons/EditIcon' import GCodeFileIcon from '../../components/Icons/GCodeFileIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' export const GCodeFile = { name: 'gcodeFile', @@ -15,12 +18,28 @@ export const GCodeFile = { icon: InfoCircleIcon, url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}` }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=reload` + }, { name: 'download', label: 'Download', row: true, + icon: DownloadIcon, url: (_id) => `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=download` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit` } ], diff --git a/src/database/models/Job.js b/src/database/models/Job.js index 002577f..bac89b3 100644 --- a/src/database/models/Job.js +++ b/src/database/models/Job.js @@ -1,5 +1,7 @@ import JobIcon from '../../components/Icons/JobIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' +import EditIcon from '../../components/Icons/EditIcon' export const Job = { name: 'job', @@ -14,6 +16,19 @@ export const Job = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=edit` } ], columns: [ @@ -25,7 +40,7 @@ export const Job = { 'createdAt' ], filters: ['state', '_id', 'gcodeFile._id', 'quantity'], - sorters: ['createdAt', 'state', 'quantity', '_id'], + sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'], properties: [ { name: '_id', diff --git a/src/database/models/NoteType.js b/src/database/models/NoteType.js index dc1cc05..caf4f4e 100644 --- a/src/database/models/NoteType.js +++ b/src/database/models/NoteType.js @@ -1,5 +1,7 @@ import NoteTypeIcon from '../../components/Icons/NoteTypeIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' +import EditIcon from '../../components/Icons/EditIcon' export const NoteType = { name: 'noteType', @@ -14,6 +16,21 @@ export const NoteType = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/notetypes/info?noteTypeId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/notetypes/info?noteTypeId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit` } ], columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'], diff --git a/src/database/models/Part.js b/src/database/models/Part.js index 1652474..f59a242 100644 --- a/src/database/models/Part.js +++ b/src/database/models/Part.js @@ -1,5 +1,8 @@ +import DownloadIcon from '../../components/Icons/DownloadIcon' +import EditIcon from '../../components/Icons/EditIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import PartIcon from '../../components/Icons/PartIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' export const Part = { name: 'part', @@ -14,10 +17,39 @@ export const Part = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/parts/info?partId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/parts/info?partId=${_id}&action=reload` + }, + { + name: 'download', + label: 'Download', + row: true, + icon: DownloadIcon, + url: (_id) => + `/dashboard/management/parts/info?partId=${_id}&action=download` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => `/dashboard/management/parts/info?partId=${_id}&action=edit` } ], - columns: ['name', '_id', 'product', 'product._id', 'createdAt'], - filters: ['name', '_id', 'product', 'product._id'], + columns: [ + 'name', + '_id', + 'product', + 'product._id', + 'globalPricing', + 'createdAt' + ], + filters: ['name', '_id', 'product', 'product._id', 'globalPricing'], sorters: ['name', 'email', 'role', 'createdAt', '_id'], properties: [ { @@ -25,8 +57,9 @@ export const Part = { label: 'ID', columnFixed: 'left', type: 'id', - objectType: 'user', - showCopy: true + objectType: 'part', + showCopy: true, + readOnly: true }, { name: 'createdAt', @@ -52,13 +85,58 @@ export const Part = { name: 'product', label: 'Product', type: 'object', + required: true, objectType: 'product' }, { name: 'product._id', label: 'Product ID', type: 'id', + readOnly: true, + showHyperlink: true, objectType: 'product' + }, + { + name: 'globalPricing', + label: 'Global Price', + columnWidth: 150, + required: true, + type: 'bool' + }, + { + name: 'priceMode', + label: 'Price Mode', + type: 'priceMode', + disabled: (objectData) => { + return objectData.globalPricing == true + } + }, + { + name: 'margin', + label: 'Margin', + type: 'number', + disabled: (objectData) => { + return ( + objectData.globalPricing == true || objectData.priceMode == 'amount' + ) + }, + suffix: '%', + min: 0, + max: 100, + step: 0.01 + }, + { + name: 'amount', + label: 'Amount', + disabled: (objectData) => { + return ( + objectData.globalPricing == true || objectData.priceMode == 'margin' + ) + }, + type: 'number', + prefix: '£', + min: 0, + step: 0.1 } ] } diff --git a/src/database/models/Printer.js b/src/database/models/Printer.js index d384225..2fd69bb 100644 --- a/src/database/models/Printer.js +++ b/src/database/models/Printer.js @@ -1,5 +1,8 @@ import PrinterIcon from '../../components/Icons/PrinterIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' +import EditIcon from '../../components/Icons/EditIcon' +import PlayCircleIcon from '../../components/Icons/PlayCircleIcon' export const Printer = { name: 'printer', @@ -14,12 +17,34 @@ export const Printer = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/production/printers/info?printerId=${_id}` + }, + + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/production/printers/info?printerId=${_id}&action=reload` + }, + { + name: 'control', + label: 'Control', + row: true, + icon: PlayCircleIcon, + url: (_id) => `/dashboard/production/printers/control?printerId=${_id}` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/production/printers/info?printerId=${_id}&action=edit` } ], - url: (id) => `/dashboard/production/printers/info?printerId=${id}`, columns: ['name', '_id', 'state', 'tags', 'connectedAt'], filters: ['name', '_id', 'state', 'tags'], - sorters: ['name', 'state', 'connectedAt', '_id'], + sorters: ['name', 'state', 'connectedAt'], properties: [ { name: '_id', diff --git a/src/database/models/Product.js b/src/database/models/Product.js index ac0c75a..3f28ed8 100644 --- a/src/database/models/Product.js +++ b/src/database/models/Product.js @@ -1,5 +1,7 @@ import ProductIcon from '../../components/Icons/ProductIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' +import EditIcon from '../../components/Icons/EditIcon' export const Product = { name: 'product', @@ -14,16 +16,43 @@ export const Product = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/products/info?productId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/products/info?productId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/management/products/info?productId=${_id}&action=edit` } ], - url: (id) => `/dashboard/management/products/info?productId=${id}`, + columns: [ + '_id', + 'name', + 'tags', + 'vendor', + 'vendor._id', + 'price', + 'createdAt', + 'updatedAt' + ], + filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'], + sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'], properties: [ { name: '_id', label: 'ID', type: 'id', - objectType: 'printer', - showCopy: true + objectType: 'product', + showCopy: true, + readOnly: true }, { name: 'createdAt', @@ -42,6 +71,59 @@ export const Product = { label: 'Updated At', type: 'dateTime', readOnly: true + }, + { + name: 'vendor', + label: 'Vendor', + required: true, + type: 'object', + objectType: 'vendor' + }, + { + name: 'vendor._id', + label: 'Vendor ID', + readOnly: true, + type: 'id', + showHyperlink: true, + objectType: 'vendor' + }, + { + name: 'version', + label: 'Version', + type: 'text' + }, + { + name: 'tags', + label: 'Tags', + type: 'tags' + }, + { + name: 'priceMode', + label: 'Price Mode', + type: 'priceMode' + }, + { + name: 'margin', + label: 'Margin', + type: 'number', + disabled: (objectData) => { + return objectData.priceMode == 'amount' + }, + suffix: '%', + min: 0, + max: 100, + step: 0.01 + }, + { + name: 'amount', + label: 'Amount', + disabled: (objectData) => { + return objectData.priceMode == 'margin' + }, + type: 'number', + prefix: '£', + min: 0, + step: 0.1 } ] } diff --git a/src/database/models/User.js b/src/database/models/User.js index d72f529..0a451c5 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -1,5 +1,6 @@ import PersonIcon from '../../components/Icons/PersonIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' export const User = { name: 'user', @@ -14,9 +15,15 @@ export const User = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/users/info?userId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/users/info?userId=${_id}&action=reload` } ], - url: (id) => `/dashboard/management/users/info?userId=${id}`, columns: ['name', '_id', 'username', 'email', 'role', 'createdAt'], filters: ['name', '_id', 'email', 'role'], sorters: ['name', 'email', 'role', 'createdAt', '_id'], diff --git a/src/database/models/Vendor.js b/src/database/models/Vendor.js index 9ce6131..b38c2f0 100644 --- a/src/database/models/Vendor.js +++ b/src/database/models/Vendor.js @@ -1,5 +1,7 @@ import VendorIcon from '../../components/Icons/VendorIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import ReloadIcon from '../../components/Icons/ReloadIcon' export const Vendor = { name: 'vendor', @@ -14,10 +16,25 @@ export const Vendor = { row: true, icon: InfoCircleIcon, url: (_id) => `/dashboard/management/vendors/info?vendorId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/management/vendors/info?vendorId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/management/vendors/info?vendorId=${_id}&action=edit` } ], url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`, - columns: ['name', '_id', 'country', 'email', 'createdAt'], + columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'], filters: ['name', '_id', 'country', 'email'], sorters: ['name', 'country', 'email', 'createdAt', '_id'], properties: [