diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 563065029dd6533cd87ab700d05f34f874bb90b0..18f8e6f2941e5daf59de511bfbcabcea3163a757 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -70,7 +70,6 @@ class App extends Component { {label: 'Reports', icon: 'pi pi-fw pi-chart-bar', to:'/reports',section: 'reports'}, ]; } - onWrapperClick(event) { if (!this.menuClick) { this.setState({ @@ -168,21 +167,22 @@ class App extends Component { subscribe('edit-dirty', (flag) => { this.setState({ isEditDirty: flag }, () => { if (flag) { - window.addEventListener("beforeunload", function (e) { - var confirmationMessage = "\o/"; - (e || window.event).returnValue = confirmationMessage; //Gecko + IE - return confirmationMessage; - // this.toggleDirtyDialog(); - }); + window.addEventListener("beforeunload", reloadDirty); // window.addEventListener('popstate', this.onBackButtonEvent); window.history.pushState(null, document.title, window.location.href); window.addEventListener('popstate', this.onBackButtonEvent); } else { - //window.removeEventListener('beforeunload'); + window.removeEventListener("beforeunload",reloadDirty); } }); }); - + + var reloadDirty =function (e) { + var confirmationMessage = "\o/"; + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; + // this.toggleDirtyDialog(); + }; } onBackButtonEvent = (e) => { @@ -286,7 +286,6 @@ class App extends Component { </Router> </> } - <CustomDialog type="confirmation" visible={this.state.showDirtyDialog} width="40vw" header={'Confirmation'} message={'Do you want to leave this page? Your changes may not be saved.'} content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index edd2baf03bc4ed342deb9b56e16a62c794ad66d1..3d9c1e655a7a0ff9ff1993ea8a723e5bbd48ecf2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -1,7 +1,7 @@ import React, { useRef, useState } from "react"; import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination, useRowSelect, useColumnOrder } from 'react-table' import matchSorter from 'match-sorter' -import _ from 'lodash'; +import _, { filter } from 'lodash'; import moment from 'moment'; import { useHistory } from "react-router-dom"; import { OverlayPanel } from 'primereact/overlaypanel'; @@ -19,9 +19,11 @@ import { RadioButton } from 'primereact/radiobutton'; import { useExportData } from "react-table-plugins"; import { ProgressBar } from 'primereact/progressbar'; //import UIConstants from '../utils/ui.constants'; +import UtilService from '../../src/services/util.service' import Papa from "papaparse"; import JsPDF from "jspdf"; import "jspdf-autotable"; +import TableUtil from "../utils/table.util"; let doServersideFilter = false; let tbldata = [], filteredData = []; @@ -36,7 +38,7 @@ let allowRowSelection = false; let columnclassname = []; let parentCallbackFunction, parentCBonSelection; let showCSV = false; -let anyOfFilter = ''; +let multiSelectOption = {}; let filterCallback = null; let tableOptionsState = null; let setLoaderFunction = null; @@ -45,6 +47,8 @@ let hasFilters = false; let loadingStatus = false; let tmpTableData = null; let currentTableName = null; +let storeFilter = false; +let storage_array = []; // Define a default UI for filtering function GlobalFilter({ @@ -75,10 +79,20 @@ function DefaultColumnFilter({ const [filtered, setFiltered] = useState(false); React.useEffect(() => { if (!filterValue && value) { - setValue(''); + setValue(''); } - }, [filterValue, value]); - + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if (filterValue) { + setFiltered(true); + } + if(!value){ + setValue(filterValue); + setFilter(filterValue); + } + } + }, [filterValue, value]); + // Function to call the server side filtering const callServerFilter = (event, isCleared) => { hasFilters = true; @@ -91,8 +105,7 @@ function DefaultColumnFilter({ } else { filterCallback(tableOptionsState, setLoaderFunction); } - }; - + }; return ( <> <div className="table-filter" onClick={e => { e.stopPropagation() }} style={{marginRight: '5px'}}> @@ -101,6 +114,9 @@ function DefaultColumnFilter({ onChange={e => { setValue(e.target.value); setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } }} onKeyUp={(e) => { if (e.key === "Enter" && doServersideFilter) { @@ -119,6 +135,9 @@ function DefaultColumnFilter({ setValue(''); callServerFilter(e, true); } + if(storeFilter){ + TableUtil.saveFilter(currentTableName, Header, ''); + } }} /> } </div> @@ -127,6 +146,32 @@ function DefaultColumnFilter({ ) } +const setStoreData = (id, value) => { + debugger + let localstorage_key = window.location.pathname.split('/')[1]; + let storage = UtilService.localStore({ type: 'get', key: localstorage_key }); + if(storage && storage.length > 0) { + storage.forEach(function(value, index) { + if(value.name === id) { + value.name = id + value.value = value + } + }); + const selected = _.filter(storage, function (filter) { + if ( filter.name === id) { + return true; + } + }) + if(selected.length <= 0) { + storage.push({name: id, value: value}) + } + } else { + storage = [{name: id, value: value}] + } + UtilService.localStore({ type: 'set', key: localstorage_key, value: storage }); +} + + /* Generate and download csv */ @@ -174,7 +219,13 @@ function SelectColumnFilter({ if (!filterValue && value) { setValue(''); } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + setValue(filterValue); + setFilter(filterValue); + } }, [filterValue, value]); + const options = React.useMemo(() => { let options = null; if (showFilterOption) { @@ -233,6 +284,9 @@ function SelectColumnFilter({ callServerFilter(e, false); } } + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } }} > <option value="">All</option> @@ -248,15 +302,18 @@ function SelectColumnFilter({ // Multi-Select Custom Filter and set unique options value function MultiSelectColumnFilter({ - column: { filterValue, setFilter, preFilteredRows, id }, + column: { filterValue, setFilter, preFilteredRows, id, Header }, }) { const [value, setValue] = useState(''); const [filtertype, setFiltertype] = useState('Any'); // Set Any / All Filter type const setSelectTypeOption = (option) => { setFiltertype(option); - anyOfFilter = option + multiSelectOption[Header] = option if (value !== '') { + if (storeFilter) { + TableUtil.saveFilter(currentTableName, `${Header}-FilterOption`, option); + } setFilter(value); } }; @@ -266,10 +323,24 @@ function MultiSelectColumnFilter({ setValue(''); setFiltertype('Any'); } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + const filterType = TableUtil.getFilter(currentTableName, `${Header}-FilterOption`); + if(filterValue && !value){ + setValue(filterValue); + setFilter(filterValue); + setFiltertype(filterType); + multiSelectOption[Header] = filterType; + } + } }, [filterValue, value, filtertype]); - anyOfFilter = filtertype; + + multiSelectOption[Header] = filtertype; const options = React.useMemo(() => { let options = new Set(); + if (showFilterOption) { + return showFilterOption(id); + } preFilteredRows.forEach(row => { row.values[id].split(',').forEach(value => { if (value !== '') { @@ -306,8 +377,94 @@ function MultiSelectColumnFilter({ options={options} onChange={e => { setValue(e.target.value); - setFilter(e.target.value || undefined, filtertype) + setFilter(e.target.value || undefined); + setFiltertype(filtertype); + if(storeFilter) { + if (e.target.value.length > 0) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + TableUtil.saveFilter(currentTableName, `${Header}-FilterOption`, filtertype); + } else { + TableUtil.clearColumnFilter(currentTableName, Header); + TableUtil.clearColumnFilter(currentTableName, `${Header}-FilterOption`); + } + } + }} + maxSelectedLabels="1" + selectedItemsLabel="{0} Selected" + className="multi-select" + /> + </div> + </div> + ) +} + +// Multi-Select Custom Filter and set unique options value +function MultiSelectFilter({ + column: { filterValue, setFilter, preFilteredRows, id, Header }, +}) { + const [value, setValue] = useState(''); + const [filtertype, setFiltertype] = useState('Any'); + + React.useEffect(() => { + if (!filterValue && value) { + setValue(''); + setFiltertype('Any'); + } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if(filterValue && !value){ + setValue(filterValue); + setFilter(filterValue); + setFiltertype(filtertype); + } + } + }, [filterValue, value, filtertype]); + + multiSelectOption[Header] = filtertype; + const options = React.useMemo(() => { + let options = new Set(); + if (showFilterOption) { + return showFilterOption(id); + } + preFilteredRows.forEach(row => { + row.values[id].split(',').forEach(value => { + if (value !== '') { + let hasValue = false; + options.forEach(option => { + if (option.name === value) { + hasValue = true; + } + }); + if (!hasValue) { + let option = { 'name': value, 'value': value }; + options.add(option); + } + } + }); + }); + return [...options.values()] + }, [id, preFilteredRows]); + + // Render a multi-select box + return ( + <div onClick={e => { e.stopPropagation() }} > + <div style={{ position: 'relative' }} > + <MultiSelect data-testid="multi-select" id="multi-select-1" optionLabel="value" optionValue="value" filter={true} + value={value} + options={options} + onChange={e => { + setValue(e.target.value); + setFilter(e.target.value || undefined); + if(storeFilter) { + if (e.target.value.length > 0) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } else { + TableUtil.clearColumnFilter(currentTableName, Header); + } + } }} + maxSelectedLabels="1" + selectedItemsLabel="{0} Selected" className="multi-select" /> </div> @@ -319,7 +476,7 @@ function MultiSelectColumnFilter({ // slider to set the filter value between a column's // min and max values function SliderColumnFilter({ - column: { filterValue, setFilter, preFilteredRows, id }, + column: { filterValue, setFilter, preFilteredRows, id, Header }, }) { // Calculate the min and max // using the preFilteredRows @@ -334,9 +491,25 @@ function SliderColumnFilter({ return [min, max] }, [id, preFilteredRows])*/ + React.useEffect(() => { + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if (filterValue && !value) { + setValue(filterValue); + setFilter(filterValue); + } + } + }); + return ( <div onClick={e => { e.stopPropagation() }} className="table-slider"> - <Slider value={value} onChange={(e) => { setFilter(e.value); setValue(e.value) }} /> + <Slider value={value} + onChange={(e) => { + setFilter(e.value); setValue(e.value); + if (storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.value); + } + }} /> </div> ) } @@ -354,6 +527,13 @@ function BooleanColumnFilter({ if (!filterValue && value) { setValue(null); } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if(filterValue && !value){ + setValue(filterValue); + setFilter(filterValue); + } + } }, [filterValue, value]); // Function to call the server side filtering const callServerFilter = (isCleared) => { @@ -381,6 +561,9 @@ function BooleanColumnFilter({ setValue(e.target.value); setFilter(e.target.value || undefined); setFiltered(true); + if (storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } if (doServersideFilter) { callServerFilter(e.value); } @@ -389,6 +572,158 @@ function BooleanColumnFilter({ ) } +// This is a custom filter UI that uses a +// calendar to set the valueCalendar +function ColumnFilter({ + column: { setFilter, filterValue, Header }, +}) { + // Calculate the min and max + // using the preFilteredRows + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(null); + } + }, [filterValue, value]); + + React.useEffect(() => { + // let localstorage_key = window.location.pathname.split('/')[1]; + // let storage = UtilService.localStore({ type: 'get', key: localstorage_key }); + // let storageStatus =_.filter(storage, function (filter) { + // if ( filter.name === Header && storeFilter) { + // setValue(filter.value); + // setFilter(filter.value); + // setFiltered(true); + // return true; + // } + // }) + }, []); + + // Function to call the server side filtering + const callServerFilter = (event, isCleared) => { + hasFilters = true; + if (isCleared) { + hasFilters = false; + if (filtered) { + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + filterCallback(tableOptionsState, setLoaderFunction); + } + } else { + filterCallback(tableOptionsState, setLoaderFunction); + } +}; + return ( + + <div className="table-filter" onClick={e => { e.stopPropagation() }}> + <Calendar value={filterValue} appendTo={document.body} dateFormat="yy-mm-dd" + onChange={(e) => { + const value = moment(e.value).format('YYYY-MM-DD') + setValue(value); + setFilter(e.value); + if (value !== 'Invalid date' && doServersideFilter) { + setFiltered(true); + callServerFilter(e); + } + // if(storeFilter) { + // let localstorage_key = window.location.pathname.split('/')[1]; + // let storage = UtilService.localStore({ type: 'get', key: localstorage_key }); + // if(storage && storage.length > 0) { + // storage.forEach(function(value, index) { + // if(value.name === Header) { + // value.name = Header + // value.value = moment(e.value).format('YYYY-MM-DD') + // } + // }); + // const selected = _.filter(storage, function (filter) { + // if ( filter.name === Header) { + // return true; + // } + // }) + // if(selected.length <= 0) { + // storage.push({name: Header, value: moment(e.value).format('YYYY-MM-DD')}) + // } + // } else { + // storage = [{name: Header, value: value}] + // } + // UtilService.localStore({ type: 'set', key: localstorage_key, value: storage }); + // } + }} + showIcon></Calendar> + {value && <i onClick={(e) => { setFilter(undefined); setValue(''); setFiltered(false); + if (doServersideFilter) { + setFilter(undefined); + setValue(''); + callServerFilter(e, true); + } }} className="tb-cal-reset fa fa-times" />} + </div> + ) +} + +// This is a custom filter UI that uses a +// calendar range to set the value +function DateRangeColumnFilter({ + column: { setFilter, filterValue, Header }, +}) { + // Calculate the min and max + // using the preFilteredRows + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(null); + } + + if (storeFilter) { + const filter = TableUtil.getFilter(currentTableName, Header); + const filterValue = _.map(filter, date => {return new Date(date)} ) + if (filterValue[1] && !value ){ + setValue(filterValue); + setFilter(filterValue); + } + } + }, [filterValue, value]); + // Function to call the server side filtering + const callServerFilter = (event, isCleared) => { + hasFilters = true; + if (isCleared) { + hasFilters = false; + if (filtered) { + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + filterCallback(tableOptionsState, setLoaderFunction); + } + } else { + filterCallback(tableOptionsState, setLoaderFunction); + } +}; + return ( + <div className="table-filter" onClick={e => { e.stopPropagation() }}> + <Calendar selectionMode="range" value={filterValue} appendTo={document.body} + placeholder="Range" + onChange={(e) => { + setValue(e.value); + setFilter(e.value); + if (value !== 'Invalid date' && doServersideFilter) { + setFiltered(true); + callServerFilter(e); + } + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } + }} + showIcon></Calendar> + {value && <i onClick={(e) => { setFilter(undefined); setValue(''); setFiltered(false); + if(storeFilter){ + TableUtil.saveFilter(currentTableName, Header, [] ); + } + if (doServersideFilter) { + setFilter(undefined); + setValue(''); + callServerFilter(e, true); + } }} className="tb-cal-reset fa fa-times" />} + </div> + ) +} // This is a custom filter UI that uses a // calendar to set the value function CalendarColumnFilter({ @@ -402,7 +737,17 @@ function CalendarColumnFilter({ if (!filterValue && value) { setValue(null); } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if(filterValue && !value){ + const valueAsDate = new Date(filterValue) + setValue(valueAsDate); + setFilter(valueAsDate); + } + } + }, [filterValue, value]); + // Function to call the server side filtering const callServerFilter = (event, isCleared) => { hasFilters = true; @@ -428,9 +773,21 @@ function CalendarColumnFilter({ setFiltered(true); callServerFilter(e); } + + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, value); + } }} + onClearButtonClick={(e) => { + if(storeFilter) { + TableUtil.clearColumnFilter(currentTableName, Header); + } + }} showIcon></Calendar> {value && <i onClick={(e) => { setFilter(undefined); setValue(''); setFiltered(false); + if (storeFilter) { + TableUtil.clearColumnFilter(currentTableName, Header); + } if (doServersideFilter) { setFilter(undefined); setValue(''); @@ -450,9 +807,17 @@ function DateTimeColumnFilter({ if (!filterValue && value) { setValue(null); } + if (storeFilter) { + const getValue = TableUtil.getFilter(currentTableName, Header); + if (getValue && !value) { + const valueAsDate = new Date(getValue) + setValue(valueAsDate); + setFilter(valueAsDate); + } + } }, [filterValue, value]); - - // Function to call the server side filtering + + // Function to call the server side filtering const callServerFilter = (event, isCleared) => { hasFilters = true; _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); @@ -465,7 +830,6 @@ function DateTimeColumnFilter({ } }; return ( - <div className="table-filter" onClick={e => { e.stopPropagation() }}> <Calendar value={value} appendTo={document.body} dateFormat="yy/mm/dd" onKeyUp={(e) => { @@ -479,6 +843,10 @@ function DateTimeColumnFilter({ if (value !== 'Invalid date' && doServersideFilter) { callServerFilter(e); } + + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, value); + } }} showIcon // showTime= {true} @@ -492,6 +860,9 @@ function DateTimeColumnFilter({ setValue(''); callServerFilter(e, true); } + if (storeFilter) { + TableUtil.clearColumnFilter(currentTableName, Header); + } }} className="tb-cal-reset fa fa-times" />} </div> ) @@ -540,21 +911,13 @@ function multiSelectFilterFn(rows, id, filterValue) { } let rowValue = row.values[id]; let hasData = false; - if (anyOfFilter === 'Any') { - hasData = false; - filterValue.forEach(filter => { - if (rowValue.includes(filter)) { - hasData = true; - } - }); + let columnValues = rowValue.split(','); + let unfilteredColValues = _.difference(filterValue, columnValues); + if (multiSelectOption[id] === 'Any') { + hasData = unfilteredColValues.length < filterValue.length; } else { - hasData = true; - filterValue.forEach(filter => { - if (!rowValue.includes(filter)) { - hasData = false; - } - }); + hasData = unfilteredColValues.length === 0; } return hasData; }); @@ -612,11 +975,38 @@ function dateFilterFn(rows, id, filterValue) { return filteredRows; } +/** + * Custom function to filter data Range from date field. + * @param {Array} rows + * @param {String} id + * @param {String} filterValue + */ + function dateRangeFilterFn(rows, id, filterValue) { + const filteredRows = _.filter(rows, function (row) { + // If cell value is null or empty + if (!row.values[id]) { + return false; + } + //Remove microsecond if value passed is UTC string in format "YYYY-MM-DDTHH:mm:ss.sssss" + let rowValue = moment.utc(row.values[id].split('.')[0]); + if (!rowValue.isValid()) { + // For cell data in format 'YYYY-MMM-DD' + rowValue = moment.utc(moment(row.values[id], 'YYYY-MM-DD').format("YYYY-MM-DDT00:00:00")); + } + const start = moment.utc(moment(filterValue[0], 'YYYY-MM-DD').format("YYYY-MM-DDT00:00:00")); + const end = moment.utc(moment(filterValue[1], 'YYYY-MM-DD').format("YYYY-MM-DDT23:59:59")); + return (start.isSameOrBefore(rowValue) && end.isSameOrAfter(rowValue)); + }); + return filteredRows; +} + // This is a custom UI for our 'between' or number range // filter. It uses slider to filter between min and max values. function RangeColumnFilter({ - column: { filterValue = [], preFilteredRows, setFilter, id }, + column: { filterValue = [], preFilteredRows, setFilter, id, Header }, }) { + let [value, setValue] = useState(''); + let [firstLoad, setFirstLoad] = useState(true); const [min, max] = React.useMemo(() => { let min = 0; let max = 0; @@ -627,12 +1017,19 @@ function RangeColumnFilter({ min = Math.min(row.values[id] ? row.values[id] : 0, min); max = Math.max(row.values[id] ? row.values[id] : 0, max); }); + if (storeFilter && firstLoad) { + let storedFilter = TableUtil.getFilter(currentTableName, Header); + if (storedFilter) { + setValue(storedFilter); + setFilter(storedFilter); + setFirstLoad(false); + } + } return [min, max]; }, [id, preFilteredRows]); if (filterValue.length === 0) { filterValue = [min, max]; } - return ( <> <div className="filter-slider-label"> @@ -641,7 +1038,13 @@ function RangeColumnFilter({ </div> <Slider value={filterValue} min={min} max={max} className="filter-slider" style={{}} - onChange={(e) => { setFilter(e.value); }} range /> + onChange={(e) => { + setValue(e.value); + setFilter(e.value); + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.value); + } + }} range /> </> ); } @@ -726,20 +1129,31 @@ const filterTypes = { 'select': { fn: SelectColumnFilter, }, + // This component has Any and All Radio buttons 'multiselect': { fn: MultiSelectColumnFilter, type: multiSelectFilterFn }, + // This component does not have Any and All Radio buttons + 'multiselect-filter': { + fn: MultiSelectFilter, + type: multiSelectFilterFn + }, 'switch': { fn: BooleanColumnFilter }, 'slider': { fn: SliderColumnFilter - }, + }, + 'date': { fn: CalendarColumnFilter, type: dateFilterFn }, + 'dateRange': { + fn: DateRangeColumnFilter, + type: dateRangeFilterFn + }, 'fromdatetime': { fn: DateTimeColumnFilter, type: fromDatetimeFilterFn @@ -908,7 +1322,7 @@ function Table(props) { if (currentTableName && currentTableName !== tablename) { state.sortBy = defaultSortColumn } - currentTableName = tablename; + currentTableName = props.tablename; tableOptionsState = _.cloneDeep(state); // Pass the table's state to the parent function if the parent function has set the callback function for it. if (props.setTableState) { @@ -1001,7 +1415,8 @@ function Table(props) { if (e.target.id === '') { defaultVisible = e.target.checked; } - allColumns.forEach(acolumn => { + // allColumns.forEach(acolumn => { + for (const acolumn of allColumns) { let jsonobj = {}; let visible = (acolumn.Header === e.target.id) ? ((acolumn.isVisible) ? false : true) : e.target.id === '' ? defaultVisible : acolumn.isVisible; jsonobj['Header'] = acolumn.Header; @@ -1012,7 +1427,16 @@ function Table(props) { sortedColumn['Header'] = acolumn.Header; sortedColumn['isVisible'] = visible; } - }); + // Remove the column filters stored and clear the column filter, if the column is toogled to + if (!visible) { + if (storeFilter) { + TableUtil.clearColumnFilter(currentTableName, acolumn.Header); + } + _.remove(tableOptionsState.filters, ['id', acolumn.Header]); + setAllFilters(tableOptionsState.filters); + } + } + // }); localStorage.setItem(tablename, JSON.stringify(lsToggleColumns)); if (onColumnToggleViewTable) { @@ -1052,7 +1476,11 @@ function Table(props) { if (doServersideFilter) { filterCallback(tableOptionsState, setLoaderFunction); } + if (storeFilter) { + TableUtil.clearTableFilter(currentTableName); + } } + return ( <> <div style={{display:'flex',justifyContent:'space-between',height:'35px'}}> @@ -1246,6 +1674,7 @@ function ViewTable(props) { let defaultheader = props.defaultcolumns; let optionalheader = props.optionalcolumns; let defaultSortColumn = props.defaultSortColumn; + storeFilter = props.storeFilter? props.storeFilter : false let tablename = (props.tablename) ? props.tablename : window.location.pathname; if (!defaultSortColumn) { @@ -1394,14 +1823,22 @@ function ViewTable(props) { // const [loading, setLoading] = React.useState(false); const [currentPage, setCurrentPage] = React.useState(0); const fetchData = React.useCallback( ({ state, setLoading }) => { - loadServerData(state, setLoading); + loadServerData(state, setLoading, true); }, []); - const loadServerData = (state, setLoading) => { + const loadServerData = (state, setLoading, onload) => { //setLoading(true); loadingStatus = true; setCurrentPage(state.pageIndex); if(props.callBackFunction) { + if(storeFilter) { + if (onload) { + let filters = UtilService.localStore({ type: 'get', key: tablename }); + UtilService.localStore({ type: 'set', key: tablename, value: filters}); + } else { + UtilService.localStore({ type: 'set', key: tablename, value: state.filters}); + } + } const promises = [props.callBackFunction(state)]; Promise.all(promises).then(async responses => { tbldata = responses[0][0]; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 192a08ec3dfcfd916615ab33b5ddf1c8233213bd..6c5c78d9ebd3f729e2ddc254fc93ced7e2762795 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -263,6 +263,9 @@ In Excel View the for Accordion background color override .p-multiselect-panel { min-width: 15em !important; } +.p-highlight label { + color: #ffffff !important; +} .pi-search { right: -1.25em !important; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js index 2fd3c9c2c3be1cd013b31ecc8f371361c542b33e..36fdcb1febdf2569f4944e362bfb076c043ffa43 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/CustomDialog.js @@ -46,7 +46,7 @@ export class CustomDialog extends Component { })} </div> } > - <div className="p-grid"> + <div className="p-grid" style={{marginTop: '10px'}}> {showIcon && <div className="col-lg-2 col-md-2 col-sm-2"> <span style={{position: 'absolute', top: '50%', '-ms-transform': 'translateY(-50%)', transform: 'translateY(-50%)'}}> @@ -57,10 +57,13 @@ export class CustomDialog extends Component { <div className= {(showIcon)? "col-lg-10 col-md-10 col-sm-10":"dialog-delete-msg"}> {/* Display message passed */} {this.props.message?this.props.message:""} - {/* Render subcomponent passed as function */} - {this.props.content?this.props.content():""} + </div> </div> + <div> + {/* Render subcomponent passed as function */} + {this.props.content?this.props.content():""} + </div> </Dialog> </div> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss index 01e0e0eee1ce5e7b4547ac240e0e0f804e8c0877..fe83753908167bd00d16a39fc386151ed528d9a8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_timeline.scss @@ -430,6 +430,10 @@ body .p-multiselect-panel .p-multiselect-header .p-multiselect-filter-container padding: 0em .2em 0em .2em } +#reserv-reasons .p-multiselect-panel { + left: -110px !important +} + .alignTimeLineHeader { display: flex; justify-content: space-between; @@ -451,6 +455,10 @@ body .p-multiselect-panel .p-multiselect-header .p-multiselect-filter-container .sub-header .toggle-btn { margin-top: 5px; + font-size: 10px !important; + min-height: 25px; + height: auto; + vertical-align: top; } .body .p-inputswitch { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js index b137037c0a2ef0ba24b5a175c035de0dc74cf37b..c9f14bae85cdaec59745940e67ffea153d44fe96 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -227,6 +227,7 @@ class CycleList extends Component { lsKeySortColumn={this.lsKeySortColumn} descendingColumn={this.descendingColumn} pageUpdated={this.pageUpdated} + storeFilter={false} /> : <></> } </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index 3b937cb55067067736f3b5deccbd0e9102650bbc..635c8a622fe8f9a13c3231809d90e64ceb4b6984 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -196,7 +196,7 @@ export class ProjectCreate extends Component { * @param {any} value */ async setProjectParams(key, value, type) { - let project = _.cloneDeep(this.state.project); + let project = this.state.project; switch(type) { case 'NUMBER': { project[key] = value?parseInt(value):0; @@ -239,12 +239,9 @@ export class ProjectCreate extends Component { if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { - this.setState({validForm: validForm, isDirty: true}); - publish('edit-dirty', true); - } else { - this.setState({validForm: validForm}); - } + this.setState({validForm: validForm, isDirty: true}); + publish('edit-dirty', true); + } /** @@ -384,9 +381,7 @@ export class ProjectCreate extends Component { */ cancelCreate() { publish('edit-dirty', false); - this.props.history.goBack(); - this.setState({showDialog: false}); - this.props.history.goBack(); + this.setState({redirect: `/project`}); } /** diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js index b763a3e5a4ff6db0859eccbed184e5c28bfa546a..e8b8a5623810afceb5efbd59d7a48c41811dfa6b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -262,17 +262,15 @@ export class ProjectEdit extends Component { break; } } - await this.setState({project: project}); + let isDirty = this.state.isDirty || ( !this.state.isDirty && !_.isEqual(this.state.project, project) ); + await this.setState({project: _.cloneDeep(project)}); let validForm = this.validateForm(key); if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { - this.setState({validForm: validForm, isDirty: true}); - publish('edit-dirty', true); - } else { - this.setState({validForm: validForm}); - } + this.setState({validForm: validForm, isDirty: isDirty}); + publish('edit-dirty', true); + } /** @@ -451,9 +449,7 @@ export class ProjectEdit extends Component { */ cancelEdit() { publish('edit-dirty', false); - this.props.history.goBack(); - this.setState({showDialog: false}); - this.props.history.goBack(); + this.setState({redirect: `/project/view/${this.props.match.params.id}`}); } render() { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js index 08c0763cb1ce2ddb9904ce902b748ab9a0526f0e..bbb4dcaee36c1a7d8c29f2770670b206812e9b42 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/list.js @@ -27,7 +27,7 @@ export class ProjectList extends Component { name: "Category of Project", filter: "select" }, - description: "Description", + description: "Description", archive_location_label: { name: "LTA Storage Location", filter: "select" @@ -230,6 +230,7 @@ export class ProjectList extends Component { toggleBySorting={(sortData) => this.toggleBySorting(sortData)} lsKeySortColumn={this.lsKeySortColumn} pageUpdated={this.pageUpdated} + storeFilter={false} /> : <div>No project found </div> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.create.js index 510f8ccd624b9999e706c619a6e4035914ba676c..bfa70d3a2fd71f4aedb02a59a8540a70a0962288 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.create.js @@ -672,7 +672,7 @@ export class ReservationCreate extends Component { <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" header={'Add Reservation'} message={'Do you want to leave this page? Your changes may not be saved.'} - content={''} onClose={this.cancelCreate} onCancel={this.close} onSubmit={this.cancelCreate}> + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> </CustomDialog> </div> </React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.edit.js index 46e6f0f87d0ad4e0461e67ed4d31ae49e732c574..0263f87a5c5ea5823cd17c10c423b9b78fda99a8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.edit.js @@ -630,7 +630,7 @@ export class ReservationEdit extends Component { <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" header={'Edit Reservation'} message={'Do you want to leave this page? Your changes may not be saved.'} - content={''} onClose={this.cancelEdit} onCancel={this.close} onSubmit={this.cancelEdit}> + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> </CustomDialog> </React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.list.js index 17043373e5a652f6c9ccb8b0b3ce5278a587173e..220ae97b1bd3014092190d2069bfe2a1e933bfa6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.list.js @@ -80,9 +80,18 @@ export class ReservationList extends Component{ name: "Project", filter:"select" }, - expert: "Expert", - hba_rfi: "HBA-RFI", - lba_rfi: "LBA-RFI", + expert: { + name: "Expert", + filter:"switch" + }, + hba_rfi: { + name: "HBA-RFI", + filter: 'switch' + }, + lba_rfi: { + name: "LBA-RFI", + filter: 'switch' + }, actionpath: "actionpath" }], optionalcolumns: [{ @@ -510,6 +519,7 @@ export class ReservationList extends Component{ toggleBySorting={(sortData) => this.toggleBySorting(sortData)} lsKeySortColumn={this.lsKeySortColumn} pageUpdated={this.pageUpdated} + storeFilter={true} /> </> : <div>No Reservation found </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.summary.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.summary.js index b8efdbb609987c7bc7e00837d9d6c9ba3c2d11ff..e1e5d89bbdb7fe920ad96933626ed1a220560b46 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.summary.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Reservation/reservation.summary.js @@ -92,6 +92,17 @@ export class ReservationSummary extends Component { return specification; } + /** + * Funtion to open the reservation view page in same or new tab/window. + */ + redirectToReservDetails = () => { + if (this.props.viewInNewWindow) { + window.open(`/reservation/view/${this.props.reservation.id}`, '_blank'); + } else { + this.props.history.push(`/reservation/view/${this.props.reservation.id}`); + } + } + render() { const reservation = this.props.reservation; let specifications = reservation?_.cloneDeep(reservation.specifications_doc):null; @@ -105,8 +116,8 @@ export class ReservationSummary extends Component { <div className="p-grid timeline-details-pane" style={{marginTop: '10px'}}> <h6 className="col-lg-10 col-sm-10">Reservation Details</h6> {/* TODO: Enable the link once Reservation view page is created */} - {/* <Link to={`/su/timeline/reservation/view/${reservation.id}`} title="View Full Details" ><i className="fa fa-eye"></i></Link> */} - <i className="fa fa-eye" style={{color: 'grey'}}></i> + <Link onClick={this.redirectToReservDetails} title="View Full Details" ><i className="fa fa-eye"></i></Link> + {/* <i className="fa fa-eye" style={{color: 'grey'}}></i> */} <Link to={this.props.location?this.props.location.pathname:"/su/timelineview"} onClick={this.closeSUDets} title="Close Details"><i className="fa fa-times"></i></Link> <div className="col-4"><label>Name:</label></div> <div className="col-8">{reservation.name}</div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index 62f864e695ef95ebacc68d0e5341cc2d190611c8..ea5c0dd490e5c52a18fcb848856cf07bf44e1c0b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -427,7 +427,7 @@ class SchedulingUnitList extends Component{ } else { let scheduleunits = []; let expand = suType.toLowerCase() === 'draft' ? this.SU_DRAFT_EXPAND: this.SU_BLUEPRINT_EXPAND; - let response = await ScheduleService.getSchedulingUnitsExpandWithFilter(suType.toLowerCase(), expand, filterQry, orderBy, limit, offset); + let response = await ScheduleService.getSchedulingUnitsExpandWithFilter(suType.toLowerCase(), expand, filterQry, orderBy, limit, offset); if (response && response.data) { this.totalPage = response.data.count; scheduleunits = response.data.results; @@ -581,7 +581,7 @@ class SchedulingUnitList extends Component{ } return su; }); - await this.setState({ + await this.setState({ scheduleunit: schedulingUnits, isLoading: false, optionalColumns: [optionalColumns], columnclassname: [columnclassname], loadingStatus: dataLoadstatus }); @@ -593,6 +593,7 @@ class SchedulingUnitList extends Component{ this.setState({userrole: permission}); //this.getUserRolePermission(); this.pageUpdated = true; + this.getFilterColumns(this.state.suType.toLowerCase()); this.getSchedulingUnitList(true, this.state.suType, this.filterQry, this.orderBy, this.limit, this.offset); this.setToggleBySorting(); } @@ -1090,25 +1091,30 @@ class SchedulingUnitList extends Component{ * @returns */ async fetchTableData(state) { + // UtilService.localStore({ type: 'set', key: 'schedulingUnitFilter' ,value:state.filters}); this.filterQry = ''; this.orderBy = ''; this.pageUpdated = true; this.setState({loadingStatus:true}); - for( const filter of state.filters) { - if (filter.id === 'Start Time') { - this.filterQry += 'start_time_after='+filter.value+'&start_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&'; - } else if (filter.id === 'End Time') { - //let end = moment.utc(moment(filter.value, 'YYYY-MM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - //end = moment(end, "DD-MM-YYYY").add(1, 'days'); - //filter.value = _.replace(filter.value, '00:00:00', '23:59:59'); - this.filterQry += 'stop_time_after='+filter.value+'&stop_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&' - } else { - let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); - if(columnDetails) { - this.filterQry += columnDetails.orgField +'='+filter.value+'&' + let filters = UtilService.localStore({ type: 'get', key: "scheduleunit_list_"+this.state.suType }); + if(filters.length > 0 ) { + for( const filter of filters) { + if (filter.id === 'Start Time') { + this.filterQry += 'start_time_after='+filter.value+'&start_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&'; + } else if (filter.id === 'End Time') { + //let end = moment.utc(moment(filter.value, 'YYYY-MM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); + //end = moment(end, "DD-MM-YYYY").add(1, 'days'); + //filter.value = _.replace(filter.value, '00:00:00', '23:59:59'); + this.filterQry += 'stop_time_after='+filter.value+'&stop_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&' + } else { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + if(columnDetails) { + this.filterQry += columnDetails.orgField +'='+filter.value+'&' + } } } } + let sortBy = state.sortBy?state.sortBy[0]:null; if (sortBy) { this.defaultSortColumn = sortBy; @@ -1507,6 +1513,7 @@ class SchedulingUnitList extends Component{ loadingStatus={this.state.loadingStatus} showFilterOption={this.getFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter pageUpdated={this.pageUpdated} + storeFilter={true} setTableReloadFunction={this.setTableReloadFunction} setTableState={this.setTableState} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js index 8aac693a65c3ee17558a2b36c761b9373c2a1e0b..1774831fce1c7f372e7f35c55843fcbe2726b1b1 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -396,7 +396,7 @@ export class EditSchedulingUnit extends Component { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Template Missing.'}); } this.setState({isDirty: false}); - publish('edit-dirty', true); + publish('edit-dirty', false); } @@ -657,4 +657,4 @@ export class EditSchedulingUnit extends Component { ); } } -export default EditSchedulingUnit +export default EditSchedulingUnit \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js index ea37b6ee52fbf8a21eb9052bdafc17621d273d8a..1ca2038c9390ad1674f5a23386681fc36af4d503 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js @@ -654,7 +654,9 @@ export class TaskList extends Component { this.orderBy = ''; this.pageUpdated = true; this.setState({loadingStatus:true}); - for( const filter of state.filters) { + let filters = UtilService.localStore({ type: 'get', key: "su_task_list_"+this.state.taskType }); + if(filters.length > 0 ) { + for( const filter of filters) { if (filter.id === 'Start Time') { this.filterQry += 'start_time_after='+filter.value+'&start_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&'; } else if (filter.id === 'End Time') { @@ -666,6 +668,7 @@ export class TaskList extends Component { } } } + } let sortBy = state.sortBy?state.sortBy[0]:null; if (sortBy) { this.setState({defaultSortColumn: [sortBy]}); @@ -726,6 +729,7 @@ export class TaskList extends Component { callBackFunction={this.fetchTableData} loadingStatus={this.state.loadingStatus} showGlobalFilter={false} + storeFilter={true} /> </> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index e50e9d13912246383a41c71461eb1f4058306369..a470c2a260c798fa9f4d731fce020efd0a7feaea 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -78,13 +78,14 @@ export class TimelineView extends Component { stationView: this.timelineUIAttributes.stationView || false, stationGroup: [], selectedStationGroup: [], //Station Group(core,international,remote) - reservationFilter: null, + reservationFilter: [], showSUs: this.timelineUIAttributes.showSUs===undefined?true:this.timelineUIAttributes.showSUs, showTasks: this.timelineUIAttributes.showTasks || false, groupByProject: this.timelineUIAttributes.groupByProject || false, taskTypes: [], selectedTaskTypes: this.timelineUIAttributes["taskTypes"] || ['observation'], - isStationTasksVisible: this.timelineUIAttributes.isStationTasksVisible===undefined?true:this.timelineUIAttributes.isStationTasksVisible + isStationTasksVisible: this.timelineUIAttributes.isStationTasksVisible===undefined?true:this.timelineUIAttributes.isStationTasksVisible, + showReservation: this.timelineUIAttributes.showReservation || false // Flag to show reservations in normal timeline view } this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group this.allStationsGroup = []; @@ -116,6 +117,7 @@ export class TimelineView extends Component { this.getStationsByGroupName = this.getStationsByGroupName.bind(this); this.setGroupByProject = this.setGroupByProject.bind(this); this.changeViewBlocks = this.changeViewBlocks.bind(this); + this.showReservationBlocks = this.showReservationBlocks.bind(this); } async componentDidMount() { @@ -488,9 +490,16 @@ export class TimelineView extends Component { suBlueprintList.push(suBlueprint); } } - if (this.state.stationView) { + if (this.state.stationView || this.state.showReservation) { items = this.addStationReservations(items, startTime, endTime); } + if (this.state.showReservation) { + let reservationGroup = [{id: "RESERVATION", parent: "RESERVATION", + start: moment.utc("1900-01-01", "YYYY-MM-DD"), + title: "RESERVATIONS"} + ]; + group = reservationGroup.concat(group); + } } else { suBlueprintList = _.clone(this.state.suBlueprints); group = this.state.group; @@ -547,6 +556,7 @@ export class TimelineView extends Component { */ addStationReservations(items, startTime, endTime) { let reservations = this.reservations; + let reservationItems = []; for (const reservation of reservations) { const reservationStartTime = moment.utc(reservation.start_time); const reservationEndTime = reservation.duration ? reservationStartTime.clone().add(reservation.duration, 'seconds') : endTime; @@ -559,13 +569,18 @@ export class TimelineView extends Component { || reservationEndTime.isBetween(startTime, endTime) || (reservationStartTime.isSameOrBefore(startTime) && reservationEndTime.isSameOrAfter(endTime))) - && (!this.state.reservationFilter || // No reservation filter added - reservationSpec.activity.type === this.state.reservationFilter)) { // Reservation reason == Filtered reaseon - if (reservationSpec.resources.stations) { - items = items.concat(this.getReservationItems(reservation, endTime)); + && (this.state.reservationFilter.length === 0 || // No reservation filter added + this.state.reservationFilter.indexOf(reservationSpec.activity.type) >= 0 )) { // Reservation reason == Filtered reaseon + if (this.state.stationView) { + if (reservationSpec.resources.stations) { + items = items.concat(this.getReservationItems(reservation, endTime, this.state.stationView)); + } + } else { + reservationItems.push(this.getReservationItems(reservation, endTime, this.state.stationView)[0]); } } } + items = reservationItems.concat(items); return items; } @@ -574,18 +589,33 @@ export class TimelineView extends Component { * @param {Object} reservation * @param {moment} endTime */ - getReservationItems(reservation, endTime) { + getReservationItems(reservation, endTime, stationView) { const reservationSpec = reservation.specifications_doc; let items = []; const start_time = moment.utc(reservation.start_time); const end_time = reservation.duration ? start_time.clone().add(reservation.duration, 'seconds') : endTime; - for (const station of reservationSpec.resources.stations) { + if (stationView) { + for (const station of reservationSpec.resources.stations) { + const blockColor = RESERVATION_COLORS[this.getReservationType(reservationSpec.schedulability)]; + let item = { + id: `Res-${reservation.id}-${station}`, + start_time: start_time, end_time: end_time, + name: reservationSpec.activity.type, project: reservation.project_id, + group: station, type: 'RESERVATION', + title: `${reservationSpec.activity.type}${reservation.project_id ? ("-" + reservation.project_id) : ""}`, + desc: reservation.description, + duration: reservation.duration ? UnitConverter.getSecsToHHmmss(reservation.duration) : "Unknown", + bgColor: blockColor.bgColor, selectedBgColor: blockColor.bgColor, color: blockColor.color + }; + items.push(item); + } + } else { const blockColor = RESERVATION_COLORS[this.getReservationType(reservationSpec.schedulability)]; let item = { - id: `Res-${reservation.id}-${station}`, + id: `Res-${reservation.id}`, start_time: start_time, end_time: end_time, name: reservationSpec.activity.type, project: reservation.project_id, - group: station, type: 'RESERVATION', + group: "RESERVATION", type: 'RESERVATION', title: `${reservationSpec.activity.type}${reservation.project_id ? ("-" + reservation.project_id) : ""}`, desc: reservation.description, duration: reservation.duration ? UnitConverter.getSecsToHHmmss(reservation.duration) : "Unknown", @@ -642,8 +672,8 @@ export class TimelineView extends Component { || reservationEndTime.isBetween(startTime, endTime) || (reservationStartTime.isSameOrBefore(startTime) && reservationEndTime.isSameOrAfter(endTime))) - && (!this.state.reservationFilter || // No reservation filter added - reservationSpec.activity.type === this.state.reservationFilter)) { // Reservation reason == Filtered reaseon + && (this.state.reservationFilter.length === 0 || // No reservation filter added + this.state.reservationFilter.indexOf(reservationSpec.activity.type) >= 0 )) { // Reservation reason == Filtered reaseon if (!this.state.stationView || (this.state.stationView && reservationSpec.resources.stations)) { let item = _.cloneDeep(reservation); @@ -761,9 +791,16 @@ export class TimelineView extends Component { } } } - if (this.state.stationView) { + if (this.state.stationView || this.state.showReservation) { items = this.addStationReservations(items, this.state.currentStartTime, this.state.currentEndTime); } + if (this.state.showReservation) { + let reservationGroup = [{id: "RESERVATION", parent: "RESERVATION", + start: moment.utc("1900-01-01", "YYYY-MM-DD"), + title: "RESERVATIONS"} + ]; + group = reservationGroup.concat(group); + } if (this.timeline) { this.timeline.updateTimeline({ group: this.state.stationView ? this.getStationsByGroupName() : _.orderBy(_.uniqBy(group, 'id'), ["parent", "start"], ['asc', 'asc']), items: items, stationView: this.state.stationView}); @@ -1013,6 +1050,15 @@ export class TimelineView extends Component { this.setState({isStationTasksVisible: !this.state.isStationTasksVisible}); } + /** + * Function sets the flag to show or hide reservation blocks in normal timeline view and the options is remembered. + */ + showReservationBlocks() { + this.timelineUIAttributes["showReservation"] = !this.state.showReservation; + this.timelineCommonUtils.storeUIAttributes(this.timelineUIAttributes); + this.setState({showReservation: !this.state.showReservation}); + } + render() { if (this.state.redirect) { return <Redirect to={{ pathname: this.state.redirect }}></Redirect> @@ -1094,7 +1140,7 @@ export class TimelineView extends Component { } </div> <div className={`timeline-view-toolbar p-grid ${this.state.stationView && 'alignTimeLineHeader'}`}> - <div className="sub-header col-lg-3"> + <div className="sub-header col-lg-2"> <label >Station View</label> <InputSwitch checked={this.state.stationView} onChange={(e) => { this.setStationView(e) }} /> </div> @@ -1119,22 +1165,11 @@ export class TimelineView extends Component { onChange={(e) => this.setSelectedStationGroup(e.value)} /> </div> - <div className="sub-header col-lg-3"> - <label style={{ marginLeft: '20px' }}>Reservation</label> - <Dropdown optionLabel="name" optionValue="name" - style={{ top: '2px' }} - value={this.state.reservationFilter} - options={this.reservationReasons} - filter showClear={true} filterBy="name" - onChange={(e) => { this.setReservationFilter(e.value) }} - placeholder="Reason" /> - - </div> </> } {!this.state.stationView && <> - <div className="sub-header col-lg-6"> + <div className="sub-header col-lg-5"> <div className={`sub-header-content ${isSUListVisible?"col-lg-12":"col-lg-8"}`} style={{padding: '0px !important'}}> <fieldset> <label style={{ marginLeft: '0px' }}>Show :</label> @@ -1163,7 +1198,7 @@ export class TimelineView extends Component { </div> } </div> - <div className="sub-header col-lg-3" style={{paddingTop: "15px !important"}}> + <div className="sub-header col-lg-2" style={{paddingTop: "15px !important"}}> {this.state.groupByProject && <Button className="p-button-rounded toggle-btn" label="Group By SU" onClick={e => this.setGroupByProject(false)} />} {!this.state.groupByProject && @@ -1171,6 +1206,35 @@ export class TimelineView extends Component { </div> </> } + <div className="sub-header col-lg-3"> + {!this.state.stationView && + <Button className="p-button-rounded toggle-btn" + tooltip={this.state.showReservation?"Hide Reservations":"Show Reservations"} + style={{minWidth: "50px"}} + label={this.state.showReservation?"Hide Reservation":"Show Reservation"} + onClick={e => this.showReservationBlocks()} /> + } + { this.state.stationView && + <label style={{ marginLeft: '20px' }}>Reservation</label> + } + { (this.state.stationView || this.state.showReservation) && + <> + <MultiSelect data-testid="reserv-reasons" id="reserv-reasons" + optionLabel="name" optionValue="name" + style={{ top: '2px', marginLeft:'5px', minWidth: '100px' }} + panelStyle={{right: '0px'}} + tooltip="Select Reservation Reason(s)" + value={this.state.reservationFilter} + options={this.reservationReasons} + maxSelectedLabels="1" + filter showClear={true} filterBy="name" + placeholder="Reason" + onChange={(e) => this.setReservationFilter(e.value)} + /> + </> + } + </div> + </div> <Timeline ref={(tl) => { this.timeline = tl }} @@ -1217,7 +1281,8 @@ export class TimelineView extends Component { <div className="col-lg-3 col-md-3 col-sm-12" style={{ borderLeft: "1px solid #efefef", marginTop: "0px", backgroundColor: "#f2f2f2" }}> {this.state.isSummaryLoading ? <AppLoader /> : - <ReservationSummary reservation={reservation} closeCallback={this.closeSUDets}></ReservationSummary> + <ReservationSummary reservation={reservation} viewInNewWindow={true} + closeCallback={this.closeSUDets}></ReservationSummary> } </div> } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js index fc4d5e0be02873c7a6a9888bcc0d5c7d137d7d44..c579ad91966f3ef76b8c73d39c3894fd762bb70f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Workflow/workflow.list.js @@ -16,9 +16,12 @@ import UtilService from '../../services/util.service'; class WorkflowList extends Component{ lsKeySortColumn = 'SortDataWorkflowList'; defaultSortColumn = []; + SU_BLUEPRINT_EXPAND = 'draft,draft.scheduling_set,draft.scheduling_set.project,draft.observation_strategy_template'; + constructor(props) { super(props); this.setToggleBySorting(); + this.workflowUIAttr = UtilService.localStore({ type: 'get', key: 'WORKFLOW_UI_ATTR' }) || {}; this.state={ ftAssignstatus: '', activeWorkflow: null, @@ -36,11 +39,16 @@ class WorkflowList extends Component{ name: "Scheduling Unit Status", filter: "select", }, + project: "Project", assignedTo: "Assigned To", - lastTaskName:"Current Workflow Stage", + lastTaskName: { + name:"Current Workflow Stage", + filter: "multiselect-filter" + }, + updated_at: { name:"Updated At", - filter:"date", + filter:"dateRange", format:UIConstants.CALENDAR_DATETIME_FORMAT }, actionpath: "actionpath" @@ -79,21 +87,27 @@ class WorkflowList extends Component{ this.setToggleBySorting(); const promises = [ WorkflowService.getWorkflowProcesses(), WorkflowService.getWorkflowTasks(), - ScheduleService.getSchedulingUnitBlueprint(),]; + ScheduleService.getSchedulingUnitsExpandWithFilter('blueprint',this.SU_BLUEPRINT_EXPAND, '', 10, 0), + ]; Promise.all(promises).then(async responses => { this.workflowProcessList = responses[0]; this.workflowTasksList = responses[1]; this.schedulingUnitList = responses[2].data.results; this.prepareWorkflowProcesslist(); }); + + } toggleBySorting = (sortData) => { - UtilService.localStore({ type: 'set', key: this.lsKeySortColumn, value: sortData }); + + this.storeUIAttr(this.lsKeySortColumn, sortData) } setToggleBySorting() { - let sortData = UtilService.localStore({ type: 'get', key: this.lsKeySortColumn }); + this.workflowUIAttr = UtilService.localStore({ type: 'get', key: 'WORKFLOW_UI_ATTR' }); + let sortData = this.workflowUIAttr[this.lsKeySortColumn]; + if (sortData) { if (Object.prototype.toString.call(sortData) === '[object Array]') { this.defaultSortColumn = sortData; @@ -103,21 +117,16 @@ class WorkflowList extends Component{ } } this.defaultSortColumn = this.defaultSortColumn || []; - UtilService.localStore({ type: 'set', key: this.lsKeySortColumn, value: [...this.defaultSortColumn] }); + + this.storeUIAttr(this.lsKeySortColumn, [...this.defaultSortColumn]) } - /** - * Get Option-list values for Select Dropdown filter in 'Viewtable' - * @param {String} id : Column id - * @returns - */ - getFilterOptions(id) { - let options = null; - if(id && id === 'Scheduling Unit Status') { - options = UIConstants.SU_STATUS; - } - return options; + storeUIAttr(key, value){ + this.workflowUIAttr[key] = value + UtilService.localStore({ type: 'set', key: 'WORKFLOW_UI_ATTR', value: this.workflowUIAttr }) } + + /** * Prepare Workflow Process data @@ -126,9 +135,10 @@ class WorkflowList extends Component{ let workflowProcessList = []; if (this.workflowProcessList) { for(const wfSU of this.workflowProcessList) { - const schedulingUnit = _.find(this.schedulingUnitList, {'id': wfSU.su}); + const schedulingUnit = _.find(this.schedulingUnitList, {'id': wfSU.su}); if(schedulingUnit) { wfSU['suName'] = schedulingUnit.name; + wfSU['project'] = schedulingUnit.draft.scheduling_set.project.name; wfSU['status'] = schedulingUnit.status; } const workflowTasks = _.orderBy(this.workflowTasksList.filter(item => item.process === wfSU.id), ['id'], ['desc']); @@ -141,7 +151,7 @@ class WorkflowList extends Component{ //let currenttask = await WorkflowService.getCurrentTask(wfSU.id); //wfSU['owner'] = (currenttask && currenttask.fields.owner)? currenttask.fields.owner : ''; wfSU['owner'] = workflowTask.owner? workflowTask.owner : ''; - wfSU['assignedTo'] = workflowTask.owner? workflowTask.owner : '';; + wfSU['assignedTo'] = workflowTask.owner? workflowTask.owner : ''; } //TODO: this code commented and can be used to show only current task owner details when filter enabled @@ -158,7 +168,13 @@ class WorkflowList extends Component{ workflowProcessList.push(wfSU); } } - this.setState({workflowProcessList: workflowProcessList, filteredWorkflowProcessList:workflowProcessList,isLoading: false}); + + this.workflowUIAttr= UtilService.localStore({ type: 'get', key: 'WORKFLOW_UI_ATTR' }); + let ftAssignstatus = this.workflowUIAttr.ftAssignstatus; + let activeWorkflow = this.workflowUIAttr.activeWorkflow || this.state.activeWorkflow; + let showActiveStage = this.workflowUIAttr.showActiveStage ; + this.setState({ftAssignstatus: ftAssignstatus?ftAssignstatus: [] , activeWorkflow:activeWorkflow, showActiveStage:showActiveStage,workflowProcessList: workflowProcessList, filteredWorkflowProcessList:workflowProcessList,isLoading: false }); + this.filterTableData(); } /** @@ -168,12 +184,30 @@ class WorkflowList extends Component{ this.props.history.goBack(); } + /** + * Get Option-list values for Select Dropdown filter in 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getFilterOptions(id) { + let options = null; + if(id && id === 'Current Workflow Stage') { + options = _.map(UIConstants.CURRENT_WORKFLOW_STAGE, stage=> {return {name: stage, value: stage}}); + } + else if(id && id === 'Scheduling Unit Status') { + options = UIConstants.SU_STATUS; + } + return options; + } + /** * Filter the table data with drop down selected * @param {String} value - Drop Down filter value */ async assignStatusChangeEvent(value) { await this.setState({'ftAssignstatus': value}); + + this.storeUIAttr('ftAssignstatus', value) //TODO: commented this code. It requires user details, waiting for API changes //await this.filterTableData(); } @@ -182,7 +216,7 @@ class WorkflowList extends Component{ * Filter the table data with checkbox value, it will fetch active workflow stagerows if the checkbox selected * @param {Boolena} value */ - async setWorkflowActiveState(value) { + async setWorkflowActiveState(value) { let showActiveStage = ''; if(value === null) { showActiveStage = 'Show Active Workflow Process'; @@ -192,6 +226,10 @@ class WorkflowList extends Component{ showActiveStage = 'Show All Workflow Process'; } await this.setState({'activeWorkflow': value, showActiveStage: showActiveStage}); + + this.storeUIAttr('activeWorkflow', value) + + this.storeUIAttr('showActiveStage', showActiveStage ) await this.filterTableData(); } @@ -250,7 +288,7 @@ class WorkflowList extends Component{ <PageHeader location={this.props.location} title={'Workflow - List'} actions={[{icon: 'fa-window-close', title:'Click to Close Workflow - List', type: 'button', actOn: 'click', props:{ callback: this.close }}]}/> - <div> + <div style={{marginTop: '15px'}}> {this.state.isLoading ? <AppLoader/> : (this.state.workflowProcessList.length>0 )? <> <div className="p-select " style={{position: 'relative', marginLeft: '27em', marginTop: '-2em'}}> @@ -295,9 +333,9 @@ class WorkflowList extends Component{ showColumnFilter={true} showFilterOption={this.getFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter lsKeySortColumn={this.lsKeySortColumn} - toggleBySorting={(sortData) => this.toggleBySorting(sortData)} - defaultSortColumn= {this.defaultSortColumn} + toggleBySorting={(sortData) => this.toggleBySorting(sortData)} pageUpdated={this.pageUpdated} + storeFilter={true} /> </> :<div>No Workflow Process SU found</div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/table.util.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/table.util.js new file mode 100644 index 0000000000000000000000000000000000000000..aaf33d71cd15f7569d1ae34a527f21ae2471ffdb --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/table.util.js @@ -0,0 +1,60 @@ +import { find, findIndex, remove } from 'lodash'; + +import UtilService from '../services/util.service' + +/** Utility to handle table filter in localstorage */ +const TableUtil = { + /** + * Function to store the filter values applied in a table against each column/header + * @param {String} tableName + * @param {String} headerName + * @param {any} updatedValue + */ + saveFilter: (tableName, headerName, updatedValue) => { + let tableStore = UtilService.localStore({ type: 'get', key: tableName }); + if(tableStore && tableStore.length>0) { + let columnFilter = {id: headerName, value: updatedValue}; + let index = findIndex(tableStore, ['id', headerName]); + if (index >= 0) { + tableStore.splice(index, 1, columnFilter); + } else { + tableStore.push(columnFilter); + } + } else { + tableStore = [{id: headerName, value: updatedValue}] + } + UtilService.localStore({ type: 'set', key: tableName, value: tableStore }); + }, + /** + * Get the stored filtered value of the header in the table + * @param {String} tableName + * @param {String} headerName + * @returns any + */ + getFilter: (tableName, headerName) => { + let tableStore = UtilService.localStore({ type: 'get', key: tableName }); + let columnFilter = tableStore?find(tableStore, ['id', headerName]):null; + return columnFilter?columnFilter.value:""; + }, + /** + * Remove the filter value of the column in the table + * @param {String} tableName + * @param {String} columnName + */ + clearColumnFilter: (tableName, columnName) => { + let tableFilters = UtilService.localStore({type: 'get', key: tableName}); + if (tableFilters) { + remove(tableFilters, filter => filter.id === columnName); + UtilService.localStore({type: 'set', key: tableName, value: tableFilters}); + } + }, + /** + * Remove all the values stored for the table + * @param {String} tableName + */ + clearTableFilter: (tableName) => { + UtilService.localStore({type: 'remove', key: tableName}); + } +} + +export default TableUtil; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js index 8768347688b9579a43ec2e6a0f673dc08f0fda60..649d3d069c973b2d3fd8edc3a172ca57e653fa7c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js @@ -28,6 +28,7 @@ const UIConstants = { 'NumberFilter':'', 'PropertyIsoDateTimeFromToRangeFilter':'', }, - SU_STATUS:['cancelled', 'error', 'defining', 'defined', 'schedulable', 'scheduled','started', 'observing', 'observed', 'processing', 'processed', 'ingesting','finished', 'unschedulable'] + SU_STATUS:['cancelled', 'error', 'defining', 'defined', 'schedulable', 'scheduled','started', 'observing', 'observed', 'processing', 'processed', 'ingesting','finished', 'unschedulable'], + CURRENT_WORKFLOW_STAGE:['Waiting To Be Scheduled','Scheduled','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingesting','Unpin Data','Done'] } export default UIConstants; \ No newline at end of file