diff --git a/docs/generated/PROTOCOL_BUFFERS.md b/docs/generated/PROTOCOL_BUFFERS.md
index 4639dc28c..8cc0e1061 100644
--- a/docs/generated/PROTOCOL_BUFFERS.md
+++ b/docs/generated/PROTOCOL_BUFFERS.md
@@ -3,6 +3,9 @@
## Table of Contents
+- [column.proto](#column-proto)
+ - [Column](#tanagra-Column)
+
- [criteriaselector/configschema/attribute.proto](#criteriaselector_configschema_attribute-proto)
- [Attribute](#tanagra-configschema-Attribute)
@@ -11,7 +14,6 @@
- [criteriaselector/configschema/entity_group.proto](#criteriaselector_configschema_entity_group-proto)
- [EntityGroup](#tanagra-configschema-EntityGroup)
- - [EntityGroup.Column](#tanagra-configschema-EntityGroup-Column)
- [EntityGroup.EntityGroupConfig](#tanagra-configschema-EntityGroup-EntityGroupConfig)
- [criteriaselector/configschema/multi_attribute.proto](#criteriaselector_configschema_multi_attribute-proto)
@@ -20,6 +22,10 @@
- [criteriaselector/configschema/output_unfiltered.proto](#criteriaselector_configschema_output_unfiltered-proto)
- [OutputUnfiltered](#tanagra-configschema-OutputUnfiltered)
+- [criteriaselector/configschema/survey.proto](#criteriaselector_configschema_survey-proto)
+ - [Survey](#tanagra-configschema-Survey)
+ - [Survey.EntityGroupConfig](#tanagra-configschema-Survey-EntityGroupConfig)
+
- [criteriaselector/configschema/text_search.proto](#criteriaselector_configschema_text_search-proto)
- [TextSearch](#tanagra-configschema-TextSearch)
@@ -50,6 +56,10 @@
- [criteriaselector/dataschema/output_unfiltered.proto](#criteriaselector_dataschema_output_unfiltered-proto)
- [OutputUnfiltered](#tanagra-dataschema-OutputUnfiltered)
+- [criteriaselector/dataschema/survey.proto](#criteriaselector_dataschema_survey-proto)
+ - [Survey](#tanagra-dataschema-Survey)
+ - [Survey.Selection](#tanagra-dataschema-Survey-Selection)
+
- [criteriaselector/dataschema/text_search.proto](#criteriaselector_dataschema_text_search-proto)
- [TextSearch](#tanagra-dataschema-TextSearch)
- [TextSearch.Selection](#tanagra-dataschema-TextSearch-Selection)
@@ -93,6 +103,42 @@
+
+
Top
+
+## column.proto
+
+
+
+
+
+### Column
+Defines a column in the UI.
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| key | [string](#string) | | A unique key for the column. By default, used to look up attributes in the displayed data. |
+| width_string | [string](#string) | | Passed directly to the style of the column. "100%" can be used to take up space remaining after laying out fixed columns. |
+| width_double | [double](#double) | | Units used by the UI library to standardize dimensions. |
+| title | [string](#string) | | The visible title of the column. |
+| sortable | [bool](#bool) | | Whether the column supports sorting. |
+| filterable | [bool](#bool) | | Whether the column supports filtering. |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Top
@@ -176,8 +222,8 @@ which have condition_name of "Diabetes").
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
-| columns | [EntityGroup.Column](#tanagra-configschema-EntityGroup-Column) | repeated | Columns displayed in the list view. |
-| hierarchy_columns | [EntityGroup.Column](#tanagra-configschema-EntityGroup-Column) | repeated | Columns displayed in the hierarchy view. |
+| columns | [tanagra.Column](#tanagra-Column) | repeated | Columns displayed in the list view. |
+| hierarchy_columns | [tanagra.Column](#tanagra-Column) | repeated | Columns displayed in the hierarchy view. |
| name_column_index | [int32](#int32) | | This has been replaced by nameAttribute for determining stored names. Now this only determines which is the primary column for checkboxes, etc. |
| classification_entity_groups | [EntityGroup.EntityGroupConfig](#tanagra-configschema-EntityGroup-EntityGroupConfig) | repeated | Entity groups where the related entity is what is selected (e.g. condition when filtering condition_occurrences). |
| grouping_entity_groups | [EntityGroup.EntityGroupConfig](#tanagra-configschema-EntityGroup-EntityGroupConfig) | repeated | Entity groups where the related entity is not what is selected (e.g. brands when filtering ingredients or genotyping platforms when filtering people). |
@@ -192,26 +238,6 @@ which have condition_name of "Diabetes").
-
-
-### EntityGroup.Column
-Defines a column in the UI.
-
-
-| Field | Type | Label | Description |
-| ----- | ---- | ----- | ----------- |
-| key | [string](#string) | | A unique key for the column. By default, used to look up attributes in the displayed data. |
-| width_string | [string](#string) | | Passed directly to the style of the column. "100%" can be used to take up space remaining after laying out fixed columns. |
-| width_double | [double](#double) | | Units used by the UI library to standardize dimensions. |
-| title | [string](#string) | | The visible title of the column. |
-| sortable | [bool](#bool) | | Whether the column supports sorting. |
-| filterable | [bool](#bool) | | Whether the column supports filtering. |
-
-
-
-
-
-
### EntityGroup.EntityGroupConfig
@@ -307,6 +333,57 @@ include entire entities (e.g. demographics).
+
+Top
+
+## criteriaselector/configschema/survey.proto
+
+
+
+
+
+### Survey
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| columns | [tanagra.Column](#tanagra-Column) | repeated | Columns displayed in the list view. |
+| entity_groups | [Survey.EntityGroupConfig](#tanagra-configschema-Survey-EntityGroupConfig) | repeated | Entity groups where the related entity is what is selected (e.g. surveyBasics when filtering surveyOccurrence). |
+| value_configs | [tanagra.ValueConfig](#tanagra-ValueConfig) | repeated | Optional configuration of a categorical or numeric value associated with the selection (e.g. a numeric answer). Applied to the entire selection so generally not compatible with multi_select. Currently only one is supported. |
+| default_sort | [tanagra.SortOrder](#tanagra-SortOrder) | | The sort order to use in the list view, or in hierarchies where no sort order has been specified. |
+| nameAttribute | [string](#string) | optional | The attribute used to name selections if not the first column. This can be used to include extra context with the selected values that's not visible in the table view. |
+
+
+
+
+
+
+
+
+### Survey.EntityGroupConfig
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| id | [string](#string) | | The id of the entity group. |
+| sort_order | [tanagra.SortOrder](#tanagra-SortOrder) | | The sort order applied to this entity group when displayed in the hierarchy view. |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Top
@@ -644,6 +721,57 @@ values.
+
+Top
+
+## criteriaselector/dataschema/survey.proto
+
+
+
+
+
+### Survey
+Data for an entity group criteria is a list of selected values.
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| selected | [Survey.Selection](#tanagra-dataschema-Survey-Selection) | repeated | |
+| value_data | [tanagra.ValueData](#tanagra-ValueData) | | Data for an additional categorical or numeric value associated with the selection (e.g. a numeric answer). |
+
+
+
+
+
+
+
+
+### Survey.Selection
+
+
+
+| Field | Type | Label | Description |
+| ----- | ---- | ----- | ----------- |
+| key | [tanagra.Key](#tanagra-Key) | | The key of the selected value, which references a related entity (e.g. surveyBasics when filtering surveyOccurrence). |
+| name | [string](#string) | | The visible name for the selection. This is stored to avoid extra lookups when rendering. |
+| entityGroup | [string](#string) | | The entity group is stored to differentiate between them when multiple are configured within a single criteria. |
+| question_key | [tanagra.Key](#tanagra-Key) | | If the selected item is an answer, the key of the question it belongs to. |
+| question_name | [string](#string) | | If the selected item is an answer, the visible name of the question it belongs to. |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Top
diff --git a/docs/generated/UNDERLAY_CONFIG.md b/docs/generated/UNDERLAY_CONFIG.md
index d700d3b88..2c48725ae 100644
--- a/docs/generated/UNDERLAY_CONFIG.md
+++ b/docs/generated/UNDERLAY_CONFIG.md
@@ -193,6 +193,11 @@ Use `plugin: "multiAttribute"`.
Use `plugin: "outputUnfiltered"`.
+### SZCorePlugin.SURVEY
+**required** [SZCorePlugin](#szcoreplugin)
+
+Use `plugin: "survey"`.
+
### SZCorePlugin.TEXT_SEARCH
**required** [SZCorePlugin](#szcoreplugin)
diff --git a/ui/src/components/treegrid.tsx b/ui/src/components/treegrid.tsx
index 7c833f28b..e5a988137 100644
--- a/ui/src/components/treegrid.tsx
+++ b/ui/src/components/treegrid.tsx
@@ -15,6 +15,7 @@ import Typography from "@mui/material/Typography";
import produce from "immer";
import { GridBox } from "layout/gridBox";
import GridLayout from "layout/gridLayout";
+import * as columnProto from "proto/column";
import {
ChangeEvent,
MutableRefObject,
@@ -49,8 +50,8 @@ export type TreeGridItem = {
data: TreeGridRowData;
};
-export type TreeGridData = {
- [key: TreeGridId]: TreeGridItem;
+export type TreeGridData = {
+ [key: TreeGridId]: ItemType;
};
export type TreeGridColumn = {
@@ -84,9 +85,9 @@ export type TreeGridFilters = {
[col: string]: string;
};
-export type TreeGridProps = {
+export type TreeGridProps = {
columns: TreeGridColumn[];
- data: TreeGridData;
+ data: TreeGridData;
defaultExpanded?: TreeGridId[];
highlightId?: TreeGridId;
rowCustomization?: (
@@ -109,7 +110,9 @@ export type TreeGridProps = {
onFilter?: (filters: TreeGridFilters) => void;
};
-export function TreeGrid(props: TreeGridProps) {
+export function TreeGrid(
+ props: TreeGridProps
+) {
const theme = useTheme();
const [state, updateState] = useImmer(
@@ -391,6 +394,18 @@ export function useArrayAsTreeGridData<
}, [array]);
}
+export function fromProtoColumns(
+ columns: columnProto.Column[]
+): TreeGridColumn[] {
+ return columns.map((c) => ({
+ key: c.key,
+ width: c.widthString ?? c.widthDouble ?? 100,
+ title: c.title,
+ sortable: c.sortable,
+ filterable: c.filterable,
+ }));
+}
+
function renderChildren(
theme: Theme,
props: TreeGridProps,
diff --git a/ui/src/criteria/classification.tsx b/ui/src/criteria/classification.tsx
index db02822ed..7f05c8219 100644
--- a/ui/src/criteria/classification.tsx
+++ b/ui/src/criteria/classification.tsx
@@ -11,6 +11,7 @@ import Loading from "components/loading";
import { Search } from "components/search";
import { useSimpleDialog } from "components/simpleDialog";
import {
+ fromProtoColumns,
TreeGrid,
TreeGridColumn,
TreeGridData,
@@ -1022,18 +1023,6 @@ function fromProtoSortOrder(sortOrder: sortOrderProto.SortOrder): SortOrder {
};
}
-function fromProtoColumns(
- columns: configProto.EntityGroup_Column[]
-): TreeGridColumn[] {
- return columns.map((c) => ({
- key: c.key,
- width: c.widthString ?? c.widthDouble ?? 100,
- title: c.title,
- sortable: c.sortable,
- filterable: c.filterable,
- }));
-}
-
function nameAttribute(config: configProto.EntityGroup) {
return (
config.nameAttribute ?? config.columns[config.nameColumnIndex ?? 0].key
diff --git a/ui/src/criteria/survey.tsx b/ui/src/criteria/survey.tsx
new file mode 100644
index 000000000..a2ef3e635
--- /dev/null
+++ b/ui/src/criteria/survey.tsx
@@ -0,0 +1,860 @@
+import DeleteIcon from "@mui/icons-material/Delete";
+import Button from "@mui/material/Button";
+import IconButton from "@mui/material/IconButton";
+import Paper from "@mui/material/Paper";
+import Typography from "@mui/material/Typography";
+import { CriteriaPlugin, registerCriteriaPlugin } from "cohort";
+import Checkbox from "components/checkbox";
+import Empty from "components/empty";
+import Loading from "components/loading";
+import { Search } from "components/search";
+import { useSimpleDialog } from "components/simpleDialog";
+import {
+ fromProtoColumns,
+ TreeGrid,
+ TreeGridColumn,
+ TreeGridData,
+ TreeGridId,
+ TreeGridItem,
+ TreeGridRowData,
+} from "components/treegrid";
+import {
+ ANY_VALUE_DATA,
+ decodeValueData,
+ encodeValueData,
+ ValueData,
+ ValueDataEdit,
+} from "criteria/valueData";
+import {
+ ROLLUP_COUNT_ATTRIBUTE,
+ SortDirection,
+ SortOrder,
+} from "data/configuration";
+import {
+ CommonSelectorConfig,
+ dataKeyFromProto,
+ EntityNode,
+ protoFromDataKey,
+ UnderlaySource,
+} from "data/source";
+import { compareDataValues, DataEntry, DataKey } from "data/types";
+import { useUnderlaySource } from "data/underlaySourceContext";
+import { useUpdateCriteria } from "hooks";
+import emptyImage from "images/empty.svg";
+import produce from "immer";
+import { GridBox } from "layout/gridBox";
+import GridLayout from "layout/gridLayout";
+import * as configProto from "proto/criteriaselector/configschema/survey";
+import * as dataProto from "proto/criteriaselector/dataschema/survey";
+import * as sortOrderProto from "proto/sort_order";
+import { useCallback, useEffect, useMemo } from "react";
+import useSWRImmutable from "swr/immutable";
+import { useImmer } from "use-immer";
+import { base64ToBytes } from "util/base64";
+import { safeRegExp } from "util/safeRegExp";
+import { useLocalSearchState } from "util/searchState";
+import { isValid } from "util/valid";
+
+type Selection = {
+ key: DataKey;
+ name: string;
+ entityGroup: string;
+ questionKey?: DataKey;
+ questionName: string;
+};
+
+enum EntityNodeItemType {
+ Question = "QUESTION",
+ Answer = "ANSWER",
+ Topic = "TOPIC",
+}
+
+// A custom TreeGridItem allows us to store the EntityNode along with
+// the rest of the data.
+type EntityNodeItem = TreeGridItem & {
+ node: EntityNode;
+ entityGroup: string;
+ type: EntityNodeItemType;
+ parentKey?: DataKey;
+};
+
+type EntityTreeGridData = TreeGridData;
+
+// Exported for testing purposes.
+export interface Data {
+ selected: Selection[];
+ valueData: ValueData;
+}
+
+// "survey" plugins are designed to handle medium sized (~<100k rows) amount of
+// survey data in an optimized fashion.
+@registerCriteriaPlugin(
+ "survey",
+ (
+ underlaySource: UnderlaySource,
+ c: CommonSelectorConfig,
+ dataEntry?: DataEntry
+ ) => {
+ const config = decodeConfig(c);
+
+ const data: Data = {
+ selected: [],
+ valueData: { ...ANY_VALUE_DATA },
+ };
+
+ if (dataEntry) {
+ const name = String(dataEntry[nameAttribute(config)]);
+ const entityGroup = String(dataEntry.entityGroup);
+ if (!name || !entityGroup) {
+ throw new Error(
+ `Invalid parameters from search [${name}, ${entityGroup}].`
+ );
+ }
+
+ // TODO(tjennison): There's no way to get the question information for
+ // answers added via global search. We're currently not showing answers
+ // there so this isn't an issue, but if we were, that information would
+ // need to be made available at index time.
+ data.selected.push({
+ key: dataEntry.key,
+ name,
+ entityGroup,
+ questionName: "",
+ });
+ }
+
+ return encodeData(data);
+ },
+ search
+)
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+class _ implements CriteriaPlugin {
+ public data: string;
+ private selector: CommonSelectorConfig;
+ private config: configProto.Survey;
+
+ constructor(public id: string, selector: CommonSelectorConfig, data: string) {
+ this.selector = selector;
+ this.config = decodeConfig(selector);
+ this.data = data;
+ }
+
+ renderEdit(
+ doneAction: () => void,
+ setBackAction: (action?: () => void) => void
+ ) {
+ return (
+
+ );
+ }
+
+ renderInline(groupId: string) {
+ const decodedData = decodeData(this.data);
+
+ if (!this.config.valueConfigs.length || decodedData.selected.length) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
+ displayDetails() {
+ const decodedData = decodeData(this.data);
+
+ const sel = groupSelection(decodedData.selected);
+ if (sel.length > 0) {
+ return {
+ title:
+ sel.length > 1
+ ? `${sel[0].name} and ${sel.length - 1} more`
+ : sel[0].name,
+ standaloneTitle: true,
+ additionalText: sel.map(
+ (s) =>
+ s.name +
+ (s.children.length
+ ? " (" + s.children.map((child) => child.name).join(", ") + ")"
+ : "")
+ ),
+ };
+ }
+
+ return {
+ title: "(any)",
+ };
+ }
+}
+
+function dataKey(key: DataKey, entityGroup: string): string {
+ return JSON.stringify({
+ entityGroup,
+ key,
+ });
+}
+
+type SearchState = {
+ // The query entered in the search box.
+ query?: string;
+};
+
+type SurveyEditProps = {
+ data: string;
+ config: configProto.Survey;
+ doneAction: () => void;
+ setBackAction: (action?: () => void) => void;
+};
+
+function SurveyEdit(props: SurveyEditProps) {
+ const underlaySource = useUnderlaySource();
+ const updateEncodedCriteria = useUpdateCriteria();
+ const updateCriteria = useCallback(
+ (data: Data) => updateEncodedCriteria(encodeData(data)),
+ [updateEncodedCriteria]
+ );
+
+ const decodedData = useMemo(() => decodeData(props.data), [props.data]);
+
+ const [localCriteria, updateLocalCriteria] = useImmer(decodedData);
+
+ const selectedSets = useMemo(() => {
+ const sets = new Map>();
+ localCriteria.selected.forEach((s) => {
+ if (!sets.has(s.entityGroup)) {
+ sets.set(s.entityGroup, new Set());
+ }
+ sets.get(s.entityGroup)?.add(s.key);
+ });
+ return sets;
+ }, [localCriteria]);
+
+ const updateCriteriaFromLocal = useCallback(() => {
+ updateCriteria(produce(decodedData, () => localCriteria));
+ }, [updateCriteria, localCriteria]);
+
+ const [searchState, updateSearchState] = useLocalSearchState();
+
+ const [unconfirmedChangesDialog, showUnconfirmedChangesDialog] =
+ useSimpleDialog();
+
+ const unconfirmedChangesCallback = useCallback(
+ () =>
+ showUnconfirmedChangesDialog({
+ title: "Unsaved changes",
+ text: "Unsaved changes will be lost if you go back without saving.",
+ buttons: ["Cancel", "Go back", "Save"],
+ onButton: (button) => {
+ if (button === 1) {
+ props.doneAction();
+ } else if (button === 2) {
+ updateCriteriaFromLocal();
+ props.doneAction();
+ }
+ },
+ }),
+ [updateCriteriaFromLocal]
+ );
+
+ useEffect(() => {
+ // The extra function works around React defaulting to treating a function
+ // as an update function.
+ props.setBackAction(() => {
+ if (isDataEqual(decodedData, localCriteria)) {
+ return undefined;
+ } else {
+ return unconfirmedChangesCallback;
+ }
+ });
+ }, [searchState, localCriteria]);
+
+ const processEntities = useCallback(
+ (allEntityGroups: [string, EntityNode[]][]) => {
+ const data: EntityTreeGridData = {};
+
+ allEntityGroups.forEach(([entityGroup, nodes]) => {
+ nodes.forEach((node) => {
+ const rowData: TreeGridRowData = { ...node.data };
+ const key = dataKey(node.data.key, entityGroup);
+
+ let parentKey = "root";
+ if (node.ancestors?.length) {
+ parentKey = dataKey(node.ancestors[0], entityGroup);
+ }
+
+ const cItem: EntityNodeItem = {
+ data: rowData,
+ children: data[key]?.children ?? [],
+ node: node,
+ entityGroup,
+ type:
+ data[key]?.type ??
+ (node.childCount === 0
+ ? EntityNodeItemType.Answer
+ : EntityNodeItemType.Topic),
+ parentKey: parentKey,
+ };
+ data[key] = cItem;
+
+ if (data[parentKey]) {
+ data[parentKey].children?.push(key);
+ if (cItem.type === EntityNodeItemType.Answer) {
+ data[parentKey].type = EntityNodeItemType.Question;
+ }
+ } else {
+ const d = {
+ key: parentKey,
+ };
+ data[parentKey] = {
+ data: d,
+ node: {
+ data: d,
+ entity: "loading",
+ },
+ entityGroup,
+ type:
+ cItem.type === EntityNodeItemType.Answer
+ ? EntityNodeItemType.Question
+ : EntityNodeItemType.Topic,
+ children: [key],
+ };
+ }
+ });
+ });
+
+ return data;
+ },
+ []
+ );
+
+ const attributes = useMemo(
+ () => [
+ ...new Set(
+ [
+ props.config.columns.map(({ key }) => key),
+ nameAttribute(props.config),
+ ]
+ .flat()
+ .filter(isValid)
+ ),
+ ],
+ [props.config.columns]
+ );
+
+ const calcSortOrder = useCallback(
+ (primaryEntityGroupId?: string) => {
+ if (primaryEntityGroupId) {
+ const egSortOrder = props.config.entityGroups.find(
+ (c) => c.id === primaryEntityGroupId
+ )?.sortOrder;
+ if (egSortOrder) {
+ return egSortOrder;
+ }
+ }
+
+ return props.config.defaultSort ?? DEFAULT_SORT_ORDER;
+ },
+ [underlaySource]
+ );
+
+ const fetchInstances = useCallback(async () => {
+ const raw: [string, EntityNode[]][] = await Promise.all(
+ props.config.entityGroups.map(async (c) => [
+ c.id,
+ (
+ await underlaySource.searchEntityGroup(
+ attributes,
+ c.id,
+ fromProtoSortOrder(calcSortOrder(c.id)),
+ {
+ hierarchy: true,
+ fetchAll: true,
+ }
+ )
+ ).nodes,
+ ])
+ );
+
+ return processEntities(raw);
+ }, [underlaySource, attributes, processEntities]);
+
+ const instancesState = useSWRImmutable(
+ {
+ type: "entityGroupInstances",
+ entityGroupIds: [...props.config.entityGroups].map((eg) => eg.id),
+ attributes,
+ },
+ fetchInstances
+ );
+
+ const filteredData = useMemo(() => {
+ const data = instancesState.data;
+ if (!data || !searchState?.query) {
+ return data ?? {};
+ }
+
+ // TODO(tjennison): Handle RegExp errors.
+ const [re] = safeRegExp(searchState?.query);
+ const matched = new Set();
+
+ const matchNode = (key: TreeGridId) => {
+ const node = data[key];
+ if (node.type != EntityNodeItemType.Topic) {
+ for (const k in node.data) {
+ if (re.test(String(node.data[k]))) {
+ matched.add(key);
+ node.node.ancestors?.forEach((a) =>
+ matched.add(dataKey(a, node.entityGroup))
+ );
+ break;
+ }
+ }
+ }
+
+ node.children?.forEach((c) => matchNode(c));
+ };
+ matchNode("root");
+
+ const filtered = produce(data, (data) => {
+ for (const key in data) {
+ const node = data[key];
+ data[key].children =
+ node.type !== EntityNodeItemType.Topic
+ ? node.children
+ : node.children?.filter((c) => matched.has(c));
+ }
+ });
+ return filtered;
+ }, [instancesState.data, searchState?.query]);
+
+ const columns: TreeGridColumn[] = useMemo(
+ () => fromProtoColumns(props.config.columns),
+ [props.config.columns]
+ );
+
+ const groupedSelection = useMemo(
+ () => groupSelection(localCriteria.selected ?? []),
+ [localCriteria.selected]
+ );
+
+ return (
+ theme.palette.background.paper,
+ }}
+ >
+
+
+
+ {
+ updateSearchState((data: SearchState) => {
+ data.query = query;
+ });
+ }}
+ initialValue={searchState?.query}
+ />
+
+
+ {!filteredData?.root?.children?.length ? (
+
+ ) : (
+
+ columns={columns}
+ data={filteredData}
+ expandable
+ reserveExpansionSpacing
+ rowCustomization={(
+ id: TreeGridId,
+ rowData: TreeGridRowData
+ ) => {
+ if (!instancesState.data) {
+ return undefined;
+ }
+
+ // TODO(tjennison): Make TreeGridData's type generic so we can
+ // avoid this type assertion. Also consider passing the
+ // TreeGridItem to the callback instead of the TreeGridRowData.
+ const item = instancesState.data[id];
+ if (!item) {
+ return undefined;
+ }
+
+ const entityGroupSet = selectedSets.get(item.entityGroup);
+ const found = !!entityGroupSet?.has(item.node.data.key);
+ const foundAncestor = !!item.node.ancestors?.reduce(
+ (acc, cur) => acc || !!entityGroupSet?.has(cur),
+ false
+ );
+
+ return [
+ {
+ column: 0,
+ prefixElements: (
+ {
+ updateLocalCriteria((data) => {
+ if (found) {
+ data.selected = data.selected.filter(
+ (s) =>
+ item.node.data.key !== s.key ||
+ item.entityGroup !== s.entityGroup
+ );
+ } else {
+ const question =
+ item.parentKey &&
+ item.type === EntityNodeItemType.Answer
+ ? instancesState.data?.[item.parentKey]
+ : undefined;
+ const name =
+ rowData[nameAttribute(props.config)];
+ const questionName =
+ question?.node?.data?.[
+ nameAttribute(props.config)
+ ];
+ data.selected.push({
+ key: item.node.data.key,
+ name: !!name ? String(name) : "",
+ entityGroup: item.entityGroup,
+ questionKey: question?.node?.data?.key,
+ questionName: !!questionName
+ ? String(questionName)
+ : "",
+ });
+ }
+ data.valueData = ANY_VALUE_DATA;
+ });
+ }}
+ />
+ ),
+ },
+ ];
+ }}
+ />
+ )}
+
+
+ theme.palette.background.default,
+ }}
+ >
+
+
+ {groupedSelection.length ? (
+
+
+ Selected items:
+
+
+ {groupedSelection.map((s, i) => (
+
+
+ `0 -1px 0 ${theme.palette.divider}`
+ : undefined,
+ }}
+ >
+ {s.name}
+ {s.index >= 0 ? (
+
+ updateLocalCriteria((data) => {
+ data.selected.splice(s.index, 1);
+ })
+ }
+ >
+
+
+ ) : null}
+
+
+ {s.children.map((child) => (
+
+
+ {child.name}
+
+ {child.index >= 0 ? (
+
+ updateLocalCriteria((data) => {
+ data.selected.splice(child.index, 1);
+ })
+ }
+ >
+
+
+ ) : null}
+
+ ))}
+
+
+ ))}
+
+
+
+
+ ) : (
+
+ )}
+
+
+ {
+ updateCriteriaFromLocal();
+ props.doneAction();
+ }}
+ >
+ Save criteria
+
+
+
+
+
+ {unconfirmedChangesDialog}
+
+ );
+}
+
+type SurveyInlineProps = {
+ groupId: string;
+ criteriaId: string;
+ data: string;
+ config: configProto.Survey;
+};
+
+function SurveyInline(props: SurveyInlineProps) {
+ const underlaySource = useUnderlaySource();
+ const updateEncodedCriteria = useUpdateCriteria();
+ const updateCriteria = useCallback(
+ (data: Data) => updateEncodedCriteria(encodeData(data)),
+ [updateEncodedCriteria]
+ );
+
+ const decodedData = useMemo(() => decodeData(props.data), [props.data]);
+
+ if (!props.config.valueConfigs.length || !decodedData.selected.length) {
+ return null;
+ }
+
+ const entityGroup = underlaySource.lookupEntityGroup(
+ decodedData.selected[0].entityGroup
+ );
+
+ return (
+
+ updateCriteria(
+ produce(decodedData, (data) => {
+ data.valueData = valueData[0];
+ })
+ )
+ }
+ />
+ );
+}
+
+async function search(
+ underlaySource: UnderlaySource,
+ c: CommonSelectorConfig,
+ query: string
+): Promise {
+ const config = decodeConfig(c);
+ const results = await Promise.all(
+ (config.entityGroups ?? []).map((eg) =>
+ underlaySource
+ .searchEntityGroup(
+ config.columns.map(({ key }) => key),
+ eg.id,
+ fromProtoSortOrder(config.defaultSort ?? DEFAULT_SORT_ORDER),
+ {
+ query,
+ isLeaf: false,
+ }
+ )
+ .then((res) =>
+ res.nodes.map((node) => ({
+ ...node.data,
+ entityGroup: eg.id,
+ }))
+ )
+ )
+ );
+
+ return results.flat();
+}
+
+function isDataEqual(data1: Data, data2: Data) {
+ // TODO(tjennison): In future the ValueData may need to be compared as well.
+ if (data1.selected.length != data2.selected.length) {
+ return false;
+ }
+ return data1.selected.reduce(
+ (acc, cur, i) =>
+ acc &&
+ cur.key === data2.selected[i].key &&
+ cur.entityGroup === data2.selected[i].entityGroup,
+ true
+ );
+}
+
+function decodeData(data: string): Data {
+ const message =
+ data[0] === "{"
+ ? dataProto.Survey.fromJSON(JSON.parse(data))
+ : dataProto.Survey.decode(base64ToBytes(data));
+
+ return {
+ selected:
+ message.selected?.map((s) => ({
+ key: dataKeyFromProto(s.key),
+ name: s.name,
+ entityGroup: s.entityGroup,
+ questionKey: s.questionKey
+ ? dataKeyFromProto(s.questionKey)
+ : undefined,
+ questionName: s.questionName,
+ })) ?? [],
+ valueData: decodeValueData(message.valueData),
+ };
+}
+
+function encodeData(data: Data): string {
+ const message: dataProto.Survey = {
+ selected: data.selected.map((s) => ({
+ key: protoFromDataKey(s.key),
+ name: s.name,
+ entityGroup: s.entityGroup,
+ questionKey: s.questionKey ? protoFromDataKey(s.questionKey) : undefined,
+ questionName: s.questionName,
+ })),
+ valueData: encodeValueData(data.valueData),
+ };
+ return JSON.stringify(dataProto.Survey.toJSON(message));
+}
+
+const DEFAULT_SORT_ORDER = {
+ attribute: ROLLUP_COUNT_ATTRIBUTE,
+ direction: sortOrderProto.SortOrder_Direction.SORT_ORDER_DIRECTION_DESCENDING,
+};
+
+function decodeConfig(selector: CommonSelectorConfig): configProto.Survey {
+ return configProto.Survey.fromJSON(JSON.parse(selector.pluginConfig));
+}
+
+function fromProtoSortOrder(sortOrder: sortOrderProto.SortOrder): SortOrder {
+ return {
+ attribute: sortOrder.attribute,
+ direction:
+ sortOrder.direction ===
+ sortOrderProto.SortOrder_Direction.SORT_ORDER_DIRECTION_DESCENDING
+ ? SortDirection.Desc
+ : SortDirection.Asc,
+ };
+}
+
+function nameAttribute(config: configProto.Survey) {
+ return config.nameAttribute ?? config.columns[0].key;
+}
+
+type GroupedSelectionItem = {
+ index: number;
+ key: DataKey;
+ name: string;
+ children: GroupedSelectionItem[];
+};
+
+function groupSelection(selected: Selection[]): GroupedSelectionItem[] {
+ const map = new Map();
+
+ selected.forEach((s, index) => {
+ const item: GroupedSelectionItem = {
+ index,
+ key: s.key,
+ name: s.name,
+ children: [],
+ };
+
+ if (!s.questionKey) {
+ const existing = map.get(s.key);
+ if (existing) {
+ // An answer and its question are both selected.
+ item.children = existing.children;
+ }
+ map.set(s.key, item);
+ } else {
+ const question = map.get(s.questionKey);
+ if (question) {
+ question.children.push(item);
+ } else {
+ map.set(s.questionKey, {
+ index: -1,
+ key: s.questionKey,
+ name: s.questionName ?? "Unknown",
+ children: [item],
+ });
+ }
+ }
+ });
+
+ return Array.from(map, ([, item]) => {
+ item.children?.sort((a, b) => compareDataValues(a.key, b.key));
+ return item;
+ }).sort((a, b) => compareDataValues(a.key, b.key));
+}
diff --git a/ui/src/data/source.tsx b/ui/src/data/source.tsx
index 22e6dd481..aa0f23147 100644
--- a/ui/src/data/source.tsx
+++ b/ui/src/data/source.tsx
@@ -27,6 +27,8 @@ export type SearchEntityGroupOptions = {
parent?: DataKey;
limit?: number;
hierarchy?: boolean;
+ fetchAll?: boolean;
+ isLeaf?: boolean;
};
export type SearchEntityGroupResult = {
@@ -638,9 +640,7 @@ export class BackendUnderlaySource implements UnderlaySource {
entityGroup,
entity,
sortOrder,
- options?.query,
- options?.parent,
- options?.limit
+ options
)
)
.then((res) => ({
@@ -895,14 +895,12 @@ export class BackendUnderlaySource implements UnderlaySource {
entityGroup: EntityGroupData,
entity: tanagraUnderlay.SZEntity,
sortOrder: SortOrder,
- query?: string,
- parent?: DataValue,
- limit?: number
+ options?: SearchEntityGroupOptions
): tanagra.ListInstancesRequest {
const hierarchy = entity.hierarchies?.[0]?.name;
const operands: tanagra.Filter[] = [];
- if (entityGroup.relatedEntityId && parent) {
+ if (entityGroup.relatedEntityId && options?.parent) {
const groupingEntity = this.lookupEntity(entityGroup.entityId);
operands.push({
filterType: tanagra.FilterFilterTypeEnum.Relationship,
@@ -915,7 +913,7 @@ export class BackendUnderlaySource implements UnderlaySource {
attributeFilter: {
attribute: groupingEntity.idAttribute,
operator: tanagra.AttributeFilterOperatorEnum.Equals,
- values: [literalFromDataValue(parent)],
+ values: [literalFromDataValue(options?.parent)],
},
},
},
@@ -923,30 +921,30 @@ export class BackendUnderlaySource implements UnderlaySource {
},
});
} else {
- if (hierarchy && parent) {
+ if (hierarchy && options?.parent) {
operands.push({
filterType: tanagra.FilterFilterTypeEnum.Hierarchy,
filterUnion: {
hierarchyFilter: {
hierarchy,
operator: tanagra.HierarchyFilterOperatorEnum.ChildOf,
- values: [literalFromDataValue(parent)],
+ values: [literalFromDataValue(options?.parent)],
},
},
});
- } else if (isValid(query)) {
- if (query !== "") {
+ } else if (isValid(options?.query)) {
+ if (options?.query !== "") {
operands.push({
filterType: tanagra.FilterFilterTypeEnum.Text,
filterUnion: {
textFilter: {
matchType: tanagra.TextFilterMatchTypeEnum.ExactMatch,
- text: query,
+ text: options?.query,
},
},
});
}
- } else if (hierarchy) {
+ } else if (hierarchy && !options?.fetchAll) {
operands.push({
filterType: tanagra.FilterFilterTypeEnum.Hierarchy,
filterUnion: {
@@ -970,9 +968,33 @@ export class BackendUnderlaySource implements UnderlaySource {
},
},
});
+
+ if (options && isValid(options.isLeaf)) {
+ let isLeafFilter: tanagra.Filter | null = {
+ filterType: tanagra.FilterFilterTypeEnum.Hierarchy,
+ filterUnion: {
+ hierarchyFilter: {
+ hierarchy,
+ operator: tanagra.HierarchyFilterOperatorEnum.IsLeaf,
+ },
+ },
+ };
+
+ if (!options.isLeaf) {
+ isLeafFilter = makeBooleanLogicFilter(
+ tanagra.BooleanLogicFilterOperatorEnum.Not,
+ [isLeafFilter]
+ );
+ }
+
+ if (isLeafFilter) {
+ operands.push(isLeafFilter);
+ }
+ }
}
}
+ const limit = options?.fetchAll ? 100000 : options?.limit;
const req = {
entityName: entity.name,
underlayName: this.underlay.name,
@@ -1705,7 +1727,10 @@ function makeBooleanLogicFilter(
if (!subfilters || subfilters.length === 0) {
return null;
}
- if (subfilters.length === 1) {
+ if (
+ subfilters.length === 1 &&
+ operator !== tanagra.BooleanLogicFilterOperatorEnum.Not
+ ) {
return subfilters[0];
}
return {
diff --git a/ui/src/plugins.ts b/ui/src/plugins.ts
index 9b3cc512d..d13e8a462 100644
--- a/ui/src/plugins.ts
+++ b/ui/src/plugins.ts
@@ -7,6 +7,7 @@ import "criteria/classification";
import "criteria/textSearch";
import "criteria/unhintedValue";
import "criteria/outputUnfiltered";
+import "criteria/survey";
// Cohort review plugins
import "cohortReview/plugins/occurrenceTable";
diff --git a/ui/src/tanagra-underlay/underlayConfig.ts b/ui/src/tanagra-underlay/underlayConfig.ts
index a7e110d0f..58f688123 100644
--- a/ui/src/tanagra-underlay/underlayConfig.ts
+++ b/ui/src/tanagra-underlay/underlayConfig.ts
@@ -26,6 +26,7 @@ export enum SZCorePlugin {
ENTITY_GROUP = "ENTITY_GROUP",
MULTI_ATTRIBUTE = "MULTI_ATTRIBUTE",
OUTPUT_UNFILTERED = "OUTPUT_UNFILTERED",
+ SURVEY = "SURVEY",
TEXT_SEARCH = "TEXT_SEARCH",
UNHINTED_VALUE = "UNHINTED_VALUE",
};
diff --git a/underlay/src/main/java/bio/terra/tanagra/api/shared/Literal.java b/underlay/src/main/java/bio/terra/tanagra/api/shared/Literal.java
index 3e2540cc3..fde0014e4 100644
--- a/underlay/src/main/java/bio/terra/tanagra/api/shared/Literal.java
+++ b/underlay/src/main/java/bio/terra/tanagra/api/shared/Literal.java
@@ -14,7 +14,7 @@
import java.util.Objects;
@SuppressWarnings("PMD.ImmutableField")
-public final class Literal {
+public final class Literal implements Comparable {
private final boolean isNull;
private final DataType dataType;
private String stringVal;
diff --git a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilder.java b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilder.java
index 37d0f5bfe..8af225c53 100644
--- a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilder.java
+++ b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilder.java
@@ -1,330 +1,22 @@
package bio.terra.tanagra.filterbuilder.impl.core;
-import static bio.terra.tanagra.filterbuilder.impl.core.utils.AttributeSchemaUtils.IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY;
import static bio.terra.tanagra.utils.ProtobufUtils.deserializeFromJsonOrProtoBytes;
-import bio.terra.tanagra.api.filter.BooleanAndOrFilter;
-import bio.terra.tanagra.api.filter.EntityFilter;
-import bio.terra.tanagra.api.filter.PrimaryWithCriteriaFilter;
import bio.terra.tanagra.api.shared.Literal;
-import bio.terra.tanagra.exception.InvalidQueryException;
-import bio.terra.tanagra.exception.SystemException;
-import bio.terra.tanagra.filterbuilder.EntityOutput;
-import bio.terra.tanagra.filterbuilder.FilterBuilder;
-import bio.terra.tanagra.filterbuilder.impl.core.utils.AttributeSchemaUtils;
-import bio.terra.tanagra.filterbuilder.impl.core.utils.EntityGroupFilterUtils;
-import bio.terra.tanagra.filterbuilder.impl.core.utils.GroupByCountSchemaUtils;
+import bio.terra.tanagra.proto.criteriaselector.ValueDataOuterClass;
import bio.terra.tanagra.proto.criteriaselector.configschema.CFEntityGroup;
-import bio.terra.tanagra.proto.criteriaselector.configschema.CFUnhintedValue;
import bio.terra.tanagra.proto.criteriaselector.dataschema.DTEntityGroup;
-import bio.terra.tanagra.proto.criteriaselector.dataschema.DTUnhintedValue;
-import bio.terra.tanagra.underlay.Underlay;
-import bio.terra.tanagra.underlay.entitymodel.Attribute;
-import bio.terra.tanagra.underlay.entitymodel.Entity;
-import bio.terra.tanagra.underlay.entitymodel.entitygroup.CriteriaOccurrence;
-import bio.terra.tanagra.underlay.entitymodel.entitygroup.EntityGroup;
-import bio.terra.tanagra.underlay.entitymodel.entitygroup.GroupItems;
import bio.terra.tanagra.underlay.uiplugin.CriteriaSelector;
-import bio.terra.tanagra.underlay.uiplugin.SelectionData;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.util.ArrayList;
-import java.util.Comparator;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
import java.util.stream.Collectors;
-import org.apache.commons.lang3.tuple.Pair;
-@SuppressFBWarnings(
- value = "NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD",
- justification = "The config and data objects are deserialized by Jackson.")
-public class EntityGroupFilterBuilder extends FilterBuilder {
+public class EntityGroupFilterBuilder extends EntityGroupFilterBuilderBase {
public EntityGroupFilterBuilder(CriteriaSelector criteriaSelector) {
super(criteriaSelector);
}
- @Override
- public EntityFilter buildForCohort(Underlay underlay, List selectionData) {
- DTEntityGroup.EntityGroup entityGroupSelectionData =
- deserializeData(selectionData.get(0).getPluginData());
- List modifiersSelectionData = selectionData.subList(1, selectionData.size());
- if (entityGroupSelectionData == null || entityGroupSelectionData.getSelectedList().isEmpty()) {
- // Empty selection data = null filter for a cohort.
- return null;
- }
-
- // We want to build one filter per entity group, not one filter per selected id.
- Map> selectedIdsPerEntityGroup =
- selectedIdsPerEntityGroup(underlay, entityGroupSelectionData);
-
- List entityFilters = new ArrayList<>();
- selectedIdsPerEntityGroup.entrySet().stream()
- .sorted(Comparator.comparing(entry -> entry.getKey().getName()))
- .forEach(
- entry -> {
- EntityGroup entityGroup = entry.getKey();
- List selectedIds = entry.getValue();
- switch (entityGroup.getType()) {
- case CRITERIA_OCCURRENCE:
- entityFilters.add(
- buildPrimaryWithCriteriaFilter(
- underlay,
- (CriteriaOccurrence) entityGroup,
- selectedIds,
- entityGroupSelectionData,
- modifiersSelectionData));
- break;
- case GROUP_ITEMS:
- entityFilters.add(
- buildGroupItemsFilter(
- underlay, (GroupItems) entityGroup, selectedIds, modifiersSelectionData));
- break;
- default:
- throw new SystemException(
- "Unsupported entity group type: " + entityGroup.getType());
- }
- });
-
- return entityFilters.size() == 1
- ? entityFilters.get(0)
- : new BooleanAndOrFilter(BooleanAndOrFilter.LogicalOperator.OR, entityFilters);
- }
-
- @Override
- public List buildForDataFeature(
- Underlay underlay, List selectionData) {
- DTEntityGroup.EntityGroup entityGroupSelectionData =
- deserializeData(selectionData.get(0).getPluginData());
- List modifiersSelectionData = selectionData.subList(1, selectionData.size());
-
- if (entityGroupSelectionData == null || entityGroupSelectionData.getSelectedList().isEmpty()) {
- // Empty selection data = output all occurrence entities with null filters.
- // Use the list of all possible entity groups in the config.
- CFEntityGroup.EntityGroup entityGroupConfig = deserializeConfig();
- Set outputEntities = new HashSet<>();
- entityGroupConfig
- .getClassificationEntityGroupsList()
- .forEach(
- classificationEntityGroup -> {
- EntityGroup entityGroup =
- underlay.getEntityGroup(classificationEntityGroup.getId());
- switch (entityGroup.getType()) {
- case CRITERIA_OCCURRENCE:
- CriteriaOccurrence criteriaOccurrence = (CriteriaOccurrence) entityGroup;
- outputEntities.addAll(criteriaOccurrence.getOccurrenceEntities());
- break;
- case GROUP_ITEMS:
- GroupItems groupItems = (GroupItems) entityGroup;
- outputEntities.add(
- groupItems.getItemsEntity().isPrimary()
- ? groupItems.getGroupEntity()
- : groupItems.getItemsEntity());
- break;
- default:
- throw new SystemException(
- "Unsupported entity group type: " + entityGroup.getType());
- }
- });
- return outputEntities.stream().map(EntityOutput::unfiltered).collect(Collectors.toList());
- } else {
- // Check that there are no group by modifiers.
- Optional>
- groupByModifierConfigAndData =
- GroupByCountSchemaUtils.getModifier(criteriaSelector, modifiersSelectionData);
- if (groupByModifierConfigAndData.isPresent()) {
- throw new InvalidQueryException("Group by modifiers are not supported for data features");
- }
-
- // We want to build filters per entity group, not per selected id.
- Map> selectedIdsPerEntityGroup =
- selectedIdsPerEntityGroup(underlay, entityGroupSelectionData);
-
- Map> filtersPerEntity = new HashMap<>();
- selectedIdsPerEntityGroup.forEach(
- (entityGroup, selectedIds) -> {
- Map> filtersForSingleEntityGroup;
- switch (entityGroup.getType()) {
- case CRITERIA_OCCURRENCE:
- CriteriaOccurrence criteriaOccurrence = (CriteriaOccurrence) entityGroup;
- filtersForSingleEntityGroup =
- EntityGroupFilterUtils.addOccurrenceFiltersForDataFeature(
- underlay, criteriaOccurrence, selectedIds);
- EntityGroupFilterUtils.buildAllModifierFilters(
- underlay,
- criteriaOccurrence.getOccurrenceEntities(),
- criteriaSelector,
- entityGroupSelectionData,
- modifiersSelectionData,
- filtersForSingleEntityGroup);
- break;
- case GROUP_ITEMS:
- GroupItems groupItems = (GroupItems) entityGroup;
- Entity notPrimaryEntity =
- groupItems.getGroupEntity().isPrimary()
- ? groupItems.getItemsEntity()
- : groupItems.getGroupEntity();
-
- filtersForSingleEntityGroup = new HashMap<>();
- EntityFilter idSubFilter =
- EntityGroupFilterUtils.buildIdSubFilter(
- underlay, notPrimaryEntity, selectedIds);
- if (idSubFilter == null) {
- filtersForSingleEntityGroup.put(notPrimaryEntity, new ArrayList<>());
- } else {
- filtersForSingleEntityGroup.put(
- notPrimaryEntity, new ArrayList<>(List.of(idSubFilter)));
- }
- EntityGroupFilterUtils.buildAllModifierFilters(
- underlay,
- List.of(notPrimaryEntity),
- criteriaSelector,
- entityGroupSelectionData,
- modifiersSelectionData,
- filtersForSingleEntityGroup);
- break;
- default:
- throw new SystemException(
- "Unsupported entity group type: " + entityGroup.getType());
- }
-
- List entityOutputsForSingleEntityGroup =
- EntityGroupFilterUtils.mergeFiltersForDataFeature(
- filtersForSingleEntityGroup, BooleanAndOrFilter.LogicalOperator.AND);
- entityOutputsForSingleEntityGroup.forEach(
- entityOutput -> {
- List filters =
- filtersPerEntity.getOrDefault(entityOutput.getEntity(), new ArrayList<>());
- if (entityOutput.hasDataFeatureFilter()) {
- filters.add(entityOutput.getDataFeatureFilter());
- } else {
- filters = new ArrayList<>();
- }
- filtersPerEntity.put(entityOutput.getEntity(), filters);
- });
- });
-
- // If there are multiple filters for a single entity, OR them together.
- return EntityGroupFilterUtils.mergeFiltersForDataFeature(
- filtersPerEntity, BooleanAndOrFilter.LogicalOperator.OR);
- }
- }
-
- private Map> selectedIdsPerEntityGroup(
- Underlay underlay, DTEntityGroup.EntityGroup entityGroupSelectionData) {
- Map> selectedIdsPerEntityGroup = new HashMap<>();
- for (DTEntityGroup.EntityGroup.Selection selectedId :
- entityGroupSelectionData.getSelectedList()) {
- EntityGroup entityGroup = underlay.getEntityGroup(selectedId.getEntityGroup());
- List selectedIds =
- selectedIdsPerEntityGroup.containsKey(entityGroup)
- ? selectedIdsPerEntityGroup.get(entityGroup)
- : new ArrayList<>();
- if (selectedId.hasKey()) {
- selectedIds.add(Literal.forInt64(selectedId.getKey().getInt64Key()));
- }
- selectedIdsPerEntityGroup.put(entityGroup, selectedIds);
- }
- return selectedIdsPerEntityGroup;
- }
-
- private EntityFilter buildPrimaryWithCriteriaFilter(
- Underlay underlay,
- CriteriaOccurrence criteriaOccurrence,
- List selectedIds,
- DTEntityGroup.EntityGroup entityGroupSelectionData,
- List modifiersSelectionData) {
- // Build the criteria sub-filter.
- EntityFilter criteriaSubFilter =
- EntityGroupFilterUtils.buildIdSubFilter(
- underlay, criteriaOccurrence.getCriteriaEntity(), selectedIds);
-
- // Build the attribute modifier filters.
- Map> subFiltersPerOccurrenceEntity =
- EntityGroupFilterUtils.buildAttributeModifierFilters(
- underlay,
- criteriaSelector,
- modifiersSelectionData,
- criteriaOccurrence.getOccurrenceEntities());
-
- // Build the instance-level modifier filters.
- if (entityGroupSelectionData.hasValueData()
- && !IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY.equalsIgnoreCase(
- entityGroupSelectionData.getValueData().getAttribute())) {
- if (criteriaOccurrence.getOccurrenceEntities().size() > 1) {
- throw new InvalidQueryException(
- "Instance-level modifiers are not supported for entity groups with multiple occurrence entities: "
- + criteriaOccurrence.getName());
- }
- Entity occurrenceEntity = criteriaOccurrence.getOccurrenceEntities().get(0);
-
- EntityFilter attrFilter =
- AttributeSchemaUtils.buildForEntity(
- underlay,
- occurrenceEntity,
- occurrenceEntity.getAttribute(entityGroupSelectionData.getValueData().getAttribute()),
- entityGroupSelectionData.getValueData());
- List subFilters =
- subFiltersPerOccurrenceEntity.containsKey(occurrenceEntity)
- ? subFiltersPerOccurrenceEntity.get(occurrenceEntity)
- : new ArrayList<>();
- subFilters.add(attrFilter);
- subFiltersPerOccurrenceEntity.put(occurrenceEntity, subFilters);
- }
-
- Optional>
- groupByModifierConfigAndData =
- GroupByCountSchemaUtils.getModifier(criteriaSelector, modifiersSelectionData);
- if (groupByModifierConfigAndData.isEmpty()
- || groupByModifierConfigAndData.get().getRight() == null) {
- return new PrimaryWithCriteriaFilter(
- underlay,
- criteriaOccurrence,
- criteriaSubFilter,
- subFiltersPerOccurrenceEntity,
- null,
- null,
- null);
- }
-
- // Build the group by filter information.
- Map> groupByAttributesPerOccurrenceEntity =
- GroupByCountSchemaUtils.getGroupByAttributesPerOccurrenceEntity(
- underlay, groupByModifierConfigAndData, criteriaOccurrence.getOccurrenceEntities());
- DTUnhintedValue.UnhintedValue groupByModifierData =
- groupByModifierConfigAndData.get().getRight();
- return new PrimaryWithCriteriaFilter(
- underlay,
- criteriaOccurrence,
- criteriaSubFilter,
- subFiltersPerOccurrenceEntity,
- groupByAttributesPerOccurrenceEntity,
- GroupByCountSchemaUtils.toBinaryOperator(groupByModifierData.getOperator()),
- (int) groupByModifierData.getMin());
- }
-
- private EntityFilter buildGroupItemsFilter(
- Underlay underlay,
- GroupItems groupItems,
- List selectedIds,
- List modifiersSelectionData) {
- Entity notPrimaryEntity =
- groupItems.getGroupEntity().isPrimary()
- ? groupItems.getItemsEntity()
- : groupItems.getGroupEntity();
-
- // Build the sub-filters on the non-primary entity.
- List idFilterNonPrimaryEntity = new ArrayList<>();
- if (!selectedIds.isEmpty()) {
- idFilterNonPrimaryEntity.add(
- EntityGroupFilterUtils.buildIdSubFilter(underlay, notPrimaryEntity, selectedIds));
- }
- return EntityGroupFilterUtils.buildGroupItemsFilter(
- underlay, criteriaSelector, groupItems, idFilterNonPrimaryEntity, modifiersSelectionData);
- }
-
@Override
public CFEntityGroup.EntityGroup deserializeConfig() {
return deserializeFromJsonOrProtoBytes(
@@ -339,4 +31,35 @@ public DTEntityGroup.EntityGroup deserializeData(String serialized) {
: deserializeFromJsonOrProtoBytes(serialized, DTEntityGroup.EntityGroup.newBuilder())
.build();
}
+
+ @Override
+ protected List entityGroupIds() {
+ return deserializeConfig().getClassificationEntityGroupsList().stream()
+ .map(
+ classificationEntityGroup -> {
+ return classificationEntityGroup.getId();
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ protected Map selectedIdsAndEntityGroups(String serializedSelectionData) {
+ Map idsAndEntityGroups = new HashMap<>();
+ DTEntityGroup.EntityGroup selectionData = deserializeData(serializedSelectionData);
+ if (selectionData != null) {
+ for (DTEntityGroup.EntityGroup.Selection selectedId : selectionData.getSelectedList()) {
+ if (selectedId.hasKey()) {
+ idsAndEntityGroups.put(
+ Literal.forInt64(selectedId.getKey().getInt64Key()), selectedId.getEntityGroup());
+ }
+ }
+ }
+ return idsAndEntityGroups;
+ }
+
+ @Override
+ protected ValueDataOuterClass.ValueData valueData(String serializedSelectionData) {
+ DTEntityGroup.EntityGroup selectionData = deserializeData(serializedSelectionData);
+ return selectionData.hasValueData() ? selectionData.getValueData() : null;
+ }
}
diff --git a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilderBase.java b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilderBase.java
new file mode 100644
index 000000000..f6af31f19
--- /dev/null
+++ b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/EntityGroupFilterBuilderBase.java
@@ -0,0 +1,368 @@
+package bio.terra.tanagra.filterbuilder.impl.core;
+
+import static bio.terra.tanagra.filterbuilder.impl.core.utils.AttributeSchemaUtils.IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY;
+
+import bio.terra.tanagra.api.filter.BooleanAndOrFilter;
+import bio.terra.tanagra.api.filter.EntityFilter;
+import bio.terra.tanagra.api.filter.PrimaryWithCriteriaFilter;
+import bio.terra.tanagra.api.shared.Literal;
+import bio.terra.tanagra.exception.InvalidQueryException;
+import bio.terra.tanagra.exception.SystemException;
+import bio.terra.tanagra.filterbuilder.EntityOutput;
+import bio.terra.tanagra.filterbuilder.FilterBuilder;
+import bio.terra.tanagra.filterbuilder.impl.core.utils.AttributeSchemaUtils;
+import bio.terra.tanagra.filterbuilder.impl.core.utils.EntityGroupFilterUtils;
+import bio.terra.tanagra.filterbuilder.impl.core.utils.GroupByCountSchemaUtils;
+import bio.terra.tanagra.proto.criteriaselector.ValueDataOuterClass;
+import bio.terra.tanagra.proto.criteriaselector.configschema.CFUnhintedValue;
+import bio.terra.tanagra.proto.criteriaselector.dataschema.DTUnhintedValue;
+import bio.terra.tanagra.underlay.Underlay;
+import bio.terra.tanagra.underlay.entitymodel.Attribute;
+import bio.terra.tanagra.underlay.entitymodel.Entity;
+import bio.terra.tanagra.underlay.entitymodel.entitygroup.CriteriaOccurrence;
+import bio.terra.tanagra.underlay.entitymodel.entitygroup.EntityGroup;
+import bio.terra.tanagra.underlay.entitymodel.entitygroup.GroupItems;
+import bio.terra.tanagra.underlay.uiplugin.CriteriaSelector;
+import bio.terra.tanagra.underlay.uiplugin.SelectionData;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.tuple.Pair;
+
+@SuppressFBWarnings(
+ value = "NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD",
+ justification = "The config and data objects are deserialized by Jackson.")
+public abstract class EntityGroupFilterBuilderBase extends FilterBuilder {
+ public EntityGroupFilterBuilderBase(CriteriaSelector criteriaSelector) {
+ super(criteriaSelector);
+ }
+
+ @Override
+ public EntityFilter buildForCohort(Underlay underlay, List selectionData) {
+ String criteriaSelectionData = selectionData.get(0).getPluginData();
+ List modifiersSelectionData = selectionData.subList(1, selectionData.size());
+
+ // We want to build one filter per entity group, not one filter per selected id.
+ Map> selectedIdsPerEntityGroup =
+ selectedIdsPerEntityGroup(underlay, criteriaSelectionData);
+ if (selectedIdsPerEntityGroup.isEmpty() && modifiersSelectionData.isEmpty()) {
+ // Empty selection data = null filter for a cohort.
+ return null;
+ }
+
+ List selectedEntityGroups =
+ selectedEntityGroups(underlay, selectedIdsPerEntityGroup);
+
+ List entityFilters = new ArrayList<>();
+ selectedEntityGroups.forEach(
+ entityGroup -> {
+ List selectedIds = selectedIdsPerEntityGroup.get(entityGroup);
+ if (selectedIds == null) {
+ selectedIds = new ArrayList<>();
+ }
+
+ switch (entityGroup.getType()) {
+ case CRITERIA_OCCURRENCE:
+ entityFilters.add(
+ buildPrimaryWithCriteriaFilter(
+ underlay,
+ (CriteriaOccurrence) entityGroup,
+ selectedIds,
+ criteriaSelectionData,
+ modifiersSelectionData));
+ break;
+ case GROUP_ITEMS:
+ entityFilters.add(
+ buildGroupItemsFilter(
+ underlay, (GroupItems) entityGroup, selectedIds, modifiersSelectionData));
+ break;
+ default:
+ throw new SystemException("Unsupported entity group type: " + entityGroup.getType());
+ }
+ });
+
+ return entityFilters.size() == 1
+ ? entityFilters.get(0)
+ : new BooleanAndOrFilter(BooleanAndOrFilter.LogicalOperator.OR, entityFilters);
+ }
+
+ @Override
+ public List buildForDataFeature(
+ Underlay underlay, List selectionData) {
+ String criteriaSelectionData = selectionData.get(0).getPluginData();
+
+ Map> selectedIdsPerEntityGroup =
+ selectedIdsPerEntityGroup(underlay, criteriaSelectionData);
+ List modifiersSelectionData = selectionData.subList(1, selectionData.size());
+
+ List selectedEntityGroups =
+ selectedEntityGroups(underlay, selectedIdsPerEntityGroup);
+
+ if (selectedIdsPerEntityGroup.isEmpty() && modifiersSelectionData.isEmpty()) {
+ // Empty selection data = output all occurrence entities with null filters.
+ // Use the list of all possible entity groups in the config.
+ Set outputEntities = new HashSet<>();
+ selectedEntityGroups.forEach(
+ entityGroup -> {
+ switch (entityGroup.getType()) {
+ case CRITERIA_OCCURRENCE:
+ CriteriaOccurrence criteriaOccurrence = (CriteriaOccurrence) entityGroup;
+ outputEntities.addAll(criteriaOccurrence.getOccurrenceEntities());
+ break;
+ case GROUP_ITEMS:
+ GroupItems groupItems = (GroupItems) entityGroup;
+ outputEntities.add(
+ groupItems.getItemsEntity().isPrimary()
+ ? groupItems.getGroupEntity()
+ : groupItems.getItemsEntity());
+ break;
+ default:
+ throw new SystemException(
+ "Unsupported entity group type: " + entityGroup.getType());
+ }
+ });
+ return outputEntities.stream().map(EntityOutput::unfiltered).collect(Collectors.toList());
+ } else {
+ // Check that there are no group by modifiers.
+ Optional>
+ groupByModifierConfigAndData =
+ GroupByCountSchemaUtils.getModifier(criteriaSelector, modifiersSelectionData);
+ if (groupByModifierConfigAndData.isPresent()) {
+ throw new InvalidQueryException("Group by modifiers are not supported for data features");
+ }
+
+ // We want to build filters per entity group, not per selected id.
+ ValueDataOuterClass.ValueData valueData = valueData(criteriaSelectionData);
+
+ Map> filtersPerEntity = new HashMap<>();
+ selectedEntityGroups.forEach(
+ entityGroup -> {
+ Map> filtersForSingleEntityGroup;
+ List selectedIds = selectedIdsPerEntityGroup.get(entityGroup);
+ if (selectedIds == null) {
+ selectedIds = new ArrayList<>();
+ }
+
+ switch (entityGroup.getType()) {
+ case CRITERIA_OCCURRENCE:
+ CriteriaOccurrence criteriaOccurrence = (CriteriaOccurrence) entityGroup;
+ filtersForSingleEntityGroup =
+ EntityGroupFilterUtils.addOccurrenceFiltersForDataFeature(
+ underlay, criteriaOccurrence, selectedIds);
+ EntityGroupFilterUtils.buildAllModifierFilters(
+ underlay,
+ criteriaOccurrence.getOccurrenceEntities(),
+ criteriaSelector,
+ valueData,
+ modifiersSelectionData,
+ filtersForSingleEntityGroup);
+ break;
+ case GROUP_ITEMS:
+ GroupItems groupItems = (GroupItems) entityGroup;
+ Entity notPrimaryEntity =
+ groupItems.getGroupEntity().isPrimary()
+ ? groupItems.getItemsEntity()
+ : groupItems.getGroupEntity();
+
+ filtersForSingleEntityGroup = new HashMap<>();
+ EntityFilter idSubFilter =
+ EntityGroupFilterUtils.buildIdSubFilter(
+ underlay, notPrimaryEntity, selectedIds);
+ if (idSubFilter == null) {
+ filtersForSingleEntityGroup.put(notPrimaryEntity, new ArrayList<>());
+ } else {
+ filtersForSingleEntityGroup.put(
+ notPrimaryEntity, new ArrayList<>(List.of(idSubFilter)));
+ }
+ EntityGroupFilterUtils.buildAllModifierFilters(
+ underlay,
+ List.of(notPrimaryEntity),
+ criteriaSelector,
+ valueData,
+ modifiersSelectionData,
+ filtersForSingleEntityGroup);
+ break;
+ default:
+ throw new SystemException(
+ "Unsupported entity group type: " + entityGroup.getType());
+ }
+
+ List entityOutputsForSingleEntityGroup =
+ EntityGroupFilterUtils.mergeFiltersForDataFeature(
+ filtersForSingleEntityGroup, BooleanAndOrFilter.LogicalOperator.AND);
+ entityOutputsForSingleEntityGroup.forEach(
+ entityOutput -> {
+ List filters =
+ filtersPerEntity.getOrDefault(entityOutput.getEntity(), new ArrayList<>());
+ if (entityOutput.hasDataFeatureFilter()) {
+ filters.add(entityOutput.getDataFeatureFilter());
+ } else {
+ filters = new ArrayList<>();
+ }
+ filtersPerEntity.put(entityOutput.getEntity(), filters);
+ });
+ });
+
+ // If there are multiple filters for a single entity, OR them together.
+ return EntityGroupFilterUtils.mergeFiltersForDataFeature(
+ filtersPerEntity, BooleanAndOrFilter.LogicalOperator.OR);
+ }
+ }
+
+ private Map> selectedIdsPerEntityGroup(
+ Underlay underlay, String serializedSelectionData) {
+ Map> selectedIdsPerEntityGroup = new HashMap<>();
+ Map selectedIdsAndEntityGroups =
+ selectedIdsAndEntityGroups(serializedSelectionData);
+ selectedIdsAndEntityGroups.forEach(
+ (key, entityGroupId) -> {
+ EntityGroup entityGroup = underlay.getEntityGroup(entityGroupId);
+ List selectedIds =
+ selectedIdsPerEntityGroup.containsKey(entityGroup)
+ ? selectedIdsPerEntityGroup.get(entityGroup)
+ : new ArrayList<>();
+ selectedIds.add(key);
+ selectedIdsPerEntityGroup.put(entityGroup, selectedIds);
+ });
+
+ // Sort selected IDs so they're consistent for tests rather than returning them in the original
+ // selection order.
+ selectedIdsPerEntityGroup.forEach(
+ (entityGroup, selectedIds) -> {
+ Collections.sort(selectedIds);
+ });
+ return selectedIdsPerEntityGroup;
+ }
+
+ // Returns a list of the union of entity groups covered by the selected items or all configured
+ // entity groups if no items are selected.
+ private List selectedEntityGroups(
+ Underlay underlay, Map> selectedIdsPerEntityGroup) {
+ List selectedEntityGroups;
+ if (!selectedIdsPerEntityGroup.isEmpty()) {
+ selectedEntityGroups = new ArrayList<>(selectedIdsPerEntityGroup.keySet());
+ } else {
+ selectedEntityGroups =
+ entityGroupIds().stream()
+ .map(
+ entityGroupId -> {
+ return underlay.getEntityGroup(entityGroupId);
+ })
+ .collect(Collectors.toList());
+ }
+
+ return selectedEntityGroups.stream()
+ .sorted(Comparator.comparing(EntityGroup::getName))
+ .collect(Collectors.toList());
+ }
+
+ private EntityFilter buildPrimaryWithCriteriaFilter(
+ Underlay underlay,
+ CriteriaOccurrence criteriaOccurrence,
+ List selectedIds,
+ String serializedSelectionData,
+ List modifiersSelectionData) {
+ // Build the criteria sub-filter.
+ EntityFilter criteriaSubFilter =
+ EntityGroupFilterUtils.buildIdSubFilter(
+ underlay, criteriaOccurrence.getCriteriaEntity(), selectedIds);
+
+ // Build the attribute modifier filters.
+ Map> subFiltersPerOccurrenceEntity =
+ EntityGroupFilterUtils.buildAttributeModifierFilters(
+ underlay,
+ criteriaSelector,
+ modifiersSelectionData,
+ criteriaOccurrence.getOccurrenceEntities());
+
+ // Build the instance-level modifier filters.
+ ValueDataOuterClass.ValueData valueData = valueData(serializedSelectionData);
+ if (valueData != null
+ && !IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY.equalsIgnoreCase(valueData.getAttribute())) {
+ if (criteriaOccurrence.getOccurrenceEntities().size() > 1) {
+ throw new InvalidQueryException(
+ "Instance-level modifiers are not supported for entity groups with multiple occurrence entities: "
+ + criteriaOccurrence.getName());
+ }
+ Entity occurrenceEntity = criteriaOccurrence.getOccurrenceEntities().get(0);
+
+ EntityFilter attrFilter =
+ AttributeSchemaUtils.buildForEntity(
+ underlay,
+ occurrenceEntity,
+ occurrenceEntity.getAttribute(valueData.getAttribute()),
+ valueData);
+ List subFilters =
+ subFiltersPerOccurrenceEntity.containsKey(occurrenceEntity)
+ ? subFiltersPerOccurrenceEntity.get(occurrenceEntity)
+ : new ArrayList<>();
+ subFilters.add(attrFilter);
+ subFiltersPerOccurrenceEntity.put(occurrenceEntity, subFilters);
+ }
+
+ Optional>
+ groupByModifierConfigAndData =
+ GroupByCountSchemaUtils.getModifier(criteriaSelector, modifiersSelectionData);
+ if (groupByModifierConfigAndData.isEmpty()
+ || groupByModifierConfigAndData.get().getRight() == null) {
+ return new PrimaryWithCriteriaFilter(
+ underlay,
+ criteriaOccurrence,
+ criteriaSubFilter,
+ subFiltersPerOccurrenceEntity,
+ null,
+ null,
+ null);
+ }
+
+ // Build the group by filter information.
+ Map> groupByAttributesPerOccurrenceEntity =
+ GroupByCountSchemaUtils.getGroupByAttributesPerOccurrenceEntity(
+ underlay, groupByModifierConfigAndData, criteriaOccurrence.getOccurrenceEntities());
+ DTUnhintedValue.UnhintedValue groupByModifierData =
+ groupByModifierConfigAndData.get().getRight();
+ return new PrimaryWithCriteriaFilter(
+ underlay,
+ criteriaOccurrence,
+ criteriaSubFilter,
+ subFiltersPerOccurrenceEntity,
+ groupByAttributesPerOccurrenceEntity,
+ GroupByCountSchemaUtils.toBinaryOperator(groupByModifierData.getOperator()),
+ (int) groupByModifierData.getMin());
+ }
+
+ private EntityFilter buildGroupItemsFilter(
+ Underlay underlay,
+ GroupItems groupItems,
+ List selectedIds,
+ List modifiersSelectionData) {
+ Entity notPrimaryEntity =
+ groupItems.getGroupEntity().isPrimary()
+ ? groupItems.getItemsEntity()
+ : groupItems.getGroupEntity();
+
+ // Build the sub-filters on the non-primary entity.
+ List idFilterNonPrimaryEntity = new ArrayList<>();
+ if (!selectedIds.isEmpty()) {
+ idFilterNonPrimaryEntity.add(
+ EntityGroupFilterUtils.buildIdSubFilter(underlay, notPrimaryEntity, selectedIds));
+ }
+ return EntityGroupFilterUtils.buildGroupItemsFilter(
+ underlay, criteriaSelector, groupItems, idFilterNonPrimaryEntity, modifiersSelectionData);
+ }
+
+ protected abstract List entityGroupIds();
+
+ protected abstract Map selectedIdsAndEntityGroups(
+ String serializedSelectionData);
+
+ protected abstract ValueDataOuterClass.ValueData valueData(String serializedSelectionData);
+}
diff --git a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/SurveyFilterBuilder.java b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/SurveyFilterBuilder.java
new file mode 100644
index 000000000..b7788cd5f
--- /dev/null
+++ b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/SurveyFilterBuilder.java
@@ -0,0 +1,62 @@
+package bio.terra.tanagra.filterbuilder.impl.core;
+
+import static bio.terra.tanagra.utils.ProtobufUtils.deserializeFromJsonOrProtoBytes;
+
+import bio.terra.tanagra.api.shared.Literal;
+import bio.terra.tanagra.proto.criteriaselector.ValueDataOuterClass;
+import bio.terra.tanagra.proto.criteriaselector.configschema.CFSurvey;
+import bio.terra.tanagra.proto.criteriaselector.dataschema.DTSurvey;
+import bio.terra.tanagra.underlay.uiplugin.CriteriaSelector;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class SurveyFilterBuilder extends EntityGroupFilterBuilderBase {
+ public SurveyFilterBuilder(CriteriaSelector criteriaSelector) {
+ super(criteriaSelector);
+ }
+
+ @Override
+ public CFSurvey.Survey deserializeConfig() {
+ return deserializeFromJsonOrProtoBytes(
+ criteriaSelector.getPluginConfig(), CFSurvey.Survey.newBuilder())
+ .build();
+ }
+
+ @Override
+ public DTSurvey.Survey deserializeData(String serialized) {
+ return (serialized == null || serialized.isEmpty())
+ ? null
+ : deserializeFromJsonOrProtoBytes(serialized, DTSurvey.Survey.newBuilder()).build();
+ }
+
+ @Override
+ protected List entityGroupIds() {
+ return deserializeConfig().getEntityGroupsList().stream()
+ .map(
+ entityGroup -> {
+ return entityGroup.getId();
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ protected Map selectedIdsAndEntityGroups(String serializedSelectionData) {
+ Map idsAndEntityGroups = new HashMap<>();
+ DTSurvey.Survey selectionData = deserializeData(serializedSelectionData);
+ for (DTSurvey.Survey.Selection selectedId : selectionData.getSelectedList()) {
+ if (selectedId.hasKey()) {
+ idsAndEntityGroups.put(
+ Literal.forInt64(selectedId.getKey().getInt64Key()), selectedId.getEntityGroup());
+ }
+ }
+ return idsAndEntityGroups;
+ }
+
+ @Override
+ protected ValueDataOuterClass.ValueData valueData(String serializedSelectionData) {
+ DTSurvey.Survey selectionData = deserializeData(serializedSelectionData);
+ return selectionData.hasValueData() ? selectionData.getValueData() : null;
+ }
+}
diff --git a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/utils/EntityGroupFilterUtils.java b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/utils/EntityGroupFilterUtils.java
index 1403e78f2..d2de7b56a 100644
--- a/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/utils/EntityGroupFilterUtils.java
+++ b/underlay/src/main/java/bio/terra/tanagra/filterbuilder/impl/core/utils/EntityGroupFilterUtils.java
@@ -14,10 +14,10 @@
import bio.terra.tanagra.api.shared.NaryOperator;
import bio.terra.tanagra.exception.InvalidQueryException;
import bio.terra.tanagra.filterbuilder.EntityOutput;
+import bio.terra.tanagra.proto.criteriaselector.ValueDataOuterClass;
import bio.terra.tanagra.proto.criteriaselector.configschema.CFAttribute;
import bio.terra.tanagra.proto.criteriaselector.configschema.CFUnhintedValue;
import bio.terra.tanagra.proto.criteriaselector.dataschema.DTAttribute;
-import bio.terra.tanagra.proto.criteriaselector.dataschema.DTEntityGroup;
import bio.terra.tanagra.proto.criteriaselector.dataschema.DTUnhintedValue;
import bio.terra.tanagra.underlay.Underlay;
import bio.terra.tanagra.underlay.entitymodel.Attribute;
@@ -189,7 +189,7 @@ public static void buildAllModifierFilters(
Underlay underlay,
List occurrenceEntities,
CriteriaSelector criteriaSelector,
- DTEntityGroup.EntityGroup entityGroupSelectionData,
+ ValueDataOuterClass.ValueData valueData,
List modifiersSelectionData,
Map> filtersPerEntity) {
// Build the attribute modifier filters.
@@ -206,9 +206,8 @@ public static void buildAllModifierFilters(
});
// Build the instance-level modifier filters.
- if (entityGroupSelectionData.hasValueData()
- && !IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY.equalsIgnoreCase(
- entityGroupSelectionData.getValueData().getAttribute())) {
+ if (valueData != null
+ && !IGNORED_ATTRIBUTE_NAME_UI_USE_ONLY.equalsIgnoreCase(valueData.getAttribute())) {
if (occurrenceEntities.size() > 1) {
throw new InvalidQueryException(
"Instance-level modifiers are not supported for entity groups with multiple occurrence entities");
@@ -219,8 +218,8 @@ public static void buildAllModifierFilters(
AttributeSchemaUtils.buildForEntity(
underlay,
occurrenceEntity,
- occurrenceEntity.getAttribute(entityGroupSelectionData.getValueData().getAttribute()),
- entityGroupSelectionData.getValueData());
+ occurrenceEntity.getAttribute(valueData.getAttribute()),
+ valueData);
List subFilters =
filtersPerEntity.containsKey(occurrenceEntity)
? filtersPerEntity.get(occurrenceEntity)
diff --git a/underlay/src/main/java/bio/terra/tanagra/underlay/serialization/SZCorePlugin.java b/underlay/src/main/java/bio/terra/tanagra/underlay/serialization/SZCorePlugin.java
index 110c973ae..d762c2b05 100644
--- a/underlay/src/main/java/bio/terra/tanagra/underlay/serialization/SZCorePlugin.java
+++ b/underlay/src/main/java/bio/terra/tanagra/underlay/serialization/SZCorePlugin.java
@@ -25,7 +25,9 @@ public enum SZCorePlugin {
@AnnotatedField(
name = "SZCorePlugin.OUTPUT_UNFILTERED",
markdown = "Use `plugin: \"outputUnfiltered\"`.")
- OUTPUT_UNFILTERED("outputUnfiltered");
+ OUTPUT_UNFILTERED("outputUnfiltered"),
+ @AnnotatedField(name = "SZCorePlugin.SURVEY", markdown = "Use `plugin: \"survey\"`.")
+ SURVEY("survey");
private final String idInConfig;
SZCorePlugin(String idInConfig) {
diff --git a/underlay/src/main/proto/column.proto b/underlay/src/main/proto/column.proto
new file mode 100644
index 000000000..76bc37c39
--- /dev/null
+++ b/underlay/src/main/proto/column.proto
@@ -0,0 +1,29 @@
+syntax = "proto3";
+
+package tanagra;
+
+option go_package = "github.com/DataBiosphere/tanagra/tanagrapb";
+
+// Defines a column in the UI.
+message Column {
+ // A unique key for the column. By default, used to look up attributes in
+ // the displayed data.
+ string key = 1;
+
+ oneof width {
+ // Passed directly to the style of the column. "100%" can be used to take
+ // up space remaining after laying out fixed columns.
+ string width_string = 2;
+ // Units used by the UI library to standardize dimensions.
+ double width_double = 3;
+ }
+
+ // The visible title of the column.
+ string title = 4;
+
+ // Whether the column supports sorting.
+ bool sortable = 5;
+
+ // Whether the column supports filtering.
+ bool filterable = 6;
+}
diff --git a/underlay/src/main/proto/criteriaselector/configschema/entity_group.proto b/underlay/src/main/proto/criteriaselector/configschema/entity_group.proto
index bfc76745a..c12bbd43b 100644
--- a/underlay/src/main/proto/criteriaselector/configschema/entity_group.proto
+++ b/underlay/src/main/proto/criteriaselector/configschema/entity_group.proto
@@ -6,38 +6,15 @@ option go_package = "github.com/DataBiosphere/tanagra/criteriaselector/configsch
option java_package = "bio.terra.tanagra.proto.criteriaselector.configschema";
option java_outer_classname = "CFEntityGroup";
-import "sort_order.proto";
+import "column.proto";
import "criteriaselector/value_config.proto";
+import "sort_order.proto";
// A criteria based on one or more entity groups. This allows the selection of
// primary entities which are related to one or more of another entity which
// match certain characteristics (e.g. people related to condition_occurrences
// which have condition_name of "Diabetes").
message EntityGroup {
- // Defines a column in the UI.
- message Column {
- // A unique key for the column. By default, used to look up attributes in
- // the displayed data.
- string key = 1;
-
- oneof width {
- // Passed directly to the style of the column. "100%" can be used to take
- // up space remaining after laying out fixed columns.
- string width_string = 2;
- // Units used by the UI library to standardize dimensions.
- double width_double = 3;
- }
-
- // The visible title of the column.
- string title = 4;
-
- // Whether the column supports sorting.
- bool sortable = 5;
-
- // Whether the column supports filtering.
- bool filterable = 6;
- }
-
// Columns displayed in the list view.
repeated Column columns = 1;
// Columns displayed in the hierarchy view.
diff --git a/underlay/src/main/proto/criteriaselector/configschema/survey.proto b/underlay/src/main/proto/criteriaselector/configschema/survey.proto
new file mode 100644
index 000000000..9b5b31f93
--- /dev/null
+++ b/underlay/src/main/proto/criteriaselector/configschema/survey.proto
@@ -0,0 +1,44 @@
+syntax = "proto3";
+
+package tanagra.configschema;
+
+option go_package = "github.com/DataBiosphere/tanagra/criteriaselector/configschemapb";
+option java_package = "bio.terra.tanagra.proto.criteriaselector.configschema";
+option java_outer_classname = "CFSurvey";
+
+import "column.proto";
+import "criteriaselector/value_config.proto";
+import "sort_order.proto";
+
+message Survey {
+ // Columns displayed in the list view.
+ repeated Column columns = 1;
+
+ message EntityGroupConfig {
+ // The id of the entity group.
+ string id = 1;
+
+ // The sort order applied to this entity group when displayed in the
+ // hierarchy view.
+ SortOrder sort_order = 2;
+ }
+
+ // Entity groups where the related entity is what is selected (e.g.
+ // surveyBasics when filtering surveyOccurrence).
+ repeated EntityGroupConfig entity_groups = 2;
+
+ // Optional configuration of a categorical or numeric value associated with
+ // the selection (e.g. a numeric answer). Applied to the entire selection
+ // so generally not compatible with multi_select. Currently only one is
+ // supported.
+ repeated ValueConfig value_configs = 3;
+
+ // The sort order to use in the list view, or in hierarchies where no sort
+ // order has been specified.
+ SortOrder default_sort = 4;
+
+ // The attribute used to name selections if not the first column. This can be
+ // used to include extra context with the selected values that's not visible
+ // in the table view.
+ optional string nameAttribute = 5;
+}
diff --git a/underlay/src/main/proto/criteriaselector/dataschema/survey.proto b/underlay/src/main/proto/criteriaselector/dataschema/survey.proto
new file mode 100644
index 000000000..ec789141d
--- /dev/null
+++ b/underlay/src/main/proto/criteriaselector/dataschema/survey.proto
@@ -0,0 +1,39 @@
+syntax = "proto3";
+
+package tanagra.dataschema;
+
+option go_package = "github.com/DataBiosphere/tanagra/criteriaselector/dataschemapb";
+option java_package = "bio.terra.tanagra.proto.criteriaselector.dataschema";
+option java_outer_classname = "DTSurvey";
+
+import "criteriaselector/key.proto";
+import "criteriaselector/value_data.proto";
+
+// Data for an entity group criteria is a list of selected values.
+message Survey {
+ message Selection {
+ // The key of the selected value, which references a related entity (e.g.
+ // surveyBasics when filtering surveyOccurrence).
+ Key key = 1;
+
+ // The visible name for the selection. This is stored to avoid extra lookups
+ // when rendering.
+ string name = 2;
+
+ // The entity group is stored to differentiate between them when multiple
+ // are configured within a single criteria.
+ string entityGroup = 3;
+
+ // If the selected item is an answer, the key of the question it belongs to.
+ Key question_key = 4;
+
+ // If the selected item is an answer, the visible name of the question it
+ // belongs to.
+ string question_name = 5;
+ }
+ repeated Selection selected = 1;
+
+ // Data for an additional categorical or numeric value associated with the
+ // selection (e.g. a numeric answer).
+ ValueData value_data = 2;
+}
diff --git a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForCriteriaOccurrenceTest.java b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForCriteriaOccurrenceTest.java
index 26c70358b..13a9d0ae5 100644
--- a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForCriteriaOccurrenceTest.java
+++ b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForCriteriaOccurrenceTest.java
@@ -123,7 +123,7 @@ void criteriaOnlyCohortFilter() {
underlay,
underlay.getEntity("condition"),
underlay.getEntity("condition").getHierarchy(Hierarchy.DEFAULT_NAME),
- List.of(Literal.forInt64(201_826L), Literal.forInt64(201_254L)));
+ List.of(Literal.forInt64(201_254L), Literal.forInt64(201_826L)));
expectedCohortFilter =
new PrimaryWithCriteriaFilter(
underlay,
@@ -871,7 +871,7 @@ void criteriaOnlySingleOccurrenceDataFeatureFilter() {
underlay,
underlay.getEntity("condition"),
underlay.getEntity("condition").getHierarchy(Hierarchy.DEFAULT_NAME),
- List.of(Literal.forInt64(201_826L), Literal.forInt64(201_254L)));
+ List.of(Literal.forInt64(201_254L), Literal.forInt64(201_826L)));
expectedDataFeatureFilter =
new OccurrenceForPrimaryFilter(
underlay,
diff --git a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForGroupTest.java b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForGroupTest.java
index b4e491b73..5ee7114c9 100644
--- a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForGroupTest.java
+++ b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForGroupTest.java
@@ -116,7 +116,7 @@ void criteriaOnlyCohortFilter() {
underlay,
underlay.getEntity("genotyping"),
underlay.getEntity("genotyping").getHierarchy(Hierarchy.DEFAULT_NAME),
- List.of(Literal.forInt64(30L), Literal.forInt64(3L)));
+ List.of(Literal.forInt64(3L), Literal.forInt64(30L)));
expectedCohortFilter =
new ItemInGroupFilter(
underlay,
@@ -638,7 +638,7 @@ void criteriaOnlyDataFeatureFilter() {
underlay,
underlay.getEntity("genotyping"),
underlay.getEntity("genotyping").getHierarchy(Hierarchy.DEFAULT_NAME),
- List.of(Literal.forInt64(30L), Literal.forInt64(3L)));
+ List.of(Literal.forInt64(3L), Literal.forInt64(30L)));
expectedDataFeatureOutput =
EntityOutput.filtered(underlay.getEntity("genotyping"), expectedDataFeatureFilter);
assertEquals(expectedDataFeatureOutput, dataFeatureOutputs.get(0));
diff --git a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForItemsTest.java b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForItemsTest.java
index 66cc45bf4..430c3e88c 100644
--- a/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForItemsTest.java
+++ b/underlay/src/test/java/bio/terra/tanagra/filterbuilder/EntityGroupFilterBuilderForItemsTest.java
@@ -52,7 +52,13 @@ void criteriaWithAttrModifiersCohortFilter() {
true,
SZCorePlugin.ATTRIBUTE.getIdInConfig(),
serializeToJson(systolicConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -72,13 +78,7 @@ void criteriaWithAttrModifiersCohortFilter() {
.build();
SelectionData systolicSelectionData =
new SelectionData("systolic", serializeToJson(systolicData));
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
EntityFilter cohortFilter =
@@ -117,7 +117,13 @@ void criteriaWithGroupByModifierCohortFilter() {
false,
SZCorePlugin.UNHINTED_VALUE.getIdInConfig(),
serializeToJson(groupByConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -139,13 +145,7 @@ void criteriaWithGroupByModifierCohortFilter() {
.build();
SelectionData groupBySelectionData =
new SelectionData("group_by_count", serializeToJson(groupByData));
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
EntityFilter cohortFilter =
@@ -185,7 +185,13 @@ void criteriaWithAttrAndGroupByModifiersCohortFilter() {
false,
SZCorePlugin.UNHINTED_VALUE.getIdInConfig(),
serializeToJson(groupByConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -213,13 +219,7 @@ void criteriaWithAttrAndGroupByModifiersCohortFilter() {
.build();
SelectionData groupBySelectionData =
new SelectionData("group_by_count", serializeToJson(groupByData));
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
EntityFilter cohortFilter =
@@ -247,7 +247,13 @@ void criteriaWithAttrAndGroupByModifiersCohortFilter() {
@Test
void emptyCriteriaCohortFilter() {
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -287,7 +293,13 @@ void emptyAttrModifierCohortFilter() {
true,
SZCorePlugin.ATTRIBUTE.getIdInConfig(),
serializeToJson(systolicConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -300,13 +312,7 @@ void emptyAttrModifierCohortFilter() {
List.of(systolicModifier));
EntityGroupFilterBuilder filterBuilder = new EntityGroupFilterBuilder(criteriaSelector);
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
EntityFilter expectedCohortFilter =
@@ -349,7 +355,13 @@ void emptyGroupByModifierCohortFilter() {
false,
SZCorePlugin.UNHINTED_VALUE.getIdInConfig(),
serializeToJson(groupByConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -362,13 +374,7 @@ void emptyGroupByModifierCohortFilter() {
List.of(groupByModifier));
EntityGroupFilterBuilder filterBuilder = new EntityGroupFilterBuilder(criteriaSelector);
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
EntityFilter expectedCohortFilter =
@@ -399,7 +405,13 @@ void emptyGroupByModifierCohortFilter() {
@Test
void criteriaOnlyDataFeatureFilter() {
- CFEntityGroup.EntityGroup config = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -408,18 +420,12 @@ void criteriaOnlyDataFeatureFilter() {
true,
"core.EntityGroupFilterBuilder",
SZCorePlugin.ENTITY_GROUP.getIdInConfig(),
- serializeToJson(config),
+ serializeToJson(bloodPressureConfig),
List.of());
EntityGroupFilterBuilder filterBuilder = new EntityGroupFilterBuilder(criteriaSelector);
// No ids.
- DTEntityGroup.EntityGroup data =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup data = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData selectionData = new SelectionData("bloodPressure", serializeToJson(data));
List dataFeatureOutputs =
filterBuilder.buildForDataFeature(underlay, List.of(selectionData));
@@ -439,7 +445,13 @@ void criteriaWithAttrModifierDataFeatureFilter() {
true,
SZCorePlugin.ATTRIBUTE.getIdInConfig(),
serializeToJson(systolicConfig));
- CFEntityGroup.EntityGroup bloodPressureConfig = CFEntityGroup.EntityGroup.newBuilder().build();
+ CFEntityGroup.EntityGroup bloodPressureConfig =
+ CFEntityGroup.EntityGroup.newBuilder()
+ .addClassificationEntityGroups(
+ CFEntityGroup.EntityGroup.EntityGroupConfig.newBuilder()
+ .setId("bloodPressurePerson")
+ .build())
+ .build();
CriteriaSelector criteriaSelector =
new CriteriaSelector(
"bloodPressure",
@@ -459,13 +471,7 @@ void criteriaWithAttrModifierDataFeatureFilter() {
.build();
SelectionData systolicSelectionData =
new SelectionData("systolic", serializeToJson(systolicData));
- DTEntityGroup.EntityGroup entityGroupData =
- DTEntityGroup.EntityGroup.newBuilder()
- .addSelected(
- DTEntityGroup.EntityGroup.Selection.newBuilder()
- .setEntityGroup("bloodPressurePerson")
- .build())
- .build();
+ DTEntityGroup.EntityGroup entityGroupData = DTEntityGroup.EntityGroup.newBuilder().build();
SelectionData entityGroupSelectionData =
new SelectionData("bloodPressure", serializeToJson(entityGroupData));
List dataFeatureOutputs =