Finished core functionality.

This commit is contained in:
2022-09-12 18:19:57 -07:00
parent bbff674b62
commit 6fe980ae6e
11 changed files with 608 additions and 194 deletions

View File

@@ -30,7 +30,7 @@ if (Meteor.isServer) {
Meteor.methods({
/**
* Collects historical data on asset assignments.
* @param params An object with a single attribute. The attribute must be one of: assetId, serial, staffId, or studentId. It will find all Assignment data for the given attribute value.
* @param params An object with a single attribute. The attribute must be one of: email, deviceId, assetId, serial, staffId, or studentId. It will find all Assignment data for the given attribute value.
* @returns {any} Array of Asset Assignment History objects.
*/
'AssetAssignmentHistory.get'(params) {
@@ -41,9 +41,24 @@ if (Meteor.isServer) {
if(params.staffId) check(params.staffId, String)
if(params.assetId) check(params.assetId, String)
if(params.serial) check(params.serial, String)
if(params.deviceId) check(params.deviceId, String)
if(params.email) check(params.email, String)
if(params.email) {
let person = Students.findOne({email: params.email})
if(person) params.studentId = person._id;
else {
person = Staff.findOne({email: params.email})
if(person) params.staffId = person._id;
// else throw new Meteor.Error("Could not find a student or staff member with the given email.")
}
}
if(params.serial) query.serial = params.serial;
else if(params.assetId) query.assetId = params.assetId;
else if(params.deviceId) query.deviceId = params.deviceId;
else if(params.studentId) {
query.assigneeId = params.studentId
query.assigneeType = "Student"
@@ -59,11 +74,48 @@ if (Meteor.isServer) {
if(query) {
//Sort by the last time the record was updated from most to least recent.
let result = AssetAssignmentHistory.find(query, {sort: {endDate: -1}}).fetch();
let assets = [];
// Get the current assignment for the device or person.
if(query.assetId || query.deviceId || query.serial) {
let asset = Assets.findOne(query)
if(asset) assets = [asset]
}
else {
// Find the assets assigned to the person.
assets = Assets.find({assigneeId: params.studentId ? params.studentId : params.staffId}).fetch()
}
// Prepend a partial assignment history record to the list. We want to show active assignments in the results.
for(let asset of assets) {
if(asset && asset.assigneeId) {
let assetType = AssetTypes.findOne(asset.assetTypeId)
let current = {
_id: 0,
assetKey: asset._id,
assetId: asset.assetId,
serial: asset.serial,
assetTypeName: assetType.name,
assigneeType: asset.assigneeType,
assigneeId: asset.assigneeId,
startDate: asset.assignmentDate,
startCondition: asset.condition,
startConditionDetails: asset.conditionDetails
}
result = [current, ...result]
}
}
//Add some additional data to the records.
for(let next of result) {
if(next.serial) {
next.asset = Assets.findOne({serial: next.serial});
// console.log(next)
if(next.assetKey) {
next.asset = Assets.findOne({_id: next.assetKey})
}
else if(next.assetId) {
next.asset = Assets.findOne({assetId: next.assetId});
}
if(next.asset) {

View File

@@ -9,6 +9,7 @@ import {Page} from './Page'
import Assignments from './pages/Assignments'
import Assets from './pages/Assets'
import History from './pages/History'
import Search from './pages/Search'
import Users from './pages/Users'
import Admin from './pages/Admin'
@@ -81,28 +82,45 @@ export const App = () => {
<ThemeProvider theme={appTheme}>
<BrowserRouter>
<Routes>
<Route path="/" element={<Page>
<div className="container">
<div className="row">
TODO: Some statistics and such.
<Route path="/" element={
<Page>
<div className="container">
<div className="row">
TODO: Some statistics and such.
</div>
</div>
</div>
</Page>}/>
<Route path="/assignments/*" element={<Page>
{canManageLaptops && <Assignments/>}
</Page>}/>
<Route path="/assets/*" element={<Page>
{isAdmin && <Assets/>}
</Page>}/>
<Route path="/admin/*" element={<Page>
{isAdmin && <Admin/>}
</Page>}/>
<Route path="/history/*" element={<Page>
</Page>}
/>
<Route path="/assignments/*" element={
<Page>
{canManageLaptops && <Assignments/>}
</Page>}
/>
<Route path="/assets/*" element={
<Page>
{isAdmin && <Assets/>}
</Page>}
/>
<Route path="/admin/*" element={
<Page>
{isAdmin && <Admin/>}
</Page>}
/>
<Route path="/history/*" element={
<Page>
{canManageLaptops && <History/>}
</Page>}/>
<Route path="/users/*" element={<Page>
{isAdmin && <Users/>}
</Page>}/>
</Page>}
/>
<Route path="/search" element={
<Page>
{canManageLaptops && <Search/>}
</Page>}
/>
<Route path="/users/*" element={
<Page>
{isAdmin && <Users/>}
</Page>}
/>
</Routes>
</BrowserRouter>
</ThemeProvider>

View File

@@ -9,9 +9,11 @@ import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import {Assets, conditions} from "/imports/api/assets";
import {AssetTypes} from "/imports/api/asset-types";
import {Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Grid, Box} from "@mui/material";
const cssEditorField = {
margin: '0.6rem 0',
minWidth: '200px'
}
const cssGridFieldContainer = {
display: 'grid',
@@ -84,8 +86,21 @@ export default () => {
Meteor.subscribe('assetTypes');
Meteor.subscribe('assets');
const [removeRow, setRemoveRow] = useState(undefined)
const [assetId, setAssetId] = useState("")
const [serial, setSerial] = useState("")
const [assetTypeId, setAssetTypeId] = useState("")
const assetTypes = [{name: "All", _id: 0}, ...AssetTypes.find({}, {sort: {year: -1}}).fetch()]
const {assets} = useTracker(() => {
const assets = Assets.find({}).fetch();
let query = {}
if(assetId) query.assetId = {$regex: assetId, $options: 'i'}
if(serial) query.serial = {$regex: serial, $options: 'i'}
if(assetTypeId) query.assetTypeId = assetTypeId
const assets = Assets.find(query).fetch();
const assetTypes = AssetTypes.find({}, {sort: {year: -1}}).fetch();
const assetTypeNameMap = assetTypes.reduce((map, obj) => {
map[obj._id] = obj;
@@ -151,11 +166,47 @@ export default () => {
key: (row) => row._id,
editor: (row, close) => {return (<AssetEditor value={row} close={close}/>)},
add: true,
remove: (row) => {setRemoveRow(row)},
maxHeight: '40rem'
}
const removeAsset = (asset) => {
Meteor.call("assets.remove", asset._id);
setRemoveRow(undefined)
}
return (
<>
<Dialog open={removeRow ? true : false} onClose={() => setRemoveRow(undefined)}>
<DialogTitle>Remove Asset?</DialogTitle>
<DialogContent>
<DialogContentText>
Are you certain you want to remove the selected Asset?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => removeAsset(removeRow)}>Remove Asset</Button>
<Button onClick={() => setRemoveRow(undefined)}>Cancel</Button>
</DialogActions>
</Dialog>
<Box component="div" sx={{m: 2, p: 2, border: '1px dashed grey'}}>
<h4 style={{margin: 0, padding: 0}}>Filter</h4>
<Grid container spacing={2}>
<Grid item xs={4}>
<TextField style={cssEditorField} variant="standard" label="Asset ID" value={assetId} onChange={(e) => {setAssetId(e.target.value)}}/>
</Grid>
<Grid item xs={4}>
<TextField style={cssEditorField} variant="standard" label="Serial" value={serial} onChange={(e) => {setSerial(e.target.value)}}/>
</Grid>
<Grid item xs={4}>
<TextField style={cssEditorField} select variant="standard" value={assetTypeId} onChange={(e)=>{setAssetTypeId(e.target.value)}} label="Asset Type">
{assetTypes.map((assetType, i) => {
return <MenuItem key={i} value={assetType._id}>{assetType.name}</MenuItem>
})}
</TextField>
</Grid>
</Grid>
</Box>
<SimpleTable rows={assets} columns={columns} options={options}/>
</>
)

View File

@@ -4,19 +4,18 @@ import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import TabNav from '../util/TabNav';
import {Route, Routes} from "react-router-dom";
import Search from './History/Search'
export default () => {
let tabs = [
{
title: "Chromebook Usage",
getElement: () => {
const ChromebookUsage = lazy(()=>import('./History/ChromebookUsage'))
return <ChromebookUsage/>
},
path: '/chromebookUsage',
href: 'chromebookUsage'
},
// {
// title: "Chromebook Usage",
// getElement: () => {
// const ChromebookUsage = lazy(()=>import('./History/ChromebookUsage'))
// return <ChromebookUsage/>
// },
// path: '/chromebookUsage',
// href: 'chromebookUsage'
// },
{
title: "Asset History",
getElement: () => {
@@ -31,10 +30,6 @@ export default () => {
return (
<>
<TabNav tabs={tabs}/>
<Routes>
<Route path="search" element={<Search/>}/>
</Routes>
</>
)
}

View File

@@ -30,9 +30,9 @@ import SearchIcon from "@mui/icons-material/Search";
export default () => {
const navigate = useNavigate()
const [resultType, setResultType] = useState("usage")
const [searchType, setSearchType] = useState("email")
const [value, setValue] = useState("")
const search = () => {
if(searchType === 'email' || searchType === 'firstName' || searchType === 'lastName') {
if (value && value.length > 1) {
@@ -45,18 +45,18 @@ export default () => {
setPeopleToPickFrom(all)
setOpenPickPersonDialog(true)
} else if (all.length === 1) {
navigate("/history/search?resultType=" + encodeURIComponent(resultType) + "&" + (students.length ? "studentId" : 'staffId') + "=" + encodeURIComponent(all[0]._id));
navigate("/search?" + (students.length ? "studentId" : 'staffId') + "=" + encodeURIComponent(all[0]._id));
}
}
}
else if(searchType === 'assetID' || searchType === 'serial') {
let asset = Assets.findOne(searchType === 'assetID' ? {assetId: value} : {serial : value});
else if(searchType === 'assetId' || searchType === 'serial') {
let asset = Assets.findOne(searchType === 'assetId' ? {assetId: value.toUpperCase()} : {serial : value});
console.log(asset)
if(asset) {
if(searchType === 'assetID')
navigate("/history/search?resultType=" + encodeURIComponent(resultType) + "&assetId=" + encodeURIComponent(asset.assetId))
if(searchType === 'assetId')
navigate("/search?assetId=" + encodeURIComponent(asset.assetId.toUpperCase()))
else
navigate("/history/search?resultType=" + encodeURIComponent(resultType) + "&serial=" + encodeURIComponent(asset.serial))
navigate("/search?serial=" + encodeURIComponent(asset.serial))
}
}
}
@@ -71,7 +71,13 @@ export default () => {
if(openPickPersonDialog) setOpenPickPersonDialog(false)
if(person && person._id) {
navigate("/history/search?resultType=" + encodeURIComponent(resultType) + "&" + (person.grade ? "studentId" : 'staffId') + "=" + encodeURIComponent(person._id));
navigate("/search?" + (person.grade ? "studentId" : 'staffId') + "=" + encodeURIComponent(person._id));
}
}
const inputKeyPress = (e) => {
if(e.key === 'Enter' && value.length > 0) {
search()
}
}
@@ -95,14 +101,14 @@ export default () => {
</DialogActions>
</Dialog>
<div style={{display: "flex", flexDirection: "column"}} sx={{ p: '2px 4px', display: 'flex', alignItems: 'center', width: 400 }}>
<Paper componet='form'>
<div style={{marginBottom: "1rem", marginTop: "2rem"}}>
<ToggleButtonGroup color="primary" value={resultType} exclusive onChange={(e, type)=>setResultType(type)} aria-label="Result Type">
<ToggleButton value="usage">Usage History</ToggleButton>
<ToggleButton value="assignment">Assignment History</ToggleButton>
</ToggleButtonGroup>
</div>
<div style={{display: "flex", flexDirection: "column", marginTop: "1rem"}} sx={{ p: '2px 4px', display: 'flex', alignItems: 'center', width: 400 }}>
{/*<Paper componet='form'>*/}
{/* <div style={{marginBottom: "1rem", marginTop: "2rem"}}>*/}
{/* <ToggleButtonGroup color="primary" value={resultType} exclusive onChange={(e, type)=>setResultType(type)} aria-label="Result Type">*/}
{/* <ToggleButton value="usage">Usage History</ToggleButton>*/}
{/* <ToggleButton value="assignment">Assignment History</ToggleButton>*/}
{/* </ToggleButtonGroup>*/}
{/* </div>*/}
<div style={{marginBottom: "1rem"}}>
<ToggleButtonGroup color="primary" value={searchType} exclusive onChange={(e, type)=>setSearchType(type)} aria-label="Search Type">
<ToggleButton value="email">Email</ToggleButton>
@@ -113,12 +119,12 @@ export default () => {
</ToggleButtonGroup>
</div>
<div>
<InputBase value={value} onChange={(e) => {setValue(e.target.value)}} sx={{ ml: 1, flex: 1 }} placeholder="Value" inputProps={{ 'aria-label': 'Search Value' }}/>
<InputBase value={value} onKeyPress={inputKeyPress} onChange={(e) => {setValue(e.target.value)}} sx={{ ml: 1, flex: 1 }} placeholder="Value" inputProps={{ 'aria-label': 'Search Value' }}/>
<IconButton type="button" sx={{ p: '10px' }} aria-label="search" onClick={search}>
<SearchIcon />
</IconButton>
</div>
</Paper>
{/*</Paper>*/}
</div>
</>
)

View File

@@ -1,134 +0,0 @@
import { Meteor } from 'meteor/meteor';
import React, { useState, useEffect } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import {Link, useSearchParams} from "react-router-dom";
const RenderUsage = ({data}) => {
return (
<>
<ul>
{data.map((next, i) => (
<li key={next._id}>
{next.person && (
<>
User: {next.person.firstName} {next.person.lastName} {next.person.grade ? "~ " + next.person.grade : ""} (<Link to={"/history/search?email=" + encodeURIComponent(next.email)}>{next.email}</Link>)<br/>
</>
)}
Device ID: <Link to={"/history/search?deviceId=" + encodeURIComponent(next.deviceId)}>{next.deviceId}</Link><br/>
Serial: <Link to={"/history/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><br/>
{next.asset && (
<>
Asset ID: <Link to={"/history/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/>
</>
)}
{new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US")}
{next.assetType && (
<>
<br/>Asset Type: {next.assetType.name}
</>
)}
{next.assignedTo && (
<>
<br/>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})
</>
)}
</li>
))}
</ul>
</>
)
}
const RenderAssignments = ({data}) => {
return (
<>
<ul>
{data.map((next, i) => (
<li key={next._id}>
{next.assignee && (
<>
User: {next.assignee.firstName} {next.assignee.lastName} {next.assignee.grade ? "~ " + next.assignee.grade : ""} (<Link to={"/history/search?email=" + encodeURIComponent(next.assignee.email)}>{next.assignee.email}</Link>)<br/>
</>
)}
Serial: <Link to={"/history/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><br/>
{next.asset && (
<>
Asset ID: <Link to={"/history/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/>
</>
)}
{next.assetType && (
<>
Asset Type: {next.assetType.name}<br/>
</>
)}
{new Date(next.startDate).toLocaleDateString("en-US") + "-" + new Date(next.endDate).toLocaleDateString("en-US")}<br/>
Comment: {next.comment}<br/>
Start Condition: {next.startCondition}<br/>
Details: {next.startConditionDetails}<br/>
End Condition: {next.endCondition}<br/>
Details: {next.endConditionDetails}<br/>
</li>
))}
</ul>
</>
)
}
export default () => {
// const query = queryString.parse(search)
const [data, setData] = useState([])
const [search, setSearch] = useSearchParams()
useEffect(() => {
let args;
if(search.get('resultType') === 'usage') {
if(search.get('studentId')) {
args = {studentId: search.get('studentId')}
}
else if(search.get('staffId')) {
args = {staffId: search.get('staffId')}
}
else if(search.get('email')) {
args = {email: search.get('email')}
}
else if(search.get('deviceId')) {
args = {deviceId: search.get('deviceId')}
}
else if(search.get('serial')) {
args = {serial: search.get('serial')}
}
else if(search.get('assetId')) {
args = {assetId: search.get('assetId')}
}
Meteor.call('DataCollection.chromebookData', args, (err, result) => {
if (err) console.error(err)
else setData(result)
})
}
else {
if(search.get('studentId')) {
args = {studentId: search.get('studentId')}
}
else if(search.get('staffId')) {
args = {staffId: search.get('staffId')}
}
else if(search.get('serial')) {
args = {serial: search.get('serial')}
}
else if(search.get('assetId')) {
args = {assetId: search.get('assetId')}
}
Meteor.call('AssetAssignmentHistory.get', args, (err, result) => {
if (err) console.error(err)
else setData(result)
})
}
}, [search])
return (search.get('resultType') === 'usage' ? <RenderUsage data={data}/> : <RenderAssignments data={data}/>)
}

160
imports/ui/pages/Search.jsx Normal file
View File

@@ -0,0 +1,160 @@
import { Meteor } from 'meteor/meteor';
import React, { useState, useEffect } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
import {Link, useSearchParams} from "react-router-dom";
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
const RenderUsage = ({data}) => {
return (
<>
<ul>
{data.map((next, i) => (
<li key={next._id}>
{next.person && (
<>
User: {next.person.firstName} {next.person.lastName} {next.person.grade ? "~ " + next.person.grade : ""} (<Link to={"/search?email=" + encodeURIComponent(next.email)}>{next.email}</Link>)<br/>
</>
)}
Device ID: <Link to={"/search?deviceId=" + encodeURIComponent(next.deviceId)}>{next.deviceId}</Link><br/>
Serial: <Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><br/>
{next.asset && (
<>
Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/>
</>
)}
{new Date(next.startTime).toLocaleDateString("en-US") + "-" + new Date(next.endTime).toLocaleDateString("en-US")}
{next.assetType && (
<>
<br/>Asset Type: {next.assetType.name}
</>
)}
{next.assignedTo && (
<>
<br/>Currently assigned to: {next.assignedTo.firstName} {next.assignedTo.lastName} {next.assignedTo.grade ? "~ " + next.assignedTo.grade : ""} ({next.assignedTo.email})
</>
)}
</li>
))}
</ul>
</>
)
}
const RenderAssignments = ({data}) => {
return (
<>
<ul>
{data.map((next, i) => (
<li key={next._id + "/" + next.assetId}>
{next.assignee && (
<>
User: {next.assignee.firstName} {next.assignee.lastName} {next.assignee.grade ? "~ " + next.assignee.grade : ""} (<Link to={"/search?email=" + encodeURIComponent(next.assignee.email)}>{next.assignee.email}</Link>)<br/>
</>
)}
Serial: <Link to={"/search?serial=" + encodeURIComponent(next.serial)}>{next.serial}</Link><br/>
{next.asset && (
<>
Asset ID: <Link to={"/search?assetId=" + encodeURIComponent(next.asset.assetId)}>{next.asset.assetId}</Link><br/>
</>
)}
{next.assetType && (
<>
Asset Type: {next.assetType.name}<br/>
</>
)}
{new Date(next.startDate).toLocaleDateString("en-US") + (next.endDate ? "-" + new Date(next.endDate).toLocaleDateString("en-US") : " - Still Assigned")}<br/>
{next.endDate && (
<>Comment: {next.comment}<br/></>
)}
Start Condition: {next.startCondition}<br/>
Details: {next.startConditionDetails}<br/>
{next.endDate && (
<>
End Condition: {next.endCondition}<br/>
Details: {next.endConditionDetails}<br/>
</>
)}
</li>
))}
</ul>
</>
)
}
export default () => {
// const query = queryString.parse(search)
const [usageData, setUsageData] = useState([])
const [assignmentData, setAssignmentData] = useState([])
const [search, setSearch] = useSearchParams()
useEffect(() => {
let args;
if(search.get('studentId')) {
args = {studentId: search.get('studentId')}
}
else if(search.get('staffId')) {
args = {staffId: search.get('staffId')}
}
else if(search.get('email')) {
args = {email: search.get('email')}
}
else if(search.get('deviceId')) {
args = {deviceId: search.get('deviceId')}
}
else if(search.get('serial')) {
args = {serial: search.get('serial')}
}
else if(search.get('assetId')) {
args = {assetId: search.get('assetId')}
}
Meteor.call('DataCollection.chromebookData', args, (err, result) => {
if (err) console.error(err)
else setUsageData(result)
})
// if(search.get('studentId')) {
// args = {studentId: search.get('studentId')}
// }
// else if(search.get('staffId')) {
// args = {staffId: search.get('staffId')}
// }
// else if(search.get('serial')) {
// args = {serial: search.get('serial')}
// }
// else if(search.get('assetId')) {
// args = {assetId: search.get('assetId')}
// }
Meteor.call('AssetAssignmentHistory.get', args, (err, result) => {
if (err) console.error(err)
else setAssignmentData(result)
})
}, [search])
const [tabIndex, setTabIndex] = useState(0)
console.log(assignmentData)
// return (search.get('resultType') === 'usage' ? <RenderUsage data={data}/> : <RenderAssignments data={data}/>)
return (
<>
<Box sx={{width: '100%'}}>
<Tabs value={tabIndex} onChange={(e, index) => {setTabIndex(index)}} aria-label='nav tabs'>
<Tab label="Usage"/>
<Tab label="Assignments"/>
</Tabs>
</Box>
<div role="tabpanel" hidden={tabIndex !== 0}>
<RenderUsage data={usageData}/>
</div>
<div role="tabpanel" hidden={tabIndex !== 1}>
<RenderAssignments data={assignmentData}/>
</div>
</>
)
}

View File

@@ -0,0 +1,199 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
//{columns.map((column) => <td>{column.value(row)}</td>)}
const WholeRow = ({row, columns}) => {
return <tr><td>Test</td></tr>
}
const ColumnHeader = ({column}) => {
// console.log("Rendering column header")
return <td>
{column.isActions ?
"TODO: Add widgets"
:
column.title
}
</td>
}
const Row = ({columns, row, getRowKey, editedRowKey, editor, setEdited}) => {
return <tr><td>Test</td></tr>
}
/*
<tr>
{!editedRowKey || getRowKey(row) !== editedRowKey ?
columns.map((column, i) => <Cell column={column} row={row}/>)
:
editor
}
</tr>
*/
// onDoubleClick={(e) => {(!editedRowKey || getRowKey(row) !== editedRowKey) && setEdited(row)}}
const Cell = ({row, column}) => {
return <td>test</td>
}
/*
<td>
{column.isActions ?
"TODO"
:
column.value(row)
}
</td>
*/
/**
* Example:
* import {useState} from 'react';
* const[edited, setEdited] = useState(undefined);
* <GridTable setEdited={setEdited} edited={edited} rows={rows} columns={columnns} actions={actions}>
* <!-- Editor JSX here. -->
* </GridTable>
*
* export let rows;
export let columns;
export let rowKey; //Must only be null/undefined if the row is a new object (not associated with a row in the table). Should not change.
export let edited;
export let actions;
export let selection;
* @param props
* @returns {JSX.Element}
*/
export default ({columns, rows, actions, getRowKey, edited, setEdited, children}) => {
// Setup a width for each column.
columns.forEach(column => {
let min = column.minWidth ? Math.max(10, column.minWidth) : 10;
let weight = column.weight ? Math.max(1, column.weight) : 1;
column.width = 'minmax(' + min + 'px, ' + weight + 'fr)';
column.isActions = false;
});
// Add the actions column to the end.
// TODO: Allow it to be positioned.
if(actions) {
// TODO: make this fixed width based on the possible widget widths.
actions.width = 'minmax(10px, 1fr)';
actions.isActions = true;
columns[columns.length] = actions;
}
// Collect the column widths for the layout.
let gridTemplateColumns = columns.map(({width}) => width).join(' ');
// Resize column code.
let headerBeingResized = null;
let horizontalScrollOffset = 0;
const initResize = ({target}) => {
headerBeingResized = target.parentNode;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', completeResize);
headerBeingResized.classList.add('header--being-resized');
};
const completeResize = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', completeResize);
headerBeingResized.classList.remove('header--being-resized');
headerBeingResized = null;
};
const onMouseMove = e => {
try {
// Calculate the desired width.
horizontalScrollOffset = document.documentElement.scrollLeft;
let parentX = Math.round(headerBeingResized.getBoundingClientRect().x);
const width = horizontalScrollOffset + (e.clientX - parentX);
// Update the column object with the new size value.
const column = columns.find(({element}) => element === headerBeingResized);
column.width = Math.max(column.minWidth, width) + "px";
// Ensure all the column widths are converted to fixed sizes.
columns.forEach((column, index) => {
if((index < columns.length - 1) && (column.width.startsWith('minmax'))) {
column.width = parseInt(column.element.clientWidth, 10) + 'px';
}
});
// Render the new column sizes.
gridTemplateColumns = columns.map(({width}) => width).join(' ');
} catch(e) {console.log(e);}
}
// Select row code.
let selectedRowElement = null;
const selectRow = (e, row) => {
let element = e.target;
while(element && element.nodeName !== "TR") element = element.parentNode;
if(selectedRowElement) {
selectedRowElement.classList.remove('selected');
}
selectedRowElement = element;
element.classList.add('selected');
dispatch('selection', selectedRowElement.dataset.key);
}
// Edit row code.
let editorContainer;
let editorTable;
// Listen for changes to the edited variable. Move the editor row and display it when there is a value.
// Relies on the table row being hidden when the edited value matches the row's value.
useTracker(() => {
if(editorContainer) {
if(edited) {
let id = rowKey($edited);
let hiddenRow = editorTable.querySelector('tbody tr[data-key="' + id + '"]');
if(!hiddenRow) {
let body = editorTable.querySelector('tbody');
body.firstChild ? body.insertBefore(editorContainer, body.firstChild) : body.appendChild(editorContainer);
editorContainer.classList.remove('hidden');
}
else {
//let editor = hiddenRow.querySelector('.editor');
let body = editorTable.querySelector('tbody');
let next = hiddenRow.nextSibling;
//editor.appendChild(editorContainer);
next ? body.insertBefore(editorContainer, next) : body.appendChild(editorContainer);
editorContainer.classList.remove('hidden');
}
}
else {
console.log("Edited cleared");
editorContainer.classList.add('hidden');
}
}
});
let editedRowKey = edited ? getRowKey(edited) : undefined;
console.log("Rendering grid table")
// let contents = "";
//
// for(let row of rows) {
// contents += <WholeRow row={row} columns={columns}/>
// // contents += <tr>
// // for(let column of columns) {
// // contents += <CellValue row={row} column={column}></CellValue>
// // }
// // contents += </tr>
// }
return <div className={'grid-table-container'}>
<table style={{gridTemplateColumns}}>
<thead>
<tr>
{columns.map((column, i) => <ColumnHeader key={column.key} column={column}/>)}
</tr>
</thead>
<tbody>
{/*{rows.map((row, i) => <Row key={getRowKey(row)} row={row} columns={columns} getRowKey={getRowKey} editedRowKey={editedRowKey} editor={children} setEdited={setEdited}/>)}*/}
{rows.map((row)=><WholeRow row={row} columns={columns}/>)}
</tbody>
</table>
</div>
}

View File

@@ -0,0 +1,56 @@
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import _ from 'lodash';
export default class GridTable2 extends React.Component {
constructor(props) {
super(props)
this.state = {
rows: [{_id: 1234, name: "Fred"}, {_id: 1235, }],
columns: [{id: "first", value: row => {return row._id}}, {id: "second", value: row => {return row.name}}]
}
}
render() {
console.log("Rendering GridTable2")
return <table><tbody>
<tr><td>Test</td><td>Test2</td></tr>
{/*<tr>*/}
{/*<GridTableCell row={this.state.row} column={this.state.column}></GridTableCell>*/}
{/*</tr>*/}
{/*{this.state.rows.map((row) => <GridTableRow key={row._id} row={row} columns={this.state.columns}></GridTableRow>)}*/}
</tbody></table>
}
}
class GridTableRow extends React.Component {
constructor(props) {
super(props)
this.state = {
columns: props.columns,
row: props.row,
}
}
render() {
console.log("Rendering GridTableRow")
return <tr>
{this.state.columns.map((column)=><GridTableCell key={this.state.row._id + "-" + column.id} row={this.state.row} column={column}/>)}
</tr>
}
}
class GridTableCell extends React.Component {
constructor(props) {
super(props)
console.log(props);
this.state = {
row: props.row,
column: props.column,
value: props.column.value(props.row)
}
}
render() {
console.log("Rendering GridTableCell")
return <td>{this.state.value}</td>
}
}

View File

@@ -32,11 +32,15 @@ import Box from "@mui/material/Box";
// let options = {
// key: (row) => row._id,
// editor: (row) => {return (<MyRowEditor value={row}/>)}
// add: true,
// maxHeight: "40rem",
// remove: (row) => { /* show dialog and/or perform remove */ }
// }
const cssTopControls = {
display: 'flex',
flexDirection: 'row-reverse',
flexDirection: 'row',
justifyContent: 'flex-end',
}
/*
@@ -79,6 +83,10 @@ export default ({columns, rows, options}) => {
setEdited({})
e.stopPropagation()
}
if(!edited && e.key === 'Delete' && selected && options.delete && _.isFunction(options.delete)) {
options.delete(selected)
}
}
const sort = (e, column) => {
@@ -119,7 +127,10 @@ export default ({columns, rows, options}) => {
// console.log(rows)
return (
<div className='simpleTableContainer'>
{options.add && <div style={cssTopControls}><Button variant="text" className="button" onClick={addRow}>Add</Button></div>}
<div style={cssTopControls}>
{options.add && <Button variant="text" className="button" onClick={addRow}>Add</Button>}
{options.remove && _.isFunction(options.remove) && <Button disabled={!selected} variant='text' className='button' onClick={() => {selected && options.remove(selected)}}>Remove</Button>}
</div>
<TableContainer className="simpleTable" component={Paper} style={containerStyle}>
<Table size="small" aria-label="Table" tabIndex="0" onKeyDown={keyHandler}>
<TableHead className="sticky">