Skip to content

Commit 9293f13

Browse files
TCBroadpaveltiunov
authored andcommitted
feat(vue): Add order, renewQuery, and reactivity to Vue component (#229). Thanks to @TCBroad
* feat(vue): made vue QueryBuilder reactive * feat(vue): Added renewQuery support to QueryBuilder * feat(vue): Added order support to QueryBuilder * feat(vue): Added tests for new functionality * feat(vue): refactored + fixed test for reactivity * feat(vue): Updated documentation
1 parent 466a849 commit 9293f13

File tree

3 files changed

+192
-53
lines changed

3 files changed

+192
-53
lines changed

docs/Cube.js-Frontend/@cubejs-client-vue.md

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ into Vue.js app.
1515

1616
### Props
1717

18-
- `query`: analytic query. [Learn more about it's format](query-format).
18+
- `query`: query parameters ([learn more about its format](query-format)).
1919
- `cubejsApi`: `CubejsApi` instance to use.
2020

2121
### Slots
@@ -24,19 +24,21 @@ into Vue.js app.
2424

2525
##### Slot Props
2626

27-
- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convient interface for data munipulation.
27+
- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convenient interface for data manipulation.
2828

2929
#### Empty Slot
3030

31-
This slot functions as a empty/loading state in which if the query is loading or empty you can show
32-
something in the meantime
31+
This slot functions as a empty/loading state in which if the query is loading or empty so you can show
32+
something in the meantime.
3333

3434
#### Error Slot
3535

36+
This slot will be rendered if any error happens while the query is loading or rendering.
37+
3638
##### Slot Props
3739

38-
- `error`: will show the details from error.
39-
- `sqlQuery`: will show tried query
40+
- `error`: the error.
41+
- `sqlQuery`: the attempted query.
4042

4143
### Example
4244
```js
@@ -93,11 +95,12 @@ export default {
9395
```
9496

9597
## QueryBuilder
96-
`<QueryBuilder />` is used to build interactive analytics query builders. It abstracts state management and API calls to Cube.js Backend. It uses scoped slot props technique.
98+
`<QueryBuilder />` is used to build interactive analytics query builders. It abstracts state management and API calls to Cube.js Backend. It uses scoped slot props technique.
9799

98100
### Props
99101

100-
- `query`: default query.
102+
- `query`: query parameters ([learn more about its format](query-format)). This property is reactive - if you change the object here,
103+
the internal query values will be overwritten. This is not two-way.
101104
- `cubejsApi`: `CubejsApi` instance to use. Required.
102105
- `defaultChartType`: default value of chart type. Default: 'line'.
103106

@@ -107,38 +110,39 @@ export default {
107110

108111
##### Slot Props
109112

110-
- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convient interface for data munipulation.
113+
- `resultSet`: A `resultSet` is an object containing data obtained from the query. [ResultSet](@cubejs-client-core#result-set) object provides a convenient interface for data manipulation.
111114

112115
#### Empty Slot
113116

114117
This slot functions as a empty/loading state in which if the query is loading or empty you can show
115-
something in the meantime
118+
something in the meantime.
116119

117120
#### Error Slot
118121

122+
This slot will be rendered if any error happens while the query is loading or rendering.
123+
119124
##### Slot Props
120125

121-
- `error`: will show the details from error.
122-
- `sqlQuery`: will show tried query
126+
- `error`: the error.
127+
- `sqlQuery`: the attempted query.
123128

124129
#### Builder Slot
125130

126-
- `measures`, `dimensions`, `segments`, `timeDimensions`, `filters` - arrays of
131+
- `measures`, `dimensions`, `segments`, `timeDimensions`, `filters` - arrays containing the
127132
selected query builder members.
128133
- `availableMeasures`, `availableDimensions`, `availableTimeDimensions`,
129-
`availableSegments` - arrays of available to select members. They are loaded via
134+
`availableSegments` - arrays containing available members to select. They are loaded via
130135
API from Cube.js Backend.
131-
- `addMeasures`, `addDimensions`, `addSegments`, `addTimeDimensions` - function to control the adding of new members to query builder
132-
- `removeMeasures`, `removeDimensions`, `removeSegments`, `removeTimeDimensions` - function to control the removing of member to query builder
133-
- `setMeasures`, `setDimensions`, `setSegments`, `setTimeDimensions` - function to control the set of members to query builder
134-
- `updateMeasures`, `updateDimensions`, `updateSegments`, `updateTimeDimensions` - function to control the update of member to query builder
135-
- `chartType` - string, containing currently selected chart type.
136+
- `addMeasures`, `addDimensions`, `addSegments`, `addTimeDimensions` - functions to control the adding of new members to query builder.
137+
- `removeMeasures`, `removeDimensions`, `removeSegments`, `removeTimeDimensions` - functions to control the removing of members to query builder.
138+
- `setMeasures`, `setDimensions`, `setSegments`, `setTimeDimensions` - functions to control the setting of members to query builder.
139+
- `updateMeasures`, `updateDimensions`, `updateSegments`, `updateTimeDimensions` - functions to control the updating of members to query builder.
140+
- `chartType` - string containing currently selected chart type.
136141
- `updateChartType` - function-setter for chart type.
137-
- `isQueryPresent` - Bool indicating whether is query ready to be displayed or
138-
not.
142+
- `isQueryPresent` - bool indicating whether is query ready to be displayed or not.
139143
- `query` - current query, based on selected members.
140-
- `setLimit`, `removeLimit` - functions to control the number of results returned
141-
- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination
144+
- `setLimit`, `removeLimit` - functions to control the number of results returned.
145+
- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination.
142146

143147
### Example
144148
[Open in CodeSandbox](https://codesandbox.io/s/3rlxjkv2p)

packages/cubejs-vue/src/QueryBuilder.js

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export default {
2929
availableSegments: [],
3030
limit: null,
3131
offset: null,
32+
renewQuery: false,
33+
order: {}
3234
};
3335

3436
data.granularities = [
@@ -41,35 +43,7 @@ export default {
4143

4244
return data;
4345
},
44-
async mounted() {
45-
this.meta = await this.cubejsApi.meta();
4646

47-
const { measures, dimensions, segments, timeDimensions, filters, limit, offset } = this.query;
48-
49-
this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') }));
50-
this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') }));
51-
this.segments = (segments || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'segments') }));
52-
this.timeDimensions = (timeDimensions || []).map((m, i) => ({
53-
...m,
54-
dimension: { ...this.meta.resolveMember(m.dimension, 'dimensions'), granularities: this.granularities },
55-
index: i
56-
}));
57-
this.filters = (filters || []).map((m, i) => ({
58-
...m,
59-
// using 'dimension' is deprecated, 'member' should be specified instead
60-
member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
61-
operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']),
62-
index: i
63-
}));
64-
65-
this.availableMeasures = this.meta.membersForQuery({}, 'measures') || [];
66-
this.availableDimensions = this.meta.membersForQuery({}, 'dimensions') || [];
67-
this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || [])
68-
.filter(m => m.type === 'time');
69-
this.availableSegments = this.meta.membersForQuery({}, 'segments') || [];
70-
this.limit = (limit || null);
71-
this.offset = (offset || null);
72-
},
7347
render(createElement) {
7448
const {
7549
chartType,
@@ -93,6 +67,8 @@ export default {
9367
removeLimit,
9468
setOffset,
9569
removeOffset,
70+
renewQuery,
71+
order
9672
} = this;
9773

9874
let builderProps = {};
@@ -119,6 +95,8 @@ export default {
11995
removeLimit,
12096
setOffset,
12197
removeOffset,
98+
renewQuery,
99+
order
122100
};
123101

124102
QUERY_ELEMENTS.forEach((e) => {
@@ -164,7 +142,7 @@ export default {
164142
validatedQuery() {
165143
const validatedQuery = {};
166144
let toQuery = member => member.name;
167-
// TODO: implement order, timezone, renewQuery
145+
// TODO: implement timezone
168146

169147
let hasElements = false;
170148
QUERY_ELEMENTS.forEach((e) => {
@@ -208,13 +186,55 @@ export default {
208186
if (this.offset) {
209187
validatedQuery.offset = this.offset;
210188
}
211-
// add order
189+
190+
if (this.order) {
191+
validatedQuery.order = this.order;
192+
}
193+
194+
if (this.renewQuery) {
195+
validatedQuery.renewQuery = this.renewQuery;
196+
}
212197
}
213198

214199
return validatedQuery;
215200
},
216201
},
202+
203+
async mounted() {
204+
this.meta = await this.cubejsApi.meta();
205+
206+
this.copyQueryFromProps();
207+
},
208+
217209
methods: {
210+
copyQueryFromProps() {
211+
const { measures, dimensions, segments, timeDimensions, filters, limit, offset, renewQuery, order } = this.query;
212+
213+
this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') }));
214+
this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') }));
215+
this.segments = (segments || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'segments') }));
216+
this.timeDimensions = (timeDimensions || []).map((m, i) => ({
217+
...m,
218+
dimension: { ...this.meta.resolveMember(m.dimension, 'dimensions'), granularities: this.granularities },
219+
index: i
220+
}));
221+
this.filters = (filters || []).map((m, i) => ({
222+
...m,
223+
member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
224+
operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']),
225+
index: i
226+
}));
227+
228+
this.availableMeasures = this.meta.membersForQuery({}, 'measures') || [];
229+
this.availableDimensions = this.meta.membersForQuery({}, 'dimensions') || [];
230+
this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || [])
231+
.filter(m => m.type === 'time');
232+
this.availableSegments = this.meta.membersForQuery({}, 'segments') || [];
233+
this.limit = (limit || null);
234+
this.offset = (offset || null);
235+
this.renewQuery = (renewQuery || false);
236+
this.order = (order || {});
237+
},
218238
addMember(element, member) {
219239
const name = element.charAt(0).toUpperCase() + element.slice(1);
220240
let mem;
@@ -364,4 +384,17 @@ export default {
364384
this.chartType = chartType;
365385
},
366386
},
387+
388+
watch: {
389+
query() {
390+
if (!this.meta) {
391+
// this is ok as if meta has not been loaded by the time query prop has changed,
392+
// then the promise for loading meta (found in mounted()) will call
393+
// copyQueryFromProps and will therefore update anyway.
394+
return;
395+
}
396+
397+
this.copyQueryFromProps();
398+
}
399+
}
367400
};

packages/cubejs-vue/tests/unit/QueryBuilder.spec.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,5 +492,107 @@ describe('QueryBuilder.vue', () => {
492492

493493
expect(wrapper.vm.offset).toBe(10);
494494
});
495+
496+
it('sets renewQuery', async () => {
497+
const cube = CubejsApi('token');
498+
jest.spyOn(cube, 'request')
499+
.mockImplementation(fetchMock(load))
500+
.mockImplementationOnce(fetchMock(meta));
501+
502+
const filter = {
503+
member: 'Orders.status',
504+
operator: 'equals',
505+
values: ['invalid'],
506+
};
507+
508+
const wrapper = mount(QueryBuilder, {
509+
propsData: {
510+
cubejsApi: cube,
511+
query: {
512+
filters: [filter],
513+
renewQuery: true
514+
},
515+
},
516+
});
517+
518+
await flushPromises();
519+
520+
expect(wrapper.vm.renewQuery).toBe(true);
521+
});
522+
523+
it('sets order', async () => {
524+
const cube = CubejsApi('token');
525+
jest.spyOn(cube, 'request')
526+
.mockImplementation(fetchMock(load))
527+
.mockImplementationOnce(fetchMock(meta));
528+
529+
const filter = {
530+
member: 'Orders.status',
531+
operator: 'equals',
532+
values: ['invalid'],
533+
};
534+
535+
const wrapper = mount(QueryBuilder, {
536+
propsData: {
537+
cubejsApi: cube,
538+
query: {
539+
filters: [filter],
540+
order: {
541+
'Orders.status': 'desc'
542+
}
543+
},
544+
},
545+
});
546+
547+
await flushPromises();
548+
549+
expect(wrapper.vm.order['Orders.status']).toBe('desc');
550+
});
551+
552+
it('is reactive when filter is changed', async () => {
553+
const cube = CubejsApi('token');
554+
jest.spyOn(cube, 'request')
555+
.mockImplementation(fetchMock(load))
556+
.mockImplementationOnce(fetchMock(meta));
557+
558+
const filter = {
559+
member: 'Orders.status',
560+
operator: 'equals',
561+
values: ['invalid'],
562+
};
563+
564+
const newFilter = {
565+
dimension: 'Orders.number',
566+
operator: 'equals',
567+
values: ['1'],
568+
};
569+
570+
const wrapper = mount(QueryBuilder, {
571+
propsData: {
572+
cubejsApi: cube,
573+
query: {
574+
filters: [filter]
575+
},
576+
},
577+
});
578+
579+
await flushPromises();
580+
581+
expect(wrapper.vm.filters.length).toBe(1);
582+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
583+
expect(wrapper.vm.filters[0].values).toContain('invalid');
584+
585+
wrapper.setProps({
586+
query: {
587+
filters: [newFilter]
588+
}
589+
});
590+
591+
await flushPromises();
592+
593+
expect(wrapper.vm.filters.length).toBe(1);
594+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.number');
595+
expect(wrapper.vm.filters[0].values).toContain('1');
596+
});
495597
});
496598
});

0 commit comments

Comments
 (0)