Skip to content

Commit c1dc5bb

Browse files
authored
feat: Add data browser filter condition containedIn (#2979)
1 parent b2a24de commit c1dc5bb

File tree

6 files changed

+73
-53
lines changed

6 files changed

+73
-53
lines changed

src/components/BrowserFilter/FilterRow.react.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,33 @@ function compareValue(
2828
onKeyDown,
2929
active,
3030
parentContentId,
31-
setFocus
31+
setFocus,
32+
currentConstraint
3233
) {
34+
if (currentConstraint === 'containedIn') {
35+
return (
36+
<input
37+
type="text"
38+
value={Array.isArray(value) ? JSON.stringify(value) : value || ''}
39+
placeholder="[1, 2, 3]"
40+
onChange={e => {
41+
try {
42+
const parsed = JSON.parse(e.target.value);
43+
if (Array.isArray(parsed)) {
44+
onChangeCompareTo(parsed);
45+
} else {
46+
onChangeCompareTo(e.target.value);
47+
}
48+
} catch {
49+
onChangeCompareTo(e.target.value);
50+
}
51+
}}
52+
onKeyDown={onKeyDown}
53+
ref={setFocus}
54+
/>
55+
);
56+
}
57+
3358
switch (info.type) {
3459
case null:
3560
return null;
@@ -223,7 +248,8 @@ const FilterRow = ({
223248
onKeyDown,
224249
active,
225250
parentContentId,
226-
setFocus
251+
setFocus,
252+
currentConstraint
227253
)}
228254
<button type="button" className={styles.remove} onClick={onDeleteRow}>
229255
<Icon name="minus-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />

src/components/Filter/Filter.react.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function changeConstraint(schema, currentClassName, filters, index, newConstrain
6262
field: field,
6363
constraint: newConstraint,
6464
compareTo:
65-
compareType && prevCompareTo ? prevCompareTo : Filters.DefaultComparisons[compareType],
65+
compareType && prevCompareTo ? prevCompareTo : newConstraint === 'containedIn' ? [] : Filters.DefaultComparisons[compareType],
6666
});
6767
return filters.set(index, newFilter);
6868
}

src/components/PushAudiencesSelector/PushAudiencesSelector.react.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const PushAudiencesOptions = ({ current, onChange, onEditAudience, schema, audie
2424
fromJS({
2525
field: 'deviceType',
2626
constraint: 'containedIn',
27-
array: query.deviceType['$in'],
27+
compareTo: query.deviceType['$in'],
2828
})
2929
)
3030
: query;

src/dashboard/Data/Views/Views.react.js

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,8 @@ class Views extends TableView {
572572
// Remove focus after action to follow UX best practices
573573
e.currentTarget.blur();
574574
}}
575-
aria-label={`Open all pointers in ${name} column in new tabs`}
576-
title="Open all pointers in new tabs"
575+
aria-label={`Filter to show all pointers from ${name} column`}
576+
title="Filter to show all pointers from this column"
577577
>
578578
<Icon
579579
name="right-outline"
@@ -870,59 +870,48 @@ class Views extends TableView {
870870
.map(row => row[columnName])
871871
.filter(value => value && value.__type === 'Pointer' && value.className && value.objectId);
872872

873-
// Open each unique pointer in a new tab
874-
const uniquePointers = new Map();
873+
if (pointers.length === 0) {
874+
this.showNote('No pointers found in this column', true);
875+
return;
876+
}
877+
878+
// Group pointers by target class
879+
const pointersByClass = new Map();
875880
pointers.forEach(pointer => {
876-
// Use a more collision-proof key format with explicit separators
877-
const key = `className:${pointer.className}|objectId:${pointer.objectId}`;
878-
if (!uniquePointers.has(key)) {
879-
uniquePointers.set(key, pointer);
881+
if (!pointersByClass.has(pointer.className)) {
882+
pointersByClass.set(pointer.className, new Set());
880883
}
884+
pointersByClass.get(pointer.className).add(pointer.objectId);
881885
});
882886

883-
if (uniquePointers.size === 0) {
884-
this.showNote('No pointers found in this column', true);
887+
// If multiple target classes, show error
888+
if (pointersByClass.size > 1) {
889+
const classNames = Array.from(pointersByClass.keys()).join(', ');
890+
this.showNote(`Cannot filter pointers from multiple classes: ${classNames}. Please use this feature on columns with pointers to a single class.`, true);
885891
return;
886892
}
887893

888-
const pointersArray = Array.from(uniquePointers.values());
894+
// Get the single target class and unique object IDs
895+
const targetClassName = Array.from(pointersByClass.keys())[0];
896+
const uniqueObjectIds = Array.from(pointersByClass.get(targetClassName));
889897

890-
// Confirm for large numbers of tabs to prevent overwhelming the user
891-
if (pointersArray.length > 10) {
892-
const confirmMessage = `This will open ${pointersArray.length} new tabs. This might overwhelm your browser. Continue?`;
893-
if (!confirm(confirmMessage)) {
894-
return;
895-
}
896-
}
898+
// Navigate to the target class with containedIn filter
899+
const filters = JSON.stringify([{
900+
field: 'objectId',
901+
constraint: 'containedIn',
902+
compareTo: uniqueObjectIds
903+
}]);
897904

898-
// Open all tabs immediately to maintain user activation context
899-
let errorCount = 0;
905+
const path = generatePath(
906+
this.context,
907+
`browser/${targetClassName}?filters=${encodeURIComponent(filters)}`,
908+
true
909+
);
900910

901-
pointersArray.forEach((pointer) => {
902-
try {
903-
const filters = JSON.stringify([{ field: 'objectId', constraint: 'eq', compareTo: pointer.objectId }]);
904-
const url = generatePath(
905-
this.context,
906-
`browser/${pointer.className}?filters=${encodeURIComponent(filters)}`,
907-
true
908-
);
909-
window.open(url, '_blank', 'noopener,noreferrer');
910-
// Note: window.open with security attributes may return null even when successful,
911-
// so we assume success unless an exception is thrown
912-
} catch (error) {
913-
console.error('Failed to open tab for pointer:', pointer, error);
914-
errorCount++;
915-
}
916-
});
911+
window.open(path, '_blank', 'noopener,noreferrer');
917912

918-
// Show result notification
919-
if (errorCount === 0) {
920-
this.showNote(`Opened ${pointersArray.length} pointer${pointersArray.length > 1 ? 's' : ''} in new tab${pointersArray.length > 1 ? 's' : ''}`, false);
921-
} else if (errorCount < pointersArray.length) {
922-
this.showNote(`Opened ${pointersArray.length - errorCount} of ${pointersArray.length} tabs. ${errorCount} failed to open.`, true);
923-
} else {
924-
this.showNote('Unable to open tabs. Please allow popups for this site and try again.', true);
925-
}
913+
// Show success notification
914+
this.showNote(`Applied filter to show ${uniqueObjectIds.length} pointer${uniqueObjectIds.length > 1 ? 's' : ''} from ${targetClassName}`, false);
926915
}
927916

928917
showNote(message, isError) {

src/lib/Filters.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,25 @@ export const Constraints = {
175175
field: null,
176176
comparable: false,
177177
},
178+
containedIn: {
179+
name: 'contained in',
180+
comparable: true,
181+
},
178182
};
179183

180184
export const FieldConstraints = {
181-
Pointer: ['exists', 'dne', 'eq', 'neq', 'starts', 'unique'],
182-
Boolean: ['exists', 'dne', 'eq', 'neq', 'unique'],
183-
Number: ['exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'unique'],
184-
String: ['exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString', 'unique'],
185+
Pointer: ['exists', 'dne', 'eq', 'neq', 'starts', 'containedIn', 'unique'],
186+
Boolean: ['exists', 'dne', 'eq', 'neq', 'containedIn', 'unique'],
187+
Number: ['exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'containedIn', 'unique'],
188+
String: ['exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString', 'containedIn', 'unique'],
185189
Date: [
186190
'exists',
187191
'dne',
188192
'before',
189193
'onOrBefore',
190194
'after',
191195
'onOrAfter',
196+
'containedIn',
192197
'unique',
193198
],
194199
Object: [

src/lib/queryFromFilters.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ function addConstraint(query, filter) {
166166
query.notEqualTo(filter.get('field'), filter.get('compareTo'));
167167
break;
168168
case 'containedIn':
169-
query.containedIn(filter.get('field'), filter.get('array'));
169+
query.containedIn(filter.get('field'), filter.get('compareTo'));
170170
break;
171171
case 'stringContainsString':
172172
query.matches(filter.get('field'), filter.get('compareTo'), 'i');

0 commit comments

Comments
 (0)