diff --git a/src/components/BrowserFilter/BrowserFilter.react.js b/src/components/BrowserFilter/BrowserFilter.react.js index 9e2e7d7b7..ca6fdd337 100644 --- a/src/components/BrowserFilter/BrowserFilter.react.js +++ b/src/components/BrowserFilter/BrowserFilter.react.js @@ -47,10 +47,50 @@ export default class BrowserFilter extends React.Component { this.wrapRef = React.createRef(); } + getClassNameFromURL() { + const pathParts = window.location.pathname.split('/'); + const browserIndex = pathParts.indexOf('browser'); + return browserIndex >= 0 && pathParts[browserIndex + 1] + ? pathParts[browserIndex + 1] + : this.props.className; + } + + initializeEditFilterMode() { + const urlParams = new URLSearchParams(window.location.search); + const isEditFilterMode = urlParams.get('editFilter') === 'true'; + + if (isEditFilterMode && !this.state.open) { + const currentFilter = this.getCurrentFilterInfo(); + let filtersToDisplay = this.props.filters; + if (this.props.filters.size === 0) { + filtersToDisplay = this.loadFiltersFromURL(); + } + + const filters = this.convertDatesForDisplay(filtersToDisplay); + this.setState({ + open: true, + showMore: true, + filters: filters, + editMode: true, + name: currentFilter.name || '', + originalFilterName: currentFilter.name || '', + relativeDates: currentFilter.hasRelativeDates || false, + originalRelativeDates: currentFilter.hasRelativeDates || false, + originalFilters: filtersToDisplay, + }); + } + } + componentWillReceiveProps(props) { if (props.className !== this.props.className) { this.setState({ open: false }); } + + this.initializeEditFilterMode(); + } + + componentDidMount() { + this.initializeEditFilterMode(); } isCurrentFilterSaved() { @@ -58,10 +98,12 @@ export default class BrowserFilter extends React.Component { const urlParams = new URLSearchParams(window.location.search); const filterId = urlParams.get('filterId'); + const urlClassName = this.getClassNameFromURL(); + if (filterId) { const preferences = ClassPreferences.getPreferences( this.context.applicationId, - this.props.className + urlClassName ); if (preferences.filters) { @@ -77,22 +119,30 @@ export default class BrowserFilter extends React.Component { // Check for legacy filters (filters parameter without filterId) const filtersParam = urlParams.get('filters'); - if (filtersParam && this.props.filters.size > 0) { + if (filtersParam) { const preferences = ClassPreferences.getPreferences( this.context.applicationId, - this.props.className + urlClassName ); if (preferences.filters) { - // Normalize current filters for comparison (remove class property if it matches current className) - const currentFilters = this.props.filters.toJS().map(filter => { + // Parse the URL filters parameter to get the actual filter data + let urlFilters; + try { + urlFilters = JSON.parse(filtersParam); + } catch { + return false; + } + + // Normalize URL filters for comparison (remove class property if it matches current className) + const normalizedUrlFilters = urlFilters.map(filter => { const normalizedFilter = { ...filter }; - if (normalizedFilter.class === this.props.className) { + if (normalizedFilter.class === urlClassName) { delete normalizedFilter.class; } return normalizedFilter; }); - const currentFiltersString = JSON.stringify(currentFilters); + const urlFiltersString = JSON.stringify(normalizedUrlFilters); const matchingFilter = preferences.filters.find(savedFilter => { try { @@ -100,13 +150,13 @@ export default class BrowserFilter extends React.Component { // Normalize saved filters for comparison (remove class property if it matches current className) const normalizedSavedFilters = savedFilters.map(filter => { const normalizedFilter = { ...filter }; - if (normalizedFilter.class === this.props.className) { + if (normalizedFilter.class === urlClassName) { delete normalizedFilter.class; } return normalizedFilter; }); const savedFiltersString = JSON.stringify(normalizedSavedFilters); - return savedFiltersString === currentFiltersString; + return savedFiltersString === urlFiltersString; } catch { return false; } @@ -117,16 +167,20 @@ export default class BrowserFilter extends React.Component { } return false; - } getCurrentFilterInfo() { + } + + getCurrentFilterInfo() { // Extract filterId from URL if present const urlParams = new URLSearchParams(window.location.search); const filterId = urlParams.get('filterId'); const filtersParam = urlParams.get('filters'); + const urlClassName = this.getClassNameFromURL(); + if (filterId) { const preferences = ClassPreferences.getPreferences( this.context.applicationId, - this.props.className + urlClassName ); if (preferences.filters) { @@ -156,22 +210,37 @@ export default class BrowserFilter extends React.Component { } // Check for legacy filters (filters parameter without filterId) - if (filtersParam && this.props.filters.size > 0) { + if (filtersParam) { const preferences = ClassPreferences.getPreferences( this.context.applicationId, - this.props.className + urlClassName ); if (preferences.filters) { - // Normalize current filters for comparison (remove class property if it matches current className) - const currentFilters = this.props.filters.toJS().map(filter => { + // Parse the URL filters parameter to get the actual filter data + let urlFilters; + try { + urlFilters = JSON.parse(filtersParam); + } catch (error) { + console.warn('Failed to parse URL filters:', error); + return { + id: null, + name: '', + isApplied: false, + hasRelativeDates: false, + isLegacy: false + }; + } + + // Normalize URL filters for comparison (remove class property if it matches current className) + const normalizedUrlFilters = urlFilters.map(filter => { const normalizedFilter = { ...filter }; - if (normalizedFilter.class === this.props.className) { + if (normalizedFilter.class === urlClassName) { delete normalizedFilter.class; } return normalizedFilter; }); - const currentFiltersString = JSON.stringify(currentFilters); + const urlFiltersString = JSON.stringify(normalizedUrlFilters); const matchingFilter = preferences.filters.find(savedFilter => { try { @@ -179,13 +248,13 @@ export default class BrowserFilter extends React.Component { // Normalize saved filters for comparison (remove class property if it matches current className) const normalizedSavedFilters = savedFilters.map(filter => { const normalizedFilter = { ...filter }; - if (normalizedFilter.class === this.props.className) { + if (normalizedFilter.class === urlClassName) { delete normalizedFilter.class; } return normalizedFilter; }); const savedFiltersString = JSON.stringify(normalizedSavedFilters); - return savedFiltersString === currentFiltersString; + return savedFiltersString === urlFiltersString; } catch { return false; } @@ -224,6 +293,52 @@ export default class BrowserFilter extends React.Component { }; } + loadFiltersFromURL() { + const urlParams = new URLSearchParams(window.location.search); + const filtersParam = urlParams.get('filters'); + const filterId = urlParams.get('filterId'); + + const urlClassName = this.getClassNameFromURL(); + + // If we have a filterId, load from saved filters + if (filterId) { + const preferences = ClassPreferences.getPreferences( + this.context.applicationId, + urlClassName + ); + + if (preferences.filters) { + const savedFilter = preferences.filters.find(filter => filter.id === filterId); + if (savedFilter) { + try { + const filterData = JSON.parse(savedFilter.filter); + return new List(filterData.map(filter => { + const processedFilter = { ...filter, class: filter.class || urlClassName }; + return new ImmutableMap(processedFilter); + })); + } catch (error) { + console.warn('Failed to parse saved filter:', error); + } + } + } + } + + // If we have filters in URL but no filterId, parse them directly + if (filtersParam) { + try { + const queryFilters = JSON.parse(filtersParam); + return new List(queryFilters.map(filter => { + const processedFilter = { ...filter, class: filter.class || urlClassName }; + return new ImmutableMap(processedFilter); + })); + } catch (error) { + console.warn('Failed to parse URL filters:', error); + } + } + + return new List(); + } + toggleMore() { const currentFilter = this.getCurrentFilterInfo(); @@ -261,9 +376,11 @@ export default class BrowserFilter extends React.Component { } isFilterNameExists(name) { + const urlClassName = this.getClassNameFromURL(); + const preferences = ClassPreferences.getPreferences( this.context.applicationId, - this.props.className + urlClassName ); if (preferences.filters && name) { @@ -496,6 +613,17 @@ export default class BrowserFilter extends React.Component { // Convert only Parse Date objects to JavaScript Date objects, preserve RelativeDate objects filters = this.convertDatesForDisplay(filters); } + + // If closing the dialog and we're in edit filter mode, remove the editFilter parameter + const urlParams = new URLSearchParams(window.location.search); + const isEditFilterMode = urlParams.get('editFilter') === 'true'; + + if (this.state.open && isEditFilterMode) { + urlParams.delete('editFilter'); + const newUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`; + window.history.replaceState({}, '', newUrl); + } + this.setState(prevState => ({ open: !prevState.open, filters: filters, diff --git a/src/components/BrowserFilter/FilterRow.react.js b/src/components/BrowserFilter/FilterRow.react.js index eb10875eb..cf8645103 100644 --- a/src/components/BrowserFilter/FilterRow.react.js +++ b/src/components/BrowserFilter/FilterRow.react.js @@ -118,6 +118,12 @@ const FilterRow = ({ }) => { const setFocus = useCallback(input => { if (input !== null && editMode) { + // For DateTimeEntry components, don't auto-focus as it opens the calendar + // Check if the input has a focus method that opens a popover/calendar + if (input.focus && input.open) { + // This is likely a DateTimeEntry component, skip auto-focus + return; + } input.focus(); } }, []); diff --git a/src/components/CategoryList/CategoryList.react.js b/src/components/CategoryList/CategoryList.react.js index 2127901b6..aa3ef1ca3 100644 --- a/src/components/CategoryList/CategoryList.react.js +++ b/src/components/CategoryList/CategoryList.react.js @@ -140,9 +140,13 @@ export default class CategoryList extends React.Component { return (
- this.props.classClicked()}> - {count} - {c.name} + this.props.classClicked()} + > + {c.name} {c.onEdit && ( )} + {count} {(c.filters || []).length !== 0 && ( {name} + {this.props.onEditFilter && ( + { + e.preventDefault(); + this.props.onEditFilter(c.name, filterData); + }} + > + + + )}
); })} @@ -202,4 +218,5 @@ CategoryList.propTypes = { ), current: PropTypes.string.describe('Id of current category to be highlighted.'), linkPrefix: PropTypes.string.describe('Link prefix used to generate link path.'), + onEditFilter: PropTypes.func.describe('Callback function for editing a filter.'), }; diff --git a/src/components/CategoryList/CategoryList.scss b/src/components/CategoryList/CategoryList.scss index 64a5e77ef..6de579c3a 100644 --- a/src/components/CategoryList/CategoryList.scss +++ b/src/components/CategoryList/CategoryList.scss @@ -76,7 +76,7 @@ &:after { @include arrow('down', 10px, 7px, #8fb9cf); content: ''; - margin-left: 10px; + margin-left: 6px; } } .link { @@ -86,11 +86,22 @@ flex-grow: 1 } } + .count { + @include DosisFont; + color: #8fb9cf; + font-size: 12px; + margin-left: auto; + margin-right: 4px; + min-width: 20px; + text-align: right; + } .edit { display: flex; - align-items: center; - margin-right: 6px; + align-items: flex-start; + padding-top: 2px; cursor: pointer; + opacity: 0; + transition: opacity 0.2s ease; svg { fill: #8fb9cf; } @@ -100,6 +111,10 @@ } } } + + &:hover .edit { + opacity: 1; + } } .childLink { @@ -108,13 +123,34 @@ &:first-of-type { flex-grow: 1; display: flex; + margin-right: 6px; } span { text-align: left !important; margin-left: 14px; - display: flex; - flex-grow: 1; margin-right: 0px !important; + font-family: Dosis, "Helvetica Neue", Helvetica, Arial, sans-serif; + flex-grow: 1; } } + .editFilter { + display: flex; + align-items: flex-start; + padding-top: 2px; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s ease; + svg { + fill: #8fb9cf; + } + &:hover { + svg { + fill: white; + } + } + } + + &:hover .editFilter { + opacity: 1; + } } diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 685e72f56..829e4849e 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -228,6 +228,7 @@ class Browser extends DashboardView { this.showCreateClass = this.showCreateClass.bind(this); this.refresh = this.refresh.bind(this); this.deleteFilter = this.deleteFilter.bind(this); + this.editFilter = this.editFilter.bind(this); this.selectRow = this.selectRow.bind(this); this.updateRow = this.updateRow.bind(this); this.updateOrdering = this.updateOrdering.bind(this); @@ -488,6 +489,11 @@ class Browser extends DashboardView { const filters = this.extractFiltersFromQuery(props); const { className, entityId, relationName } = props.params; const isRelationRoute = entityId && relationName; + + // Check if we're in edit filter mode (don't load data) + const query = new URLSearchParams(props.location.search); + const isEditFilterMode = query.get('editFilter') === 'true'; + let relation = this.state.relation; if (isRelationRoute && !relation) { const parentObjectQuery = new Parse.Query(className); @@ -497,7 +503,7 @@ class Browser extends DashboardView { } this.setState( { - data: null, + data: isEditFilterMode ? [] : null, // Set empty array in edit mode to avoid loading newObject: null, lastMax: -1, ordering: ColumnPreferences.getColumnSort(false, context.applicationId, className), @@ -505,10 +511,13 @@ class Browser extends DashboardView { relation: isRelationRoute ? relation : null, }, () => { - if (isRelationRoute) { - this.fetchRelation(relation, filters); - } else if (className) { - this.fetchData(className, filters); + // Only fetch data if not in edit filter mode + if (!isEditFilterMode) { + if (isRelationRoute) { + this.fetchRelation(relation, filters); + } else if (className) { + this.fetchData(className, filters); + } } } ); @@ -1373,6 +1382,26 @@ class Browser extends DashboardView { super.forceUpdate(); } + editFilter(className, filterData) { + // Navigate to the class with the filter loaded for editing + const { id, filter } = filterData; + + // Build URL with filter parameters for editing + const urlParams = new URLSearchParams(); + urlParams.set('filters', filter); + if (id) { + urlParams.set('filterId', id); + } + + // Add edit mode parameter to indicate we want to edit without loading data + urlParams.set('editFilter', 'true'); + + const url = `browser/${className}?${urlParams.toString()}`; + + // Navigate to the URL which will trigger the filter dialog to open in edit mode + this.props.navigate(generatePath(this.context, url)); + } + updateOrdering(ordering) { const source = this.state.relation || this.props.params.className; this.setState( @@ -2201,6 +2230,7 @@ class Browser extends DashboardView { classClicked={() => { this.resetPage(); }} + onEditFilter={this.editFilter} categories={allCategories} /> );