diff --git a/Jenkinsfile b/Jenkinsfile
index 6b5cf9c..04a1a07 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -20,7 +20,7 @@ def deploy() {
stage('Build (Ubuntu)') {
nodejs(nodeJSInstallationName: 'Node23') {
- sh 'NODE_ENV=production pnpm build:cloudflare'
+ sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production pnpm build:cloudflare"
}
}
@@ -70,9 +70,9 @@ def buildOnLabel(label, buildCommand) {
stage("Build (${label})") {
nodejs(nodeJSInstallationName: 'Node23') {
if (isUnix()) {
- sh "NODE_ENV=production ${buildCommand}"
+ sh "VITE_BUILD_NUMBER=${env.BUILD_NUMBER} NODE_ENV=production ${buildCommand}"
} else {
- bat "set NODE_ENV=production && ${buildCommand}"
+ bat "set VITE_BUILD_NUMBER=${env.BUILD_NUMBER} && set NODE_ENV=production && ${buildCommand}"
}
}
}
diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index f7ff927..96d4d41 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -596,3 +596,9 @@ body {
.ant-table-wrapper .ant-table-filter-column {
align-items: center;
}
+
+span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
+ width: 50px;
+ min-width: 0;
+ height: 20px;
+}
diff --git a/public/appupdate.js b/public/appupdate.js
new file mode 100644
index 0000000..e69de29
diff --git a/public/mainWindow.js b/public/mainWindow.js
index 28e5929..39f6d66 100644
--- a/public/mainWindow.js
+++ b/public/mainWindow.js
@@ -206,6 +206,10 @@ export function getWindow() {
return win
}
+export function getElectronVersion() {
+ return process.versions.electron
+}
+
export function setupMainWindowIPC() {
// IPC handler to get window state
ipcMain.handle('window-state', () => {
@@ -267,6 +271,8 @@ export function setupMainWindowIPC() {
applyApplicationMenu()
return true
})
+
+ ipcMain.handle('electron-version', () => getElectronVersion())
}
export function setupMainWindowAppEvents(app) {
diff --git a/src/components/Dashboard/Management/About.jsx b/src/components/Dashboard/Management/About.jsx
new file mode 100644
index 0000000..78f95b3
--- /dev/null
+++ b/src/components/Dashboard/Management/About.jsx
@@ -0,0 +1,180 @@
+import { useContext, useEffect, useState } from 'react'
+import {
+ Flex,
+ Typography,
+ Button,
+ Dropdown,
+ Skeleton,
+ Tag,
+ Divider
+} from 'antd'
+import useCollapseState from '../hooks/useCollapseState'
+import InfoCollapse from '../common/InfoCollapse'
+import InfoCircleIcon from '../../Icons/InfoCircleIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import DeveloperIcon from '../../Icons/DeveloperIcon'
+import { version as appVersion } from '../../../../package.json'
+import { ApiServerContext } from '../context/ApiServerContext'
+import { AuthContext } from '../context/AuthContext'
+import { ElectronContext } from '../context/ElectronContext'
+import { useMediaQuery } from 'react-responsive'
+const { Title, Text, Link } = Typography
+
+const About = () => {
+ const [collapseState, updateCollapseState] = useCollapseState('About', {
+ updater: true
+ })
+ const { token } = useContext(AuthContext)
+ const actions = [
+ {
+ label: 'Check for Updates',
+ icon: ,
+ onClick: () => {
+ console.log('Check for Updates')
+ }
+ }
+ ]
+
+ const buildNumber = import.meta.env.VITE_BUILD_NUMBER
+ ? 'b' + import.meta.env.VITE_BUILD_NUMBER
+ : 'dev'
+ const developmentMode = import.meta.env.MODE === 'development'
+
+ const { fetchApiServerVersion, fetchWsServerVersion } =
+ useContext(ApiServerContext)
+ const { isElectron, getElectronVersion } = useContext(ElectronContext)
+ const isMobile = useMediaQuery({ maxWidth: 768 })
+
+ useEffect(() => {
+ if (token) {
+ fetchApiServerVersion().then((version) => {
+ setApiServerVersion(version)
+ })
+ fetchWsServerVersion().then((version) => {
+ setWsServerVersion(version)
+ })
+ }
+ }, [fetchApiServerVersion, fetchWsServerVersion, token])
+
+ useEffect(() => {
+ if (!isElectron) return
+
+ getElectronVersion()
+ .then((version) => {
+ setElectronVersion(version || 'unknown')
+ })
+ .catch(() => {
+ setElectronVersion('unknown')
+ })
+ }, [getElectronVersion, isElectron])
+
+ const [apiServerVersion, setApiServerVersion] = useState(null)
+ const [wsServerVersion, setWsServerVersion] = useState(null)
+ const [electronVersion, setElectronVersion] = useState(null)
+
+ const apiServerVersionText = apiServerVersion ? (
+
+ {`v${apiServerVersion.version}-${apiServerVersion.buildNumber}`}
+
+ ) : (
+
+ )
+ const wsServerVersionText = wsServerVersion ? (
+ {`v${wsServerVersion.version}-${wsServerVersion.buildNumber}`}
+ ) : (
+
+ )
+ const electronVersionText = electronVersion ? (
+ {`v${electronVersion}`}
+ ) : (
+
+ )
+
+ return (
+
+
+
+
+
+
+
+
+ }
+ canCollapse={false}
+ active={collapseState.purchaseOrderStats}
+ onToggle={(isActive) =>
+ updateCollapseState('purchaseOrderStats', isActive)
+ }
+ className='no-t-padding-collapse'
+ collapseKey='purchaseOrderStats'
+ >
+
+
+
+
+
+ Farm Control
+
+ {developmentMode && !isMobile && (
+ }
+ >
+ Development
+
+ )}
+
+
+ 3D Printer ERP and Control Software.
+
+
+
+ User Interface:{' '}
+
+ v{appVersion}-{buildNumber}
+
+
+ {isElectron && (
+
+ Electron: {electronVersionText}
+
+ )}
+
+ REST API: {apiServerVersionText}
+
+ Web Socket: {wsServerVersionText}
+
+
+
+ {developmentMode && isMobile && (
+ }
+ >
+ Development
+
+ )}
+
+ Jenkins
+
+
+ GitHub
+
+
+
+
+
+
+
+ )
+}
+
+export default About
diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
index 73e96b5..b042a15 100644
--- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx
+++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
@@ -17,7 +17,8 @@ const breadcrumbNameMap = {
info: 'Info',
design: 'Design',
control: 'Control',
- preview: 'Preview'
+ preview: 'Preview',
+ about: 'About'
}
const mainSections = ['production', 'inventory', 'management', 'developer']
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index 8132d9f..2b1036c 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -744,7 +744,7 @@ const ApiServerProvider = ({ children }) => {
[offLockEvent]
)
- const showError = (error, callback = null) => {
+ const showError = useCallback((error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') {
setUnauthenticated()
@@ -757,7 +757,7 @@ const ApiServerProvider = ({ children }) => {
setErrorModalContent(content)
setRetryCallback(() => callback)
setShowErrorModal(true)
- }
+ }, [setUnauthenticated])
const handleRetry = () => {
setShowErrorModal(false)
@@ -1678,15 +1678,55 @@ const ApiServerProvider = ({ children }) => {
return response.data
}, [])
- const fetchAppUpdateBranches = useCallback(async () => {
+ const fetchApiServerVersion = useCallback(async () => {
try {
- const response = await axios.get(`${config.backendUrl}/appupdate/branches`, {
+ const response = await axios.get(`${config.backendUrl}/server/version`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
- return Array.isArray(response.data?.branches) ? response.data.branches : []
+ return response.data
+ } catch (err) {
+ console.error(err)
+ showError(err, () => {
+ fetchApiServerVersion()
+ })
+ return null
+ }
+ }, [token, showError])
+
+ const fetchWsServerVersion = useCallback(async () => {
+ try {
+ if (socketRef.current && socketRef.current.connected) {
+ return await new Promise((resolve) => {
+ socketRef.current.emit('getServerVersion', {}, resolve)
+ })
+ }
+ return null
+ } catch (err) {
+ console.error(err)
+ showError(err, () => {
+ fetchWsServerVersion()
+ })
+ return null
+ }
+ }, [showError])
+
+ const fetchAppUpdateBranches = useCallback(async () => {
+ try {
+ const response = await axios.get(
+ `${config.backendUrl}/appupdate/branches`,
+ {
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Bearer ${token}`
+ }
+ }
+ )
+ return Array.isArray(response.data?.branches)
+ ? response.data.branches
+ : []
} catch (err) {
console.error(err)
showError(err, () => {
@@ -1694,18 +1734,21 @@ const ApiServerProvider = ({ children }) => {
})
return []
}
- }, [token])
+ }, [token, showError])
const fetchAppUpdateCurrent = useCallback(
async (branch) => {
try {
- const response = await axios.get(`${config.backendUrl}/appupdate/current`, {
- params: { branch },
- headers: {
- Accept: 'application/json',
- Authorization: `Bearer ${token}`
+ const response = await axios.get(
+ `${config.backendUrl}/appupdate/current`,
+ {
+ params: { branch },
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Bearer ${token}`
+ }
}
- })
+ )
return response.data
} catch (err) {
console.error(err)
@@ -1715,7 +1758,7 @@ const ApiServerProvider = ({ children }) => {
return null
}
},
- [token]
+ [token, showError]
)
const flushFile = async (id) => {
@@ -1816,7 +1859,9 @@ const ApiServerProvider = ({ children }) => {
completeAppLaunchSession,
getAppLaunchSession,
fetchAppUpdateBranches,
- fetchAppUpdateCurrent
+ fetchAppUpdateCurrent,
+ fetchWsServerVersion,
+ fetchApiServerVersion
}}
>
{contextHolder}
diff --git a/src/components/Dashboard/context/ElectronContext.jsx b/src/components/Dashboard/context/ElectronContext.jsx
index c58328b..31e7ff7 100644
--- a/src/components/Dashboard/context/ElectronContext.jsx
+++ b/src/components/Dashboard/context/ElectronContext.jsx
@@ -152,6 +152,11 @@ const ElectronProvider = ({ children }) => {
[electronAvailable]
)
+ const getElectronVersion = useCallback(async () => {
+ if (!electronAvailable || !ipcRenderer) return null
+ return await ipcRenderer.invoke('electron-version')
+ }, [electronAvailable])
+
return (
{
getToken,
setToken,
resizeSpotlightWindow,
- setSidebarViewMenu
+ setSidebarViewMenu,
+ getElectronVersion
}}
>
{children}
diff --git a/src/components/Icons/sidebarIconMap.jsx b/src/components/Icons/sidebarIconMap.jsx
index a641cef..6f35de5 100644
--- a/src/components/Icons/sidebarIconMap.jsx
+++ b/src/components/Icons/sidebarIconMap.jsx
@@ -48,6 +48,7 @@ import CourierServiceIcon from './CourierServiceIcon'
import TaxRateIcon from './TaxRateIcon'
import TaxRecordIcon from './TaxRecordIcon'
import AppPasswordIcon from './AppPasswordIcon'
+import InfoCircleIcon from './InfoCircleIcon'
const toEmoji = (emoji) => {emoji}
@@ -102,6 +103,7 @@ const sidebarIconMap = {
taxRate: ,
taxRecord: ,
appPassword: ,
+ infoCircle: ,
sessionStorage: toEmoji('🗃️'),
authDebug: toEmoji('🔐'),
apiDebug: toEmoji('🌐')
diff --git a/src/database/sidebars/management.js b/src/database/sidebars/management.js
index d0d2e5d..0b53c3c 100644
--- a/src/database/sidebars/management.js
+++ b/src/database/sidebars/management.js
@@ -168,6 +168,12 @@ const managementSidebarItems = [
label: 'Developer',
path: '/dashboard/developer/sessionstorage',
devOnly: true
+ },
+ {
+ key: 'about',
+ iconKey: 'infoCircle',
+ label: 'About',
+ path: '/dashboard/management/about'
}
]
diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx
index dcb7bab..e7c348a 100644
--- a/src/routes/ManagementRoutes.jsx
+++ b/src/routes/ManagementRoutes.jsx
@@ -1,55 +1,155 @@
import { lazy } from 'react'
import { Route } from 'react-router-dom'
-const Filaments = lazy(() => import('../components/Dashboard/Management/Filaments'))
-const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx'))
-const FilamentSkus = lazy(() => import('../components/Dashboard/Management/FilamentSkus.jsx'))
-const FilamentSkuInfo = lazy(() => import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx'))
+const Filaments = lazy(
+ () => import('../components/Dashboard/Management/Filaments')
+)
+const FilamentInfo = lazy(
+ () => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx')
+)
+const FilamentSkus = lazy(
+ () => import('../components/Dashboard/Management/FilamentSkus.jsx')
+)
+const FilamentSkuInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx')
+)
const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx'))
-const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx'))
-const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx'))
-const PartSkuInfo = lazy(() => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx'))
-const Products = lazy(() => import('../components/Dashboard/Management/Products.jsx'))
-const ProductInfo = lazy(() => import('../components/Dashboard/Management/Products/ProductInfo.jsx'))
-const ProductCategories = lazy(() => import('../components/Dashboard/Management/ProductCategories.jsx'))
-const ProductCategoryInfo = lazy(() => import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx'))
-const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx'))
-const ProductSkuInfo = lazy(() => import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx'))
+const PartInfo = lazy(
+ () => import('../components/Dashboard/Management/Parts/PartInfo.jsx')
+)
+const PartSkus = lazy(
+ () => import('../components/Dashboard/Management/PartSkus.jsx')
+)
+const PartSkuInfo = lazy(
+ () => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx')
+)
+const Products = lazy(
+ () => import('../components/Dashboard/Management/Products.jsx')
+)
+const ProductInfo = lazy(
+ () => import('../components/Dashboard/Management/Products/ProductInfo.jsx')
+)
+const ProductCategories = lazy(
+ () => import('../components/Dashboard/Management/ProductCategories.jsx')
+)
+const ProductCategoryInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx')
+)
+const ProductSkus = lazy(
+ () => import('../components/Dashboard/Management/ProductSkus.jsx')
+)
+const ProductSkuInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx')
+)
const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors'))
-const VendorInfo = lazy(() => import('../components/Dashboard/Management/Vendors/VendorInfo'))
-const Materials = lazy(() => import('../components/Dashboard/Management/Materials'))
-const MaterialInfo = lazy(() => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx'))
-const Couriers = lazy(() => import('../components/Dashboard/Management/Couriers'))
-const CourierInfo = lazy(() => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx'))
-const CourierServices = lazy(() => import('../components/Dashboard/Management/CourierServices'))
-const CourierServiceInfo = lazy(() => import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx'))
-const Settings = lazy(() => import('../components/Dashboard/Management/Settings'))
-const AppUpdate = lazy(() => import('../components/Dashboard/Management/AppUpdate'))
-const AuditLogs = lazy(() => import('../components/Dashboard/Management/AuditLogs.jsx'))
-const NoteTypes = lazy(() => import('../components/Dashboard/Management/NoteTypes.jsx'))
-const NoteTypeInfo = lazy(() => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx'))
-const NoteInfo = lazy(() => import('../components/Dashboard/Management/Notes/NoteInfo.jsx'))
+const VendorInfo = lazy(
+ () => import('../components/Dashboard/Management/Vendors/VendorInfo')
+)
+const Materials = lazy(
+ () => import('../components/Dashboard/Management/Materials')
+)
+const MaterialInfo = lazy(
+ () => import('../components/Dashboard/Management/Materials/MaterialInfo.jsx')
+)
+const Couriers = lazy(
+ () => import('../components/Dashboard/Management/Couriers')
+)
+const CourierInfo = lazy(
+ () => import('../components/Dashboard/Management/Couriers/CourierInfo.jsx')
+)
+const CourierServices = lazy(
+ () => import('../components/Dashboard/Management/CourierServices')
+)
+const CourierServiceInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx')
+)
+const Settings = lazy(
+ () => import('../components/Dashboard/Management/Settings')
+)
+const AppUpdate = lazy(
+ () => import('../components/Dashboard/Management/AppUpdate')
+)
+const AuditLogs = lazy(
+ () => import('../components/Dashboard/Management/AuditLogs.jsx')
+)
+const NoteTypes = lazy(
+ () => import('../components/Dashboard/Management/NoteTypes.jsx')
+)
+const NoteTypeInfo = lazy(
+ () => import('../components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx')
+)
+const NoteInfo = lazy(
+ () => import('../components/Dashboard/Management/Notes/NoteInfo.jsx')
+)
const Users = lazy(() => import('../components/Dashboard/Management/Users.jsx'))
-const UserInfo = lazy(() => import('../components/Dashboard/Management/Users/UserInfo.jsx'))
-const AppPasswords = lazy(() => import('../components/Dashboard/Management/AppPasswords.jsx'))
-const AppPasswordInfo = lazy(() => import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx'))
+const UserInfo = lazy(
+ () => import('../components/Dashboard/Management/Users/UserInfo.jsx')
+)
+const AppPasswords = lazy(
+ () => import('../components/Dashboard/Management/AppPasswords.jsx')
+)
+const AppPasswordInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/AppPasswords/AppPasswordInfo.jsx')
+)
const Hosts = lazy(() => import('../components/Dashboard/Management/Hosts.jsx'))
-const HostInfo = lazy(() => import('../components/Dashboard/Management/Hosts/HostInfo.jsx'))
-const DocumentSizes = lazy(() => import('../components/Dashboard/Management/DocumentSizes.jsx'))
-const DocumentSizeInfo = lazy(() => import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx'))
-const DocumentTemplates = lazy(() => import('../components/Dashboard/Management/DocumentTemplates.jsx'))
-const DocumentTemplateInfo = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx'))
-const DocumentPrinters = lazy(() => import('../components/Dashboard/Management/DocumentPrinters.jsx'))
-const DocumentPrinterInfo = lazy(() => import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx'))
-const DocumentJobs = lazy(() => import('../components/Dashboard/Management/DocumentJobs.jsx'))
-const DocumentJobInfo = lazy(() => import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx'))
-const DocumentTemplateDesign = lazy(() => import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx'))
+const HostInfo = lazy(
+ () => import('../components/Dashboard/Management/Hosts/HostInfo.jsx')
+)
+const DocumentSizes = lazy(
+ () => import('../components/Dashboard/Management/DocumentSizes.jsx')
+)
+const DocumentSizeInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/DocumentSizes/DocumentSizeInfo.jsx')
+)
+const DocumentTemplates = lazy(
+ () => import('../components/Dashboard/Management/DocumentTemplates.jsx')
+)
+const DocumentTemplateInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo.jsx')
+)
+const DocumentPrinters = lazy(
+ () => import('../components/Dashboard/Management/DocumentPrinters.jsx')
+)
+const DocumentPrinterInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/DocumentPrinters/DocumentPrinterInfo.jsx')
+)
+const DocumentJobs = lazy(
+ () => import('../components/Dashboard/Management/DocumentJobs.jsx')
+)
+const DocumentJobInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/DocumentJobs/DocumentJobInfo.jsx')
+)
+const DocumentTemplateDesign = lazy(
+ () =>
+ import('../components/Dashboard/Management/DocumentTemplates/DocumentTemplateDesign.jsx')
+)
const Files = lazy(() => import('../components/Dashboard/Management/Files.jsx'))
-const FileInfo = lazy(() => import('../components/Dashboard/Management/Files/FileInfo.jsx'))
-const TaxRates = lazy(() => import('../components/Dashboard/Management/TaxRates.jsx'))
-const TaxRateInfo = lazy(() => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx'))
-const TaxRecords = lazy(() => import('../components/Dashboard/Management/TaxRecords.jsx'))
-const TaxRecordInfo = lazy(() => import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx'))
+const FileInfo = lazy(
+ () => import('../components/Dashboard/Management/Files/FileInfo.jsx')
+)
+const TaxRates = lazy(
+ () => import('../components/Dashboard/Management/TaxRates.jsx')
+)
+const TaxRateInfo = lazy(
+ () => import('../components/Dashboard/Management/TaxRates/TaxRateInfo.jsx')
+)
+const TaxRecords = lazy(
+ () => import('../components/Dashboard/Management/TaxRecords.jsx')
+)
+const TaxRecordInfo = lazy(
+ () =>
+ import('../components/Dashboard/Management/TaxRecords/TaxRecordInfo.jsx')
+)
+const About = lazy(() => import('../components/Dashboard/Management/About.jsx'))
const ManagementRoutes = [
} />,
@@ -58,7 +158,11 @@ const ManagementRoutes = [
path='management/filaments/info'
element={}
/>,
- } />,
+ }
+ />,
}
/>,
- } />,
+ }
+ />,
,
} />,
} />,
+ } />,
} />,
} />,