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

  1. Sort by Name (field id)
  2. Sort by Last Synced At (field lastSyncedAt)
  3. Sort by Last Verified At (field lastVerifiedAt)

Relevant links

Screenshots

Proposal Demo

Screen_Recording_2025-04-10_at_4.36.59_PM

Implementation plan

  1. Add the ability to manage the sort in 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);
};
  1. Add ability to add the sort to the GraphQL filter replicable_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 },
    })
}
  1. Add support for the sort through options and defaults in geo_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);
  1. 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}`
}
  1. Add new getSortFromQuery method to process query from URL in app.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>
  1. Add new <GlSorting> component with props to geo_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>
  1. Handle @sort event in app.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 🤖