Geo Replicables List: Add ability to sort replicables in UI
Why are we doing this work
note: This may need to be implemented via multiple MRs.
The list view is currently going through a migration and may look different depending on if you have the feature flag geo_replicables_filtered_list_view and/or geo_replicables_show_view enabled.
We are working to enhance the functionality available in the Geo Replicables List View &16585 (closed)
We would like to support the ability to sort a list by set fields in the UI.
Sort Options
All sorts should be asc and desc
- Sort by Name (field
id) - Sort by Last Synced At (field
lastSyncedAt) - Sort by Last Verified At (field
lastVerifiedAt)
Relevant links
- API MR: Geo Replicables API: Add ability to sort to Geo... (#514998 - closed)
geo_replicable.vue- Example of a list view with sorting
Screenshots
Proposal Demo
Screen_Recording_2025-04-10_at_4.36.59_PM
Implementation plan
- Add the ability to manage the
sortin the vuex store (geo_replicable/store)
// state.js
sort: '',
// mutation_types.js
export const SET_SORT = 'SET_SORT';
// mutations.js
[types.SET_SORT](state, sort) {
state.sort = sort;
},
// actions.js
export const setSort = ({ commit }, sort) => {
commit(types.SET_SORT, sort);
};
- Add ability to add the
sortto the GraphQL filterreplicable_type_query_builder.js
// replicable_type_query_builder.js
// some of file omitted for clairty
export default (graphQlFieldName, verificationEnabled) => {
return gql`
query($first: Int, $last: Int, $before: String!, $after: String!, $replicationState: ReplicationStateEnum, $sort: GeoRegistrySort) {
geoNode {
${graphQlFieldName}(first: $first, last: $last, before: $before, after: $after, replicationState: $replicationState, sort: $sort) {
}
}
`
}
// actions.js
// some of file omitted for clarity
export const fetchReplicableItems = ({ state, dispatch }, direction) => {
// ...
const sort = state.sort ? state.sort.toUpperCase() : null;
client
.query({
query: buildReplicableTypeQuery(state.graphqlFieldName, state.verificationEnabled),
variables: { first, last, before, after, replicationState, sort },
})
}
- Add support for the
sortthrough options and defaults ingeo_replicable/constants.js
// some of file omitted for clarity
// constants.js
export const SORT_DIRECTION = {
ASC: 'asc',
DESC: 'desc',
};
export const SORT_OPTIONS = {
ID: {
text: __('Registry ID'),
value: 'id',
},
LAST_SYNCED_AT: {
text: s__('Geo|Last synced at'),
value: 'last_synced_at',
},
LAST_VERIFIED_AT: {
text: s__('Geo|Last verified at'),
value: 'verified_at',
},
};
export const DEFAULT_SORT = {
value: SORT_OPTIONS.ID.value,
direction: SORT_DIRECTION.ASC,
};
export const SORT_OPTIONS_ARRAY = Object.values(SORT_OPTIONS);
- Add support getting the sort object and sort query string in
geo_replicable/filters.js
// some of file omitted for clarity
// filters.js
export const getSortObj = (sort) => {
try {
const regex = new RegExp(`^(.*)_(${SORT_DIRECTION.ASC}|${SORT_DIRECTION.DESC})$`);
const match = sort.match(regex);
if (isValidFilter(match?.[1], SORT_OPTIONS_ARRAY)) {
return { value: match[1], direction: match[2] };
}
return DEFAULT_SORT
} catch {
return DEFAULT_SORT
}
}
export const getSortQueryString = ({ value, direction }) => {
return direction === SORT_DIRECTION.ASC ? `${value}_${SORT_DIRECTION.ASC}` : `${value}_${SORT_DIRECTION.DESC}`
}
- Add new
getSortFromQuerymethod to process query from URL inapp.vue
// some of file omitted for clarity
// app.vue
<script>
export default {
data() {
return {
activeSort: DEFAULT_SORT
};
},
created() {
if (this.glFeatures.geoReplicablesFilteredListView) {
this.getSortFromQuery();
}
},
methods: {
getSortFromQuery() {
const { sort } = queryToObject(window.location.search || '');
if (sort) {
this.activeSort = getSortObj(sort)
this.setSort(getSortQueryString(this.activeSort))
}
},
}
}
</script>
- Add new
<GlSorting>component with props togeo_replicable_filtered_search_bar.vue
// some of file omitted for clarity
// geo_replicable_filtered_search_bar.vue
<script>
export default {
props: {
activeSort: {
type: Object,
required: true,
}
},
computed: {
sortIsAscending() {
return this.activeSort.direction === SORT_DIRECTION.ASC;
}
},
methods: {
handleSortChange(value) {
this.$emit('sort', { value, ascending: this.sortIsAscending })
},
handleSortDirectionChange(ascending) {
this.$emit('sort', { value: this.activeSort.value, ascending })
},
}
}
</script>
<template>
<div class="gl-flex flex-grow-1 gl-grow gl-flex-col sm:gl-flex-row sm:gl-gap-3">
<geo-replicable-filtered-search
class="gl-mb-4 sm:gl-mb-0"
:active-filters="activeSearchFilters"
@search="handleSearch"
/>
<gl-sorting
class="gl-max-w-max"
:sort-by="activeSort.value"
:is-ascending="sortIsAscending"
:sort-options="$options.SORT_OPTIONS_ARRAY"
@sortDirectionChange="handleSortDirectionChange"
@sortByChange="handleSortChange"
/>
</div>
</template>
- Handle
@sortevent inapp.vue
// some of file omitted for clarity
// app.vue
methods: {
onSearch(filters) {
const { query, url } = processFilters(filters);
const sort = getSortQueryString(this.activeSort)
visitUrl(setUrlParams({ ...query, sort }, url.href, true));
},
onSort({ value, ascending }) {
const direction = ascending ? SORT_DIRECTION.ASC : SORT_DIRECTION.DESC
const sort = getSortQueryString({ value, direction })
visitUrl(setUrlParams({ sort }))
}
}
Edited by 🤖 GitLab Bot 🤖