Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/components/BrowserFilter/FilterRow.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,33 @@ function compareValue(
onKeyDown,
active,
parentContentId,
setFocus
setFocus,
currentConstraint
) {
if (currentConstraint === 'containedIn') {
return (
<input
type="text"
value={Array.isArray(value) ? JSON.stringify(value) : value || ''}
placeholder="[1, 2, 3]"
onChange={e => {
try {
const parsed = JSON.parse(e.target.value);
if (Array.isArray(parsed)) {
onChangeCompareTo(parsed);
} else {
onChangeCompareTo(e.target.value);
}
} catch {
onChangeCompareTo(e.target.value);
}
}}
onKeyDown={onKeyDown}
ref={setFocus}
/>
);
}

switch (info.type) {
case null:
return null;
Expand Down Expand Up @@ -223,7 +248,8 @@ const FilterRow = ({
onKeyDown,
active,
parentContentId,
setFocus
setFocus,
currentConstraint
)}
<button type="button" className={styles.remove} onClick={onDeleteRow}>
<Icon name="minus-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/Filter/Filter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function changeConstraint(schema, currentClassName, filters, index, newConstrain
field: field,
constraint: newConstraint,
compareTo:
compareType && prevCompareTo ? prevCompareTo : Filters.DefaultComparisons[compareType],
compareType && prevCompareTo ? prevCompareTo : newConstraint === 'containedIn' ? [] : Filters.DefaultComparisons[compareType],
});
return filters.set(index, newFilter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const PushAudiencesOptions = ({ current, onChange, onEditAudience, schema, audie
fromJS({
field: 'deviceType',
constraint: 'containedIn',
array: query.deviceType['$in'],
compareTo: query.deviceType['$in'],
})
)
: query;
Expand Down
77 changes: 33 additions & 44 deletions src/dashboard/Data/Views/Views.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,8 @@ class Views extends TableView {
// Remove focus after action to follow UX best practices
e.currentTarget.blur();
}}
aria-label={`Open all pointers in ${name} column in new tabs`}
title="Open all pointers in new tabs"
aria-label={`Filter to show all pointers from ${name} column`}
title="Filter to show all pointers from this column"
>
<Icon
name="right-outline"
Expand Down Expand Up @@ -870,59 +870,48 @@ class Views extends TableView {
.map(row => row[columnName])
.filter(value => value && value.__type === 'Pointer' && value.className && value.objectId);

// Open each unique pointer in a new tab
const uniquePointers = new Map();
if (pointers.length === 0) {
this.showNote('No pointers found in this column', true);
return;
}

// Group pointers by target class
const pointersByClass = new Map();
pointers.forEach(pointer => {
// Use a more collision-proof key format with explicit separators
const key = `className:${pointer.className}|objectId:${pointer.objectId}`;
if (!uniquePointers.has(key)) {
uniquePointers.set(key, pointer);
if (!pointersByClass.has(pointer.className)) {
pointersByClass.set(pointer.className, new Set());
}
pointersByClass.get(pointer.className).add(pointer.objectId);
});

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

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

// Confirm for large numbers of tabs to prevent overwhelming the user
if (pointersArray.length > 10) {
const confirmMessage = `This will open ${pointersArray.length} new tabs. This might overwhelm your browser. Continue?`;
if (!confirm(confirmMessage)) {
return;
}
}
// Navigate to the target class with containedIn filter
const filters = JSON.stringify([{
field: 'objectId',
constraint: 'containedIn',
compareTo: uniqueObjectIds
}]);

// Open all tabs immediately to maintain user activation context
let errorCount = 0;
const path = generatePath(
this.context,
`browser/${targetClassName}?filters=${encodeURIComponent(filters)}`,
true
);

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

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

showNote(message, isError) {
Expand Down
13 changes: 9 additions & 4 deletions src/lib/Filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,25 @@ export const Constraints = {
field: null,
comparable: false,
},
containedIn: {
name: 'contained in',
comparable: true,
},
};

export const FieldConstraints = {
Pointer: ['exists', 'dne', 'eq', 'neq', 'starts', 'unique'],
Boolean: ['exists', 'dne', 'eq', 'neq', 'unique'],
Number: ['exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'unique'],
String: ['exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString', 'unique'],
Pointer: ['exists', 'dne', 'eq', 'neq', 'starts', 'containedIn', 'unique'],
Boolean: ['exists', 'dne', 'eq', 'neq', 'containedIn', 'unique'],
Number: ['exists', 'dne', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'containedIn', 'unique'],
String: ['exists', 'dne', 'eq', 'neq', 'starts', 'ends', 'stringContainsString', 'containedIn', 'unique'],
Date: [
'exists',
'dne',
'before',
'onOrBefore',
'after',
'onOrAfter',
'containedIn',
'unique',
],
Object: [
Expand Down
2 changes: 1 addition & 1 deletion src/lib/queryFromFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function addConstraint(query, filter) {
query.notEqualTo(filter.get('field'), filter.get('compareTo'));
break;
case 'containedIn':
query.containedIn(filter.get('field'), filter.get('array'));
query.containedIn(filter.get('field'), filter.get('compareTo'));
break;
case 'stringContainsString':
query.matches(filter.get('field'), filter.get('compareTo'), 'i');
Expand Down
Loading