Skip to content

Commit 47bdef5

Browse files
authored
Merge pull request #284 from RobokopU24/feature/qualified-predicates
Qualified predicates
2 parents 554b7a5 + e5397bd commit 47bdef5

File tree

7 files changed

+691
-125
lines changed

7 files changed

+691
-125
lines changed

.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module.exports = {
2424
overrides: [{
2525
files: ['*.jsx', '*.js'],
2626
}],
27+
ignorePatterns: [
28+
'useBiolinkModel.js',
29+
'App.jsx',
30+
],
2731
rules: {
2832
indent: ['error', 2, { SwitchCase: 1 }],
2933
'max-len': 'off',
Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,66 @@
1-
#queryBuilderContainer > button {
2-
margin-left: 10px;
3-
}
4-
#queryEditorContainer {
5-
display: flex;
6-
}
7-
#queryTextEditor {
8-
width: 60%;
9-
padding: 20px;
10-
}
11-
#queryTextEditor > * {
12-
padding: 12px;
13-
}
14-
#queryGraphEditor {
15-
width: 40%;
16-
padding: 20px;
17-
}
18-
.textEditorRow {
19-
display: flex;
20-
align-items: center;
21-
justify-content: space-between;
22-
}
23-
.textEditorRow > p {
24-
margin: 0;
25-
font-size: 16px;
26-
}
27-
.textEditorIconButton > span > svg {
28-
font-size: 40px;
29-
color: #b2b0b0;
30-
}
31-
.textEditorIconButton:disabled > span > svg {
32-
color: #e0e0e0;
33-
}
34-
35-
#jsonEditorTitle {
36-
display: flex;
37-
justify-content: space-between;
38-
align-items: center;
39-
padding: 5px;
40-
}
41-
#uploadIconLabel {
42-
margin-bottom: 0;
43-
}
44-
45-
#queryBuilderButtons {
46-
width: 100%;
47-
padding: 0 20px;
48-
display: flex;
49-
gap: 16px;
50-
}
51-
52-
@media (max-width: 1450px) {
53-
#queryEditorContainer {
54-
flex-direction: column;
55-
align-items: center;
56-
}
57-
#queryGraphEditor {
58-
width: 100%;
59-
}
60-
#graphContainer {
61-
margin: auto;
62-
}
63-
#queryTextEditor {
64-
width: 100%;
65-
}
1+
#queryBuilderContainer > button {
2+
margin-left: 10px;
3+
}
4+
#queryEditorContainer {
5+
display: flex;
6+
}
7+
#queryTextEditor {
8+
width: 60%;
9+
padding: 20px;
10+
display: flex;
11+
flex-direction: column;
12+
gap: 30px;
13+
}
14+
#queryGraphEditor {
15+
width: 40%;
16+
padding: 20px;
17+
}
18+
.textEditorRow {
19+
display: flex;
20+
align-items: center;
21+
justify-content: space-between;
22+
}
23+
.textEditorRow > p {
24+
margin: 0;
25+
font-size: 16px;
26+
}
27+
.textEditorIconButton > span > svg {
28+
font-size: 40px;
29+
color: #b2b0b0;
30+
}
31+
.textEditorIconButton:disabled > span > svg {
32+
color: #e0e0e0;
33+
}
34+
35+
#jsonEditorTitle {
36+
display: flex;
37+
justify-content: space-between;
38+
align-items: center;
39+
padding: 5px;
40+
}
41+
#uploadIconLabel {
42+
margin-bottom: 0;
43+
}
44+
45+
#queryBuilderButtons {
46+
width: 100%;
47+
padding: 0 20px;
48+
display: flex;
49+
gap: 16px;
50+
}
51+
52+
@media (max-width: 1450px) {
53+
#queryEditorContainer {
54+
flex-direction: column;
55+
align-items: center;
56+
}
57+
#queryGraphEditor {
58+
width: 100%;
59+
}
60+
#graphContainer {
61+
margin: auto;
62+
}
63+
#queryTextEditor {
64+
width: 100%;
65+
}
6666
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* eslint-disable no-restricted-syntax */
2+
import React, { useContext } from 'react';
3+
import { TextField } from '@material-ui/core';
4+
import { Autocomplete } from '@material-ui/lab';
5+
import QueryBuilderContext from '~/context/queryBuilder';
6+
7+
const flattenTree = (root, includeMixins) => {
8+
const items = [root];
9+
if (root.children) {
10+
for (const child of root.children) {
11+
items.push(...flattenTree(child, includeMixins));
12+
}
13+
}
14+
if (root.mixinChildren && includeMixins === true) {
15+
for (const mixinChild of root.mixinChildren) {
16+
items.push(...flattenTree(mixinChild, includeMixins));
17+
}
18+
}
19+
return items;
20+
};
21+
22+
const getQualifierOptions = ({ range, subpropertyOf }) => {
23+
const options = [];
24+
25+
if (range) {
26+
if (range.permissible_values) {
27+
options.push(...Object.keys(range.permissible_values));
28+
} else {
29+
options.push(...flattenTree(range).map(({ name }) => name));
30+
}
31+
}
32+
33+
if (subpropertyOf) {
34+
options.push(...flattenTree(subpropertyOf).map(({ name }) => name));
35+
}
36+
37+
return options;
38+
};
39+
40+
// const getBestAssociationOption = (associationOptions) => {
41+
// let best = null;
42+
// for (const opt of associationOptions) {
43+
// if (opt.qualifiers.length > (best.length || 0)) best = opt;
44+
// }
45+
// return best;
46+
// };
47+
48+
export default function QualifiersSelector({ id, associations }) {
49+
const queryBuilder = useContext(QueryBuilderContext);
50+
51+
const associationOptions = associations
52+
.filter((a) => a.qualifiers.length > 0)
53+
.map(({ association, qualifiers }) => ({
54+
name: association.name,
55+
uuid: association.uuid,
56+
qualifiers: qualifiers.map((q) => ({
57+
name: q.qualifier.name,
58+
options: getQualifierOptions(q),
59+
})),
60+
}));
61+
62+
const [value, setValue] = React.useState(associationOptions[0] || null);
63+
const [qualifiers, setQualifiers] = React.useState({});
64+
React.useEffect(() => {
65+
queryBuilder.dispatch({ type: 'editQualifiers', payload: { id, qualifiers } });
66+
}, [qualifiers]);
67+
68+
if (associationOptions.length === 0) return null;
69+
if (associationOptions.length === 1 && associationOptions[0].name === 'association') return null;
70+
71+
const subjectQualfiers = value.qualifiers.filter(({ name }) => name.includes('subject'));
72+
const predicateQualifiers = value.qualifiers.filter(({ name }) => name.includes('predicate'));
73+
const objectQualifiers = value.qualifiers.filter(({ name }) => name.includes('object'));
74+
const otherQualifiers = value.qualifiers.filter((q) => (
75+
!subjectQualfiers.includes(q) &&
76+
!predicateQualifiers.includes(q) &&
77+
!objectQualifiers.includes(q)
78+
));
79+
80+
return (
81+
<div className="qualifiers-dropdown">
82+
<div style={{ marginRight: '2rem' }}>
83+
<Autocomplete
84+
value={value}
85+
onChange={(_, newValue) => {
86+
setValue(newValue);
87+
}}
88+
disableClearable
89+
size="small"
90+
options={associationOptions}
91+
getOptionLabel={(option) => option.name}
92+
getOptionSelected={(opt, val) => opt.uuid === val.uuid}
93+
style={{ width: 300 }}
94+
renderInput={(params) => <TextField {...params} label="Association" variant="outlined" />}
95+
/>
96+
97+
{otherQualifiers.length > 0 && <hr />}
98+
99+
<QualifiersList
100+
value={otherQualifiers}
101+
qualifiers={qualifiers}
102+
setQualifiers={setQualifiers}
103+
/>
104+
</div>
105+
106+
<QualifiersList
107+
value={subjectQualfiers}
108+
qualifiers={qualifiers}
109+
setQualifiers={setQualifiers}
110+
/>
111+
<QualifiersList
112+
value={predicateQualifiers}
113+
qualifiers={qualifiers}
114+
setQualifiers={setQualifiers}
115+
/>
116+
<QualifiersList
117+
value={objectQualifiers}
118+
qualifiers={qualifiers}
119+
setQualifiers={setQualifiers}
120+
/>
121+
</div>
122+
);
123+
}
124+
125+
function QualifiersList({ value, qualifiers, setQualifiers }) {
126+
if (value.length === 0) return null;
127+
return (
128+
<div className="qualifiers-list">
129+
{value.map(({ name, options }) => (
130+
<Autocomplete
131+
key={name}
132+
value={qualifiers[name] || null}
133+
onChange={(_, newValue) => {
134+
if (newValue === null) {
135+
setQualifiers((prev) => {
136+
const next = { ...prev };
137+
delete next[name];
138+
return next;
139+
});
140+
} else { setQualifiers((prev) => ({ ...prev, [name]: newValue || null })); }
141+
}}
142+
options={options}
143+
renderInput={(params) => <TextField {...params} label={name} variant="outlined" />}
144+
size="small"
145+
/>
146+
))}
147+
</div>
148+
);
149+
}

0 commit comments

Comments
 (0)