Skip to content

Commit 2f746d3

Browse files
committed
fix: Fix create audience dialog
1 parent 1948f96 commit 2f746d3

4 files changed

Lines changed: 203 additions & 6 deletions

File tree

src/components/PushAudienceDialog/PushAudienceDialog.react.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ export default class PushAudienceDialog extends React.Component {
7272
stateSettings.platforms = deviceType.$in || [];
7373
}
7474
if (audienceInfo.filters) {
75-
stateSettings.filters = audienceInfo.filters;
75+
stateSettings.filters = audienceInfo.filters.map(filter =>
76+
filter.get('class') ? filter : filter.set('class', '_Installation')
77+
);
7678
}
7779
if (audienceInfo.name) {
7880
stateSettings.audienceName = audienceInfo.name;
@@ -100,10 +102,21 @@ export default class PushAudienceDialog extends React.Component {
100102
return;
101103
}
102104
const available = Filters.availableFilters(this.props.schema, this.state.filters);
103-
const field = Object.keys(available)[0];
105+
106+
const keys = Object.keys(available);
107+
if (keys.length === 0) {
108+
this.setState({
109+
errorMessage: 'No condition available.',
110+
});
111+
return;
112+
}
113+
114+
const field = keys[0];
104115
this.setState(
105116
({ filters }) => ({
106-
filters: filters.push(new Map({ field: field, constraint: available[field][0] })),
117+
filters: filters.push(
118+
new Map({ class: '_Installation', field: field, constraint: available[field][0] })
119+
),
107120
}),
108121
this.fetchAudienceSize.bind(this)
109122
);
@@ -284,11 +297,17 @@ export default class PushAudienceDialog extends React.Component {
284297
/>
285298
<div className={styles.filter}>
286299
<Filter
287-
schema={this.props.schema}
300+
className="_Installation"
301+
schema={{ _Installation: this.props.schema }}
302+
allClasses={{ _Installation: this.props.schema }}
288303
filters={this.state.filters}
289304
onChange={filters => {
290-
this.setState({ filters }, this.fetchAudienceSize.bind(this));
305+
this.setState(
306+
{ filters, errorMessage: undefined },
307+
this.fetchAudienceSize.bind(this)
308+
);
291309
}}
310+
onSearch={this.fetchAudienceSize.bind(this)}
292311
renderRow={props => <InstallationCondition {...props} />}
293312
/>
294313
</div>
@@ -307,7 +326,7 @@ export default class PushAudienceDialog extends React.Component {
307326
<FormNote
308327
show={Boolean(
309328
(this.props.errorMessage && this.props.errorMessage.length > 0) ||
310-
(this.state.errorMessage && this.state.errorMessage.length > 0)
329+
(this.state.errorMessage && this.state.errorMessage.length > 0)
311330
)}
312331
color="red"
313332
>

src/lib/Filters.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ export function availableFilters(schema, currentFilters, blacklist) {
260260

261261
export function findRelatedClasses(referClass, allClasses, blacklist, currentFilters) {
262262
const relatedClasses = {};
263+
if (!allClasses) {
264+
return relatedClasses;
265+
}
263266
if (allClasses[referClass]) {
264267
const availableForRefer = availableFilters(allClasses[referClass], currentFilters, blacklist);
265268
if (Object.keys(availableForRefer).length > 0) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
const { findRelatedClasses } = require('../Filters');
9+
10+
describe('findRelatedClasses', () => {
11+
it('returns an empty object when all classes are undefined', () => {
12+
expect(findRelatedClasses('_Installation', undefined, [], undefined)).toEqual({});
13+
});
14+
15+
it('returns the available filters for the referenced class', () => {
16+
const allClasses = { _Installation: { deviceType: { type: 'String' } } };
17+
const result = findRelatedClasses('_Installation', allClasses, [], undefined);
18+
19+
expect(result._Installation).toBeDefined();
20+
expect(result._Installation.deviceType).toContain('exists');
21+
});
22+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
jest.dontMock('../../components/PushAudienceDialog/PushAudienceDialog.react');
9+
jest.mock('../../components/Filter/Filter.react', () => ({
10+
__esModule: true,
11+
default: function Filter() {
12+
return null;
13+
},
14+
}));
15+
jest.mock(
16+
'context/currentApp',
17+
() => ({
18+
CurrentApp: null,
19+
}),
20+
{ virtual: true }
21+
);
22+
23+
const Filter = require('../../components/Filter/Filter.react').default;
24+
const PushAudienceDialog =
25+
require('../../components/PushAudienceDialog/PushAudienceDialog.react').default;
26+
const React = require('react');
27+
const { List, Map } = require('immutable');
28+
29+
const defaultProps = {
30+
availableDevices: [],
31+
primaryAction: jest.fn(),
32+
secondaryAction: jest.fn(),
33+
};
34+
35+
function createDialog(schema, audienceInfo) {
36+
const dialog = new PushAudienceDialog();
37+
dialog.props = {
38+
...defaultProps,
39+
audienceInfo,
40+
schema,
41+
};
42+
dialog.fetchAudienceSize = jest.fn();
43+
dialog.setState = (update, callback) => {
44+
const stateUpdate = typeof update === 'function' ? update(dialog.state, dialog.props) : update;
45+
dialog.state = {
46+
...dialog.state,
47+
...stateUpdate,
48+
};
49+
callback?.();
50+
};
51+
return dialog;
52+
}
53+
54+
function findElementByType(children, type) {
55+
let match;
56+
React.Children.forEach(children, child => {
57+
if (match || !React.isValidElement(child)) {
58+
return;
59+
}
60+
if (child.type === type) {
61+
match = child;
62+
return;
63+
}
64+
match = findElementByType(child.props.children, type);
65+
});
66+
return match;
67+
}
68+
69+
describe('PushAudienceDialog', () => {
70+
beforeEach(() => {
71+
jest.clearAllMocks();
72+
});
73+
74+
it('configures the shared filter for the Installation class', () => {
75+
const schema = {
76+
deviceType: { type: 'String' },
77+
};
78+
const dialog = createDialog(schema);
79+
const filter = findElementByType(dialog.render().props.children, Filter);
80+
81+
expect(filter).toBeDefined();
82+
expect(filter.props.className).toBe('_Installation');
83+
expect(filter.props.schema).toEqual({ _Installation: schema });
84+
expect(filter.props.allClasses).toEqual({ _Installation: schema });
85+
});
86+
87+
it('adds an available audience condition for the Installation class', () => {
88+
const dialog = createDialog({
89+
deviceType: { type: 'String' },
90+
});
91+
92+
dialog.handleAddCondition();
93+
94+
expect(dialog.state.filters.size).toBe(1);
95+
expect(dialog.state.filters.getIn([0, 'class'])).toBe('_Installation');
96+
expect(dialog.state.filters.getIn([0, 'field'])).toBe('deviceType');
97+
expect(dialog.state.filters.getIn([0, 'constraint'])).toBe('exists');
98+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
99+
});
100+
101+
it('shows an error when no audience condition is available', () => {
102+
const dialog = createDialog({
103+
unsupported: { type: 'File' },
104+
});
105+
106+
dialog.handleAddCondition();
107+
108+
expect(dialog.state.filters.size).toBe(0);
109+
expect(dialog.state.errorMessage).toBe('No condition available.');
110+
});
111+
112+
it('normalizes persisted audience filters for the Installation class', () => {
113+
const filters = new List([
114+
new Map({
115+
field: 'deviceType',
116+
constraint: 'exists',
117+
}),
118+
]);
119+
const dialog = createDialog(
120+
{ deviceType: { type: 'String' } },
121+
{
122+
filters,
123+
}
124+
);
125+
126+
dialog.componentWillMount();
127+
128+
expect(dialog.state.filters.getIn([0, 'class'])).toBe('_Installation');
129+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
130+
});
131+
132+
it('clears stale errors when filters change', () => {
133+
const schema = {
134+
deviceType: { type: 'String' },
135+
};
136+
const dialog = createDialog(schema);
137+
const filters = new List([
138+
new Map({
139+
class: '_Installation',
140+
field: 'deviceType',
141+
constraint: 'exists',
142+
}),
143+
]);
144+
dialog.state.errorMessage = 'No condition available.';
145+
const filter = findElementByType(dialog.render().props.children, Filter);
146+
147+
filter.props.onChange(filters);
148+
149+
expect(dialog.state.filters).toBe(filters);
150+
expect(dialog.state.errorMessage).toBeUndefined();
151+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
152+
});
153+
});

0 commit comments

Comments
 (0)