Skip to content

Commit

Permalink
feat: add capture tags to Captures table (Greenstand#237)
Browse files Browse the repository at this point in the history
* feat: add capture tags to Captures table

* feat(captures table): add capture tags to captures table

* fix: ensure tag lookup is populated

Co-authored-by: Nick Charlton <[email protected]>
  • Loading branch information
2 people authored and mitia-anah committed Feb 1, 2022
1 parent 8f8ea9d commit eeb084e
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 133 deletions.
3 changes: 0 additions & 3 deletions cypress/component/FilterTop.spec.py.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ describe('FilterTop', () => {
state: {
tagList: [],
},
effects: {
getTags(_payload, _state) {},
},
},
organizations: {
state: {
Expand Down
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 16 additions & 8 deletions src/api/treeTrackerApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,8 @@ export default {
/*
* get tag list
*/
getTags(filter, abortController) {
const filterString =
`filter[limit]=25&` +
(filter ? `filter[where][tagName][ilike]=${filter}%` : '');
getTags(abortController) {
const filterString = `filter[order]=tagName`;
const query = `${process.env.REACT_APP_API_ROOT}/api/tags?${filterString}`;
return fetch(query, {
method: 'GET',
Expand Down Expand Up @@ -355,10 +353,20 @@ export default {
/*
* get tags for a given tree
*/
getCaptureTags({ captureId, tagId }) {
const filterString =
(captureId ? `filter[where][treeId]=${captureId}` : '') +
(tagId ? `&filter[where][tagId]=${tagId}` : '');
getCaptureTags({ captureIds, tagIds }) {
const useAnd = captureIds && tagIds;
const captureIdClauses = (captureIds || []).map(
(id, index) =>
`filter[where]${useAnd ? '[and][0]' : ''}[or][${index}][treeId]=${id}`,
);
const tagIdClauses = (tagIds || []).map(
(id, index) =>
`filter[where][and]${
useAnd ? '[and][1]' : ''
}[or][${index}][tagId]=${id}`,
);

const filterString = [...captureIdClauses, ...tagIdClauses].join('&');
const query = `${process.env.REACT_APP_API_ROOT}/api/tree_tags?${filterString}`;
return fetch(query, {
method: 'GET',
Expand Down
14 changes: 6 additions & 8 deletions src/components/CaptureFilter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useState, useContext } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
Expand Down Expand Up @@ -101,12 +101,6 @@ function Filter(props) {
);
const [tokenId, setTokenId] = useState(filter?.tokenId || filterOptionAll);

useEffect(() => {
const abortController = new AbortController();
tagsContext.getTags(tagSearchString, { signal: abortController.signal });
return () => abortController.abort();
}, [tagSearchString]);

const handleDateStartChange = (date) => {
setDateStart(date);
};
Expand Down Expand Up @@ -360,7 +354,11 @@ function Filter(props) {
// active: true,
// public: true,
// },
...tagsContext.tagList,
...tagsContext.tagList.filter((t) =>
t.tagName
.toLowerCase()
.startsWith(tagSearchString.toLowerCase()),
),
]}
value={tag}
defaultValue={'Not set'}
Expand Down
38 changes: 7 additions & 31 deletions src/components/CaptureTags.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useState, useCallback, useContext } from 'react';
import React, { useState, useContext } from 'react';
import ChipInput from 'material-ui-chip-input';
import Autosuggest from 'react-autosuggest';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import * as _ from 'lodash';
import { TagsContext } from '../context/TagsContext';

const useStyles = makeStyles((theme) => ({
Expand Down Expand Up @@ -67,21 +66,12 @@ function getSuggestionValue(suggestion) {
return suggestion.tagName;
}

// This is needed to pass the same function to debounce() each time
const debounceCallback = ({ value, callback }) => {
return callback && callback(value);
};

const CaptureTags = (props) => {
// console.log('render: capture tags');
const classes = useStyles(props);
const tagsContext = useContext(TagsContext);
const [textFieldInput, setTextFieldInput] = useState('');
const [error, setError] = useState(false);
const debouncedInputHandler = useCallback(
_.debounce(debounceCallback, 250),
[],
);
const TAG_PATTERN = '^\\w*$';

function renderInput(inputProps) {
Expand Down Expand Up @@ -113,34 +103,20 @@ const CaptureTags = (props) => {

const isValidTagString = (value) => RegExp(TAG_PATTERN).test(value);

let handleSuggestionsFetchRequested = ({ value }) => {
debouncedInputHandler({
value,
callback: (val) => {
if (isValidTagString(val)) {
return tagsContext.getTags(val);
}
return null;
},
});
};

let handleSuggestionsClearRequested = () => {};

let handletextFieldInputChange = (event, { newValue }) => {
const handletextFieldInputChange = (event, { newValue }) => {
setTextFieldInput(newValue);
setError(!isValidTagString(newValue));
};

let handleBeforeAddChip = () => {
const handleBeforeAddChip = () => {
return !error;
};

let handleAddChip = (chip) => {
const handleAddChip = (chip) => {
tagsContext.setTagInput(tagsContext.tagInput.concat([chip]));
};

let handleDeleteChip = (_chip, index) => {
const handleDeleteChip = (_chip, index) => {
const temp = tagsContext.tagInput;
temp.splice(index, 1);
tagsContext.setTagInput(temp);
Expand All @@ -164,8 +140,8 @@ const CaptureTags = (props) => {
!tagsContext.tagInput.find((i) => i.toLowerCase() === tagName)
);
})}
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
onSuggestionsClearRequested={handleSuggestionsClearRequested}
onSuggestionsFetchRequested={() => {}}
onSuggestionsClearRequested={() => {}}
renderSuggestionsContainer={renderSuggestionsContainer}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
Expand Down
86 changes: 76 additions & 10 deletions src/components/Captures/CaptureTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { tokenizationStates } from '../../common/variables';
import useStyle from './CaptureTable.styles.js';
import ExportCaptures from 'components/ExportCaptures';
import { CaptureDetailProvider } from '../../context/CaptureDetailContext';
import { TagsContext } from 'context/TagsContext';
import api from '../../api/treeTrackerApi';

const columns = [
{
Expand Down Expand Up @@ -62,6 +64,11 @@ const columns = [
renderer: (val) =>
val ? tokenizationStates.TOKENIZED : tokenizationStates.NOT_TOKENIZED,
},
{
attr: 'captureTags',
label: 'Capture Tags',
noSort: true,
},
{
attr: 'timeCreated',
label: 'Created',
Expand All @@ -86,21 +93,58 @@ const CaptureTable = () => {
getCaptureAsync,
} = useContext(CapturesContext);
const speciesContext = useContext(SpeciesContext);
const tagsContext = useContext(TagsContext);
const [isDetailsPaneOpen, setIsDetailsPaneOpen] = useState(false);
const [speciesState, setSpeciesState] = useState({});
const [speciesLookup, setSpeciesLookup] = useState({});
const [tagLookup, setTagLookup] = useState({});
const [captureTagLookup, setCaptureTagLookup] = useState({});
const [isOpenExport, setOpenExport] = useState(false);
const classes = useStyle();

useEffect(() => {
formatSpeciesData();
}, [filter]);
populateSpeciesLookup();
}, [speciesContext.speciesList]);

const formatSpeciesData = async () => {
useEffect(() => {
populateTagLookup();
}, [tagsContext.tagList]);

useEffect(async () => {
// Don't do anything if there are no captures
if (!captures?.length) {
return;
}

// Get the capture tags for all of the displayed captures
const captureTags = await api.getCaptureTags({
captureIds: captures.map((c) => c.id),
});

// Populate a lookup for quick access when rendering the table
let lookup = {};
captureTags.forEach((captureTag) => {
if (!lookup[captureTag.treeId]) {
lookup[captureTag.treeId] = [];
}
lookup[captureTag.treeId].push(tagLookup[captureTag.tagId]);
});
setCaptureTagLookup(lookup);
}, [captures, tagLookup]);

const populateSpeciesLookup = async () => {
let species = {};
speciesContext.speciesList.map((s) => {
speciesContext.speciesList.forEach((s) => {
species[s.id] = s.name;
});
setSpeciesState(species);
setSpeciesLookup(species);
};

const populateTagLookup = async () => {
let tags = {};
tagsContext.tagList.forEach((t) => {
tags[t.id] = t.tagName;
});
setTagLookup(tags);
};

const toggleDrawer = (id) => {
Expand Down Expand Up @@ -179,7 +223,7 @@ const CaptureTable = () => {
handleClose={() => setOpenExport(false)}
columns={columns}
filter={filter}
speciesState={speciesState}
speciesLookup={speciesLookup}
/>
{tablePagination()}
</Grid>
Expand Down Expand Up @@ -214,7 +258,13 @@ const CaptureTable = () => {
>
{columns.map(({ attr, renderer }) => (
<TableCell key={attr}>
{formatCell(capture, speciesState, attr, renderer)}
{formatCell(
capture,
speciesLookup,
captureTagLookup[capture.id] || [],
attr,
renderer,
)}
</TableCell>
))}
</TableRow>
Expand All @@ -233,7 +283,13 @@ const CaptureTable = () => {
);
};

export const formatCell = (capture, speciesState, attr, renderer) => {
export const formatCell = (
capture,
speciesLookup,
additionalTags,
attr,
renderer,
) => {
if (attr === 'id' || attr === 'planterId') {
return (
<LinkToWebmap
Expand All @@ -242,11 +298,21 @@ export const formatCell = (capture, speciesState, attr, renderer) => {
/>
);
} else if (attr === 'speciesId') {
return capture[attr] === null ? '--' : speciesState[capture[attr]];
return capture[attr] === null ? '--' : speciesLookup[capture[attr]];
} else if (attr === 'verificationStatus') {
return capture['active'] === null || capture['approved'] === null
? '--'
: getVerificationStatus(capture['active'], capture['approved']);
} else if (attr === 'captureTags') {
return [
capture.age,
capture.morphology,
capture.captureApprovalTag,
capture.rejectionReason,
...additionalTags,
]
.filter((tag) => tag !== null)
.join(', ');
} else {
return renderer ? renderer(capture[attr]) : capture[attr];
}
Expand Down
Loading

0 comments on commit eeb084e

Please sign in to comment.