Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merge: update branch with latest dev #5810

Merged
merged 5 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, { useState } from 'react';
import { Autocomplete } from './Autocomplete';
import { useVizConfigContext } from '../../context';
import { useEntityByCode, useLocations } from '../../api';
import { EntityOptionLabel } from '../../../widgets';

export const LocationField = () => {
const [locationSearch, setLocationSearch] = useState('');
Expand Down Expand Up @@ -39,7 +40,9 @@ export const LocationField = () => {
setLocationSearch(newValue);
}}
getOptionLabel={option => option.name}
renderOption={option => <span>{option.name}</span>}
renderOption={option => {
return <EntityOptionLabel {...option} />;
}}
onChange={(_, newLocation) => {
setLocation(newLocation);
}}
Expand Down
3 changes: 3 additions & 0 deletions packages/admin-panel/src/autocomplete/Autocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const Autocomplete = props => {
canCreateNewOptions,
allowMultipleValues,
optionLabelKey,
renderOption,
muiProps,
error,
tooltip,
Expand Down Expand Up @@ -110,6 +111,7 @@ export const Autocomplete = props => {
options={options}
getOptionSelected={getOptionSelected}
getOptionLabel={getOptionLabel}
renderOption={renderOption}
loading={isLoading}
onChange={onChangeSelection}
onInputChange={(event, newValue) => {
Expand All @@ -134,6 +136,7 @@ Autocomplete.propTypes = {
options: PropTypes.array.isRequired,
getOptionSelected: PropTypes.func.isRequired,
getOptionLabel: PropTypes.func.isRequired,
renderOption: PropTypes.func,
isLoading: PropTypes.bool,
onChangeSelection: PropTypes.func.isRequired,
onChangeSearchTerm: PropTypes.func,
Expand Down
13 changes: 13 additions & 0 deletions packages/admin-panel/src/autocomplete/ReduxAutocomplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { connect } from 'react-redux';
import { getAutocompleteState } from './selectors';
import { changeSelection, changeSearchTerm, clearState } from './actions';
import { Autocomplete } from './Autocomplete';
import { EntityOptionLabel } from '../widgets';

const getPlaceholder = (placeholder, selection) => {
if (selection && selection.length) {
Expand Down Expand Up @@ -41,6 +42,8 @@ const ReduxAutocompleteComponent = ({
error,
tooltip,
optionValueKey,
renderOption,
optionFields,
}) => {
const [hasUpdated, setHasUpdated] = React.useState(false);
React.useEffect(() => {
Expand Down Expand Up @@ -69,6 +72,12 @@ const ReduxAutocompleteComponent = ({
selectedValue = [];
}

const getOptionRendered = option => {
if (renderOption) return renderOption(option);
if (!option || !option[optionLabelKey]) return '';
return option[optionLabelKey];
};

return (
<Autocomplete
value={selectedValue}
Expand All @@ -88,6 +97,7 @@ const ReduxAutocompleteComponent = ({
required={required}
error={error}
tooltip={tooltip}
renderOption={getOptionRendered}
/>
);
};
Expand All @@ -111,6 +121,7 @@ ReduxAutocompleteComponent.propTypes = {
required: PropTypes.bool,
error: PropTypes.bool,
optionValueKey: PropTypes.string.isRequired,
renderOption: PropTypes.func,
};

ReduxAutocompleteComponent.defaultProps = {
Expand Down Expand Up @@ -149,6 +160,7 @@ const mapDispatchToProps = (
baseFilter,
pageSize,
distinct,
optionFields,
},
) => ({
programaticallyChangeSelection: initialValue => {
Expand Down Expand Up @@ -199,6 +211,7 @@ const mapDispatchToProps = (
baseFilter,
pageSize,
distinct,
optionFields,
),
),
onClearState: () => dispatch(clearState(reduxId)),
Expand Down
3 changes: 2 additions & 1 deletion packages/admin-panel/src/autocomplete/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const changeSearchTerm =
baseFilter = {},
pageSize = MAX_AUTOCOMPLETE_RESULTS,
distinct = null,
columns = null,
) =>
async (dispatch, getState, { api }) => {
const fetchId = generateId();
Expand All @@ -46,7 +47,7 @@ export const changeSearchTerm =
filter: JSON.stringify(filter),
pageSize,
sort: JSON.stringify([`${labelColumn} ASC`]),
columns: JSON.stringify([labelColumn, valueColumn]),
columns: JSON.stringify(columns ? columns : [labelColumn, valueColumn]),
distinct,
});
dispatch({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DateTimePicker, RadioGroup } from '@tupaia/ui-components';
import { stripTimezoneFromDate } from '@tupaia/utils';
import { ReduxAutocomplete } from '../autocomplete';
import { ExportModal } from './ExportModal';
import { EntityOptionLabel } from '../widgets';

const MODES = {
COUNTRY: { value: 'country', formInput: 'countryCode' },
Expand Down Expand Up @@ -92,6 +93,8 @@ export const SurveyResponsesExportModal = () => {
endpoint="entities"
optionLabelKey="name"
optionValueKey="id"
renderOption={option => <EntityOptionLabel {...option} />}
optionFields={['id', 'code', 'name']}
allowMultipleValues
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import { ArrayFilter } from '../../table/columnTypes/columnFilters';
import { prettyArray } from '../../utilities';
import { EntityOptionLabel } from '../../widgets';

const RESOURCE_NAME = { singular: 'dashboard mailing list' };

Expand Down Expand Up @@ -52,6 +54,8 @@ const DASHBOARD_MAILING_LIST_FIELDS = {
optionLabelKey: 'name',
optionValueKey: 'id',
labelTooltip: 'Select the entity this dashboard mailing list should be for',
renderOption: option => <EntityOptionLabel {...option} />,
optionFields: ['id', 'code', 'name'],
},
},
admin_permission_groups: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export const visualisationsTabRoutes = {
dashboards,
dashboardRelations,
dashboardMailingLists,
dataTables,
legacyReports,
mapOverlays,
mapOverlayGroups,
mapOverlayGroupRelations,
indicators,
dataTables,
socialFeed,
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const FileQuestionField = ({ value: uniqueFileName, onChange, label, maxS

const api = useApiContext();
const downloadFile = async () => {
await api.download(`downloadFiles`, { uniqueFileNames: uniqueFileName }, fileName);
await api.download('downloadFiles', { uniqueFileNames: uniqueFileName }, fileName);
};

return (
Expand Down
4 changes: 4 additions & 0 deletions packages/admin-panel/src/surveyResponse/ResponseFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { format } from 'date-fns';
import { Autocomplete } from '../autocomplete';
import { useDebounce } from '../utilities';
import { useEntities } from '../VizBuilderApp/api';
import { EntityOptionLabel } from '../widgets';

const SectionWrapper = styled.div`
display: grid;
Expand Down Expand Up @@ -81,6 +82,9 @@ export const ResponseFields = ({
return option.id === selected.id;
}}
getOptionLabel={option => option?.name || ''}
renderOption={option => {
return <EntityOptionLabel {...option} />;
}}
isLoading={entityIsLoading}
onChangeSelection={(event, selectedValue) => {
if (!selectedValue) {
Expand Down
3 changes: 2 additions & 1 deletion packages/admin-panel/src/theme/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export const LIGHT_ORANGE = '#FFECE1';
// Greys (based on first 2 letters of hex code)
export const GREY_72 = '#727D84';
export const GREY_9F = '#9FA6AA';
export const GREY_B8 = '#B8B8B8';
export const GREY_DE = '#DEDEDE';
export const GREY_E2 = '#E2E2E2';
export const GREY_F1 = '#F1F1F1';
export const GREY_FB = '#FBF9F9';
export const GREY_DE = '#DEDEDE';

// Blues
export const BLUE_BF = '#BFD5E4';
Expand Down
31 changes: 31 additions & 0 deletions packages/admin-panel/src/widgets/EntityOptionLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import styled from 'styled-components';

const StyledEntityOptionLabel = styled.div`
display: flex;
flex-direction: column;
`;

const Name = styled.span`
font-style: ${props => props.theme.typography.fontWeightBold};
color: ${props => props.theme.palette.text.primary};
`;

const Code = styled.span`
margin-top: 0.25rem;
color: ${props => props.theme.palette.text.secondary};
`;

export const EntityOptionLabel = ({ name, code }) => {
return (
<StyledEntityOptionLabel>
<Name>{name}</Name>
<Code>{code}</Code>
</StyledEntityOptionLabel>
);
};
6 changes: 1 addition & 5 deletions packages/admin-panel/src/widgets/InputField/JsonEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ export const JsonEditor = ({
required,
tooltip,
}) => {
if (!value) {
return null;
}

let editorValue = value;

if (typeof value === 'string') {
Expand All @@ -64,7 +60,7 @@ export const JsonEditor = ({
mainMenuBar={false}
statusBar={false}
mode="code"
onChange={json => onChange(inputKey, stringify ? JSON.stringify(json) : json)}
onChange={json => onChange(inputKey, stringify ? JSON.stringify(json ?? {}) : json)}
value={editorValue}
htmlElementProps={{
className: 'jsoneditor-parent',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export const registerInputFields = () => {
endpoint={props.optionsEndpoint}
optionLabelKey={props.optionLabelKey}
optionValueKey={props.optionValueKey}
optionFields={props.optionFields}
renderOption={props.renderOption}
reduxId={props.inputKey}
onChange={inputValue => props.onChange(props.inputKey, inputValue)}
canCreateNewOptions={props.canCreateNewOptions}
Expand Down
1 change: 1 addition & 0 deletions packages/admin-panel/src/widgets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export {
export { JsonEditor, JsonTreeEditor } from './JsonEditor';
export { SecondaryNavbar } from '../layout/navigation/SecondaryNavbar';
export { ConfirmDeleteModal } from './ConfirmDeleteModal';
export { EntityOptionLabel } from './EntityOptionLabel';
2 changes: 1 addition & 1 deletion packages/central-server/src/apiV2/GETHandler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const constructJoinCondition = (recordType, baseRecordType, customJoinConditions
joinType,
};
if (join?.through) {
if ('nearTableKey' in join !== true || 'farTableKey' in join !== true) {
if (!('nearTableKey' in join) || !('farTableKey' in join)) {
throw new ValidationError(`Incorrect format for customJoinConditions: ${recordType}`);
}
const nearTable = join.nearTableKey.split('.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,13 @@ describe('processSurveyResponse', () => {
});
});

it('should not add to recent_entities when type is entity question is not filled in', async () => {
it('should not add to recent_entities when type is entity question and is not filled in', async () => {
const result = await processSurveyResponse(mockModels, {
...responseData,
questions: [
{
questionId: 'question1',
type: QuestionType.PrimaryEntity,
type: QuestionType.Entity,
componentNumber: 1,
text: 'question1',
screenId: 'screen1',
Expand All @@ -469,6 +469,27 @@ describe('processSurveyResponse', () => {
expect(result.recent_entities).toEqual([]);
});

it('throw an error when type is primary entity question and is not filled in', async () => {
try {
const result = await processSurveyResponse(mockModels, {
...responseData,
questions: [
{
questionId: 'question1',
type: QuestionType.PrimaryEntity,
componentNumber: 1,
text: 'question1',
screenId: 'screen1',
config: {},
},
],
answers: {},
});
} catch (error: any) {
expect(error.message).toBe('Primary Entity Question is a required field');
}
});

it('should use the country id for new entities if parent id is not filled in', async () => {
const result = await processSurveyResponse(mockModels, {
...responseData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export const processSurveyResponse = async (
surveyResponse.qr_codes_to_create?.push(entityObj);
}
}
if (type === QuestionType.PrimaryEntity && !answer) {
throw new Error(`Primary Entity Question is a required field`);
}
if (answer) {
if (typeof answer !== 'string') {
throw new Error(
Expand Down
31 changes: 25 additions & 6 deletions packages/datatrak-web/src/components/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,39 @@ const SelectedOption = styled(OptionWrapper)`
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 0.425rem;
padding-right: 0.425rem;
margin-left: 0.45rem;
margin-right: 0.45rem;
padding-inline: 0.425rem;
margin-inline: 0.45rem;
border-radius: 3px;
border: 1px solid ${({ theme }) => theme.palette.primary.main};
.MuiSvgIcon-root {
font-size: 1.2rem;
}
`;

const Label = styled.span`
font-style: ${props => props.theme.typography.fontWeightBold};
color: ${props => props.theme.palette.text.primary};
`;

const Code = styled.span`
margin-inline: 0.45rem;
padding-left: 0.45rem;
border-left: 1px solid ${props => props.theme.palette.text.secondary};
color: ${props => props.theme.palette.text.secondary};
flex: 1;
`;

const DisplayOption = ({ option, state }) => {
const { selected } = state;
const label = typeof option === 'string' ? option : option.label || option.value;
const label =
typeof option === 'string' ? (
option
) : (
<>
<Label>{option.label || option.value}</Label>
{option.secondaryLabel ? <Code>{option.secondaryLabel}</Code> : null}
</>
);

if (selected)
return (
Expand All @@ -66,9 +85,9 @@ const DisplayOption = ({ option, state }) => {

export const Autocomplete = styled(BaseAutocomplete).attrs(props => ({
muiProps: {
...(props.muiProps || {}),
renderOption: (option, state) => <DisplayOption option={option} state={state} />,
PaperComponent: StyledPaper,
...(props.muiProps || {}),
},
}))`
width: 100%;
Expand Down
Loading