diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 03f75477d..98298dd7f 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -203,6 +203,8 @@ async function save() { } catch (err) { console.error(err); notify('errors.writeRequest', 'negative'); + hasChanges.value = false; + isLoading.value = false; } } diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue index d572b8be0..1d4ec6c1a 100644 --- a/src/components/VnTable/VnFilter.vue +++ b/src/components/VnTable/VnFilter.vue @@ -22,9 +22,13 @@ const $props = defineProps({ type: String, required: true, }, + searchUrl: { + type: String, + default: 'params', + }, }); const model = defineModel(); -const arrayData = useArrayData($props.dataKey); +const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); const columnFilter = computed(() => $props.column?.columnFilter); const updateEvent = { 'update:modelValue': addFilter }; @@ -99,6 +103,7 @@ const components = { }; async function addFilter(value) { + value ||= undefined; if (value && typeof value === 'object') value = model.value; value = value === '' ? undefined : value; let field = columnFilter.value?.name ?? $props.column.name; diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 7488f0db9..35fb7a395 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -1,7 +1,7 @@ <script setup> -import { ref, onMounted, computed, watch } from 'vue'; +import { ref, onMounted, computed, watch, useAttrs } from 'vue'; import { useI18n } from 'vue-i18n'; -import { useRouter } from 'vue-router'; +import { useRoute, useRouter } from 'vue-router'; import { useQuasar } from 'quasar'; import { useStateStore } from 'stores/useStateStore'; @@ -43,15 +43,26 @@ const $props = defineProps({ type: Object, default: null, }, + cardClass: { + type: String, + default: 'flex-one', + }, + searchUrl: { + type: String, + default: 'table', + }, }); +const attrs = useAttrs(); const { t } = useI18n(); const stateStore = useStateStore(); +const route = useRoute(); const router = useRouter(); const quasar = useQuasar(); const mode = ref('card'); const selected = ref([]); -const params = ref({}); +const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}'); +const params = ref({ ...routeQuery, ...routeQuery.filter?.where }); const VnPaginateRef = ref({}); const showForm = ref(false); const splittedColumns = ref({ columns: [] }); @@ -72,6 +83,7 @@ const tableModes = [ onMounted(() => { mode.value = quasar.platform.is.mobile ? 'card' : $props.defaultMode; stateStore.rightDrawer = true; + setUserParams(route.query[$props.searchUrl]); }); watch( @@ -80,6 +92,27 @@ watch( { immediate: true } ); +// try without +watch( + () => route.params.id, + () => reload(attrs) +); + +watch( + () => route.query[$props.searchUrl], + (val) => setUserParams(val) +); + +function setUserParams(watchedParams) { + if (!watchedParams) return; + + if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); + const where = JSON.parse(watchedParams?.filter)?.where; + watchedParams = { ...watchedParams, ...where }; + delete watchedParams.filter; + params.value = { ...params.value, ...watchedParams }; +} + function splitColumns(columns) { splittedColumns.value = { columns: [], @@ -126,12 +159,12 @@ function stopEventPropagation(event) { event.stopPropagation(); } -function reload() { - VnPaginateRef.value.fetch(); +function reload(params) { + VnPaginateRef.value.fetch(params); } function columnName(col) { - const column = Object.assign({}, col, col.columnFilter); + const column = { ...col, ...col.columnFilter }; let name = column.name; if (column.alias) name = column.alias + '.' + name; return name; @@ -156,6 +189,7 @@ defineExpose({ :search-button="true" v-model="params" :disable-submit-event="true" + :search-url="searchUrl" > <template #body> <VnTableFilter @@ -179,6 +213,7 @@ defineExpose({ class="q-px-md" :limit="20" ref="VnPaginateRef" + :search-url="searchUrl" :disable-infinite-scroll="mode == 'table'" > <template #body="{ rows }"> @@ -233,6 +268,7 @@ defineExpose({ :show-title="true" :data-key="$attrs['data-key']" v-model="params[columnName(col)]" + :search-url="searchUrl" /> </QTh> </template> @@ -300,7 +336,11 @@ defineExpose({ } " > - <QCardSection vertical class="no-margin no-padding w-80"> + <QCardSection + vertical + class="no-margin no-padding" + :class="colsMap.tableActions ? 'w-80' : 'fit'" + > <!-- Chips --> <QCardSection v-if="splittedColumns.chips.length" @@ -324,17 +364,21 @@ defineExpose({ :title="row[splittedColumns.title.name]" @click="stopEventPropagation($event)" class="cursor-text" - >{{ row[splittedColumns.title.name] }}</span > + {{ row[splittedColumns.title.name] }} + </span> </QCardSection> <!-- Fields --> - <QCardSection class="q-pl-sm q-pr-lg q-py-xs flex-one"> + <QCardSection + class="q-pl-sm q-pr-lg q-py-xs" + :class="$props.cardClass" + > <div v-for="col of splittedColumns.visible" :key="col.name" class="fields" > - <VnLv :label="`${col.label}:`"> + <VnLv :label="col.label && `${col.label}:`"> <template #value> <span @click="stopEventPropagation($event)" @@ -545,6 +589,16 @@ defineExpose({ white-space: nowrap; } +.grid-two { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, max-content)); + max-width: 100%; + margin: 0 auto; + overflow: scroll; + white-space: wrap; + width: 100%; +} + .w-80 { width: 80%; } diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 9591840f7..101f4aa18 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -1,6 +1,6 @@ <script setup> import { onBeforeMount, computed, watchEffect } from 'vue'; -import { useRoute, onBeforeRouteUpdate } from 'vue-router'; +import { useRoute } from 'vue-router'; import { useArrayData } from 'src/composables/useArrayData'; import { useStateStore } from 'stores/useStateStore'; import useCardSize from 'src/composables/useCardSize'; @@ -41,15 +41,6 @@ onBeforeMount(async () => { await arrayData.fetch({ append: false }); }); -if (props.baseUrl) { - onBeforeRouteUpdate(async (to, from) => { - if (to.params.id !== from.params.id) { - arrayData.store.url = `${props.baseUrl}/${to.params.id}`; - await arrayData.fetch({ append: false }); - } - }); -} - watchEffect(() => { if (Array.isArray(arrayData.store.data)) arrayData.store.data = arrayData.store.data[0]; diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 9e3e50eed..04ccca889 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -180,6 +180,7 @@ watch(modelValue, (newValue) => { > <template v-if="isClearable" #append> <QIcon + v-show="value" name="close" @click.stop="value = null" class="cursor-pointer" diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue index e28085331..23e932c34 100644 --- a/src/components/ui/VnFilterPanel.vue +++ b/src/components/ui/VnFilterPanel.vue @@ -4,12 +4,11 @@ import { useI18n } from 'vue-i18n'; import { useArrayData } from 'composables/useArrayData'; import { useRoute } from 'vue-router'; import toDate from 'filters/toDate'; -import useRedirect from 'src/composables/useRedirect'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; const { t } = useI18n(); -const params = defineModel(); -const props = defineProps({ +const params = defineModel({ default: {}, required: true, type: Object }); +const $props = defineProps({ dataKey: { type: String, required: true, @@ -36,7 +35,7 @@ const props = defineProps({ }, hiddenTags: { type: Array, - default: () => [], + default: () => ['filter'], }, customTags: { type: Array, @@ -46,82 +45,72 @@ const props = defineProps({ type: Boolean, default: false, }, + searchUrl: { + type: String, + default: 'params', + }, }); const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']); -const arrayData = useArrayData(props.dataKey, { - exprBuilder: props.exprBuilder, +const arrayData = useArrayData($props.dataKey, { + exprBuilder: $props.exprBuilder, + searchUrl: $props.searchUrl, + navigate: {}, }); const route = useRoute(); const store = arrayData.store; -const userParams = ref({}); -const { navigate } = useRedirect(); onMounted(() => { - if (params.value) userParams.value = JSON.parse(JSON.stringify(params.value)); - if (Object.keys(store.userParams).length > 0) { - userParams.value = JSON.parse(JSON.stringify(store.userParams)); - params.value = { - ...params.value, - ...userParams.value, - ...userParams.value?.filter?.where, - }; - } - emit('init', { params: userParams.value }); + emit('init', { params: params.value }); }); -function setUserParams(params) { - if (!params) { - userParams.value = {}; - } else { - userParams.value = typeof params == 'string' ? JSON.parse(params) : params; - } +function setUserParams(watchedParams) { + if (!watchedParams) return; + + if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); + watchedParams = { ...watchedParams, ...watchedParams.filter?.where }; + delete watchedParams.filter; + params.value = { ...params.value, ...watchedParams }; } watch( - () => route.query.params, - (val) => { - setUserParams(val); - } + () => route.query[$props.searchUrl], + (val) => setUserParams(val) ); watch( () => arrayData.store.userParams, - (val) => { - setUserParams(val); - } + (val) => setUserParams(val) ); const isLoading = ref(false); async function search(evt) { - if (evt && props.disableSubmitEvent) return; + if (evt && $props.disableSubmitEvent) return; store.filter.where = {}; isLoading.value = true; - const filter = { ...userParams.value, ...params.value }; + const filter = { ...params.value }; store.userParamsChanged = true; store.filter.skip = 0; store.skip = 0; - const { params: newParams } = await arrayData.addFilter({ params: filter }); - userParams.value = newParams; + const { params: newParams } = await arrayData.addFilter({ params: params.value }); + params.value = newParams; - if (!props.showAll && !Object.values(filter).length) store.data = []; + if (!$props.showAll && !Object.values(filter).length) store.data = []; isLoading.value = false; emit('search'); - navigate(store.data, {}); } async function reload() { isLoading.value = true; - const params = Object.values(userParams.value).filter((param) => param); + const params = Object.values(params.value).filter((param) => param); await arrayData.fetch({ append: false }); - if (!props.showAll && !params.length) store.data = []; + if (!$props.showAll && !params.length) store.data = []; isLoading.value = false; emit('refresh'); - navigate(store.data, {}); } async function clearFilters() { @@ -130,19 +119,19 @@ async function clearFilters() { store.filter.skip = 0; store.skip = 0; // Filtrar los params no removibles - const removableFilters = Object.keys(userParams.value).filter((param) => - props.unremovableParams.includes(param) + const removableFilters = Object.keys(params.value).filter((param) => + $props.unremovableParams.includes(param) ); const newParams = {}; // Conservar solo los params que no son removibles for (const key of removableFilters) { - newParams[key] = userParams.value[key]; + newParams[key] = params.value[key]; } params.value = {}; - userParams.value = { ...newParams }; // Actualizar los params con los removibles - await arrayData.applyFilter({ params: userParams.value }); + params.value = { ...newParams }; // Actualizar los params con los removibles + await arrayData.applyFilter({ params: params.value }); - if (!props.showAll) { + if (!$props.showAll) { store.data = []; } @@ -152,34 +141,26 @@ async function clearFilters() { const tagsList = computed(() => { const tagList = []; - const params = { - ...userParams.value, - }; - const where = params?.filter?.where; - if (where) { - Object.assign(params, where); - } - delete params.filter; - for (const key of Object.keys(params)) { - const value = params[key]; - if (!value || (props.hiddenTags || []).includes(key)) continue; - tagList.push({ key, value }); + for (const key of Object.keys(params.value)) { + const value = params.value[key]; + if (!value || ($props.hiddenTags || []).includes(key)) continue; + tagList.push({ label: key, value }); } return tagList; }); -const tags = computed(() => - tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.key)) -); +const tags = computed(() => { + return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key)); +}); const customTags = computed(() => - tagsList.value.filter((tag) => (props.customTags || []).includes(tag.key)) + tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key)) ); async function remove(key) { - delete userParams.value[key]; - delete userParams.value.filter.where[key]; + delete params.value[key]; + delete params.value.filter?.where?.[key]; params.value[key] = undefined; - await arrayData.applyFilter({ params: userParams.value }); + await arrayData.applyFilter({ params: params.value }); emit('remove', key); } @@ -244,13 +225,13 @@ function formatValue(value) { <div> <VnFilterPanelChip v-for="chip of tags" - :key="chip.key" - :removable="!unremovableParams.includes(chip.key)" - @remove="remove(chip.key)" + :key="chip.label" + :removable="!unremovableParams.includes(chip.label)" + @remove="remove(chip.label)" > <slot name="tags" :tag="chip" :format-fn="formatValue"> <div class="q-gutter-x-xs"> - <strong>{{ chip.key }}:</strong> + <strong>{{ chip.label }}:</strong> <span>"{{ chip.value }}"</span> </div> </slot> @@ -258,7 +239,7 @@ function formatValue(value) { <slot v-if="$slots.customTags" name="customTags" - :params="userParams" + :params="params" :tags="customTags" :format-fn="formatValue" :search-fn="search" @@ -268,9 +249,9 @@ function formatValue(value) { <QSeparator /> </QList> <QList dense class="list q-gutter-y-sm q-mt-sm"> - <slot name="body" :params="userParams" :search-fn="search"></slot> + <slot name="body" :params="params" :search-fn="search"></slot> </QList> - <template v-if="props.searchButton"> + <template v-if="$props.searchButton"> <QItem> <QItemSection class="q-py-sm"> <QBtn @@ -282,7 +263,6 @@ function formatValue(value) { rounded :type="disableSubmitEvent ? 'button' : 'submit'" unelevated - @click="search()" /> </QItemSection> </QItem> @@ -295,7 +275,6 @@ function formatValue(value) { color="primary" /> </template> - <style scoped lang="scss"> .list { width: 256px; diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue index d3000bc64..2b898f747 100644 --- a/src/components/ui/VnPaginate.vue +++ b/src/components/ui/VnPaginate.vue @@ -62,6 +62,10 @@ const props = defineProps({ type: Function, default: null, }, + searchUrl: { + type: String, + default: null, + }, disableInfiniteScroll: { type: Boolean, default: false, @@ -86,6 +90,7 @@ const arrayData = useArrayData(props.dataKey, { userParams: props.userParams, exprBuilder: props.exprBuilder, keepOpts: props.keepOpts, + searchUrl: props.searchUrl, }); const store = arrayData.store; @@ -104,7 +109,8 @@ const addFilter = async (filter, params) => { await arrayData.addFilter({ filter, params }); }; -async function fetch() { +async function fetch(params) { + useArrayData(props.dataKey, params); store.filter.skip = 0; store.skip = 0; await arrayData.fetch({ append: false }); diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index e5b2f02d2..f494f0269 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -3,7 +3,6 @@ import { onMounted, ref, watch } from 'vue'; import { useQuasar } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; import VnInput from 'src/components/common/VnInput.vue'; -import useRedirect from 'src/composables/useRedirect'; import { useI18n } from 'vue-i18n'; const quasar = useQuasar(); @@ -16,17 +15,14 @@ const props = defineProps({ }, label: { type: String, - required: false, default: 'Search', }, info: { type: String, - required: false, default: '', }, redirect: { type: Boolean, - required: false, default: true, }, url: { @@ -67,10 +63,20 @@ const props = defineProps({ }, }); -let arrayData = useArrayData(props.dataKey, { ...props }); -let store = arrayData.store; const searchText = ref(''); -const { navigate } = useRedirect(); +let arrayDataProps = { ...props }; +if (props.redirect) + arrayDataProps = { + ...props, + ...{ + navigate: { + customRouteRedirectName: props.customRouteRedirectName, + searchText: searchText.value, + }, + }, + }; +let arrayData = useArrayData(props.dataKey, arrayDataProps); +let store = arrayData.store; watch( () => props.dataKey, @@ -98,13 +104,6 @@ async function search() { search: searchText.value, }, }); - - if (!props.redirect) return; - - navigate(store.data, { - customRouteRedirectName: props.customRouteRedirectName, - searchText: searchText.value, - }); } </script> diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index 81a2bf01a..aabbfbc5b 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -23,8 +23,13 @@ export function useArrayData(key, userOptions) { store.skip = 0; const query = route.query; - if (query.params) { - store.userParams = JSON.parse(query.params); + const searchUrl = store.searchUrl; + if (query[searchUrl]) { + const params = JSON.parse(query[searchUrl]); + const filter = params?.filter; + delete params.filter; + store.userParams = { ...params, ...store.userParams }; + store.userFilter = { ...JSON.parse(filter), ...store.userFilter }; } }); @@ -41,13 +46,15 @@ export function useArrayData(key, userOptions) { 'userParams', 'userFilter', 'exprBuilder', + 'searchUrl', + 'navigate', ]; if (typeof userOptions === 'object') { for (const option in userOptions) { const isEmpty = userOptions[option] == null || userOptions[option] === ''; if (isEmpty || !allowedOptions.includes(option)) continue; - if (Object.prototype.hasOwnProperty.call(store, option)) { + if (Object.hasOwn(store, option)) { const defaultOpts = userOptions[option]; store[option] = userOptions.keepOpts?.includes(option) ? Object.assign(defaultOpts, store[option]) @@ -88,8 +95,8 @@ export function useArrayData(key, userOptions) { Object.assign(params, userParams); - store.isLoading = true; store.currentFilter = params; + store.isLoading = true; const response = await axios.get(store.url, { signal: canceller.signal, params, @@ -119,6 +126,10 @@ export function useArrayData(key, userOptions) { } } + function deleteOption(option) { + delete store[option]; + } + function cancelRequest() { if (canceller) { canceller.abort(); @@ -129,7 +140,7 @@ export function useArrayData(key, userOptions) { async function applyFilter({ filter, params }) { if (filter) store.userFilter = filter; store.filter = {}; - if (params) store.userParams = Object.assign({}, params); + if (params) store.userParams = { ...params }; const response = await fetch({ append: false }); return response; @@ -138,7 +149,7 @@ export function useArrayData(key, userOptions) { async function addFilter({ filter, params }) { if (filter) store.userFilter = Object.assign(store.userFilter, filter); - let userParams = Object.assign({}, store.userParams, params); + let userParams = { ...store.userParams, ...params }; userParams = sanitizerParams(userParams, store?.exprBuilder); store.userParams = userParams; @@ -163,9 +174,7 @@ export function useArrayData(key, userOptions) { delete store.userParams[param]; delete params[param]; if (store.filter?.where) { - const key = Object.keys( - exprBuilder && exprBuilder(param) ? exprBuilder(param) : param - ); + const key = Object.keys(exprBuilder ? exprBuilder(param) : param); if (key[0]) delete store.filter.where[key[0]]; if (Object.keys(store.filter.where).length === 0) { delete store.filter.where; @@ -190,22 +199,32 @@ export function useArrayData(key, userOptions) { } function updateStateParams() { - const query = {}; - if (store.order) query.order = store.order; - if (store.limit) query.limit = store.limit; - if (store.skip) query.skip = store.skip; - if (store.userParams && Object.keys(store.userParams).length !== 0) - query.params = store.userParams; - if (store.userFilter && Object.keys(store.userFilter).length !== 0) { - if (!query.params) query.params = {}; - query.params.filter = store.userFilter; - } - if (query.params) query.params = JSON.stringify(query.params); + const newUrl = { path: route.path, query: { ...(route.query ?? {}) } }; + newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter); - router.replace({ - path: route.path, - query, - }); + if (store.navigate) { + const { customRouteRedirectName, searchText } = store.navigate; + if (customRouteRedirectName) + return router.push({ + name: customRouteRedirectName, + params: { id: searchText }, + }); + const { matched: matches } = router.currentRoute.value; + const { path } = matches.at(-1); + + const to = + store?.data?.length === 1 + ? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`) + : path.replace(/:id.*/, ''); + + if (route.path != to) { + store.userParams = {}; + store.userFilter = {}; + return router.push({ path: to }); + } + } + + router.replace(newUrl); } const totalRows = computed(() => (store.data && store.data.length) || 0); @@ -223,5 +242,6 @@ export function useArrayData(key, userOptions) { totalRows, updateStateParams, isLoading, + deleteOption, }; } diff --git a/src/composables/useRedirect.js b/src/composables/useRedirect.js deleted file mode 100644 index c1470718b..000000000 --- a/src/composables/useRedirect.js +++ /dev/null @@ -1,25 +0,0 @@ -import { useRouter } from 'vue-router'; - -export default function useRedirect() { - const router = useRouter(); - - const navigate = (data, { customRouteRedirectName, searchText }) => { - if (customRouteRedirectName) - return router.push({ - name: customRouteRedirectName, - params: { id: searchText }, - }); - - const { matched: matches } = router.currentRoute.value; - const { path } = matches.at(-1); - - const to = - data.length === 1 - ? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`) - : path.replace(/:id.*/, ''); - - router.push({ path: to }); - }; - - return { navigate }; -} diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index 115c161dd..ebe32f8d0 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -21,6 +21,8 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { isLoading: false, userParamsChanged: false, exprBuilder: null, + searchUrl: 'params', + navigate: null, }; } diff --git a/test/vitest/__tests__/composables/useRedirect.spec.js b/test/vitest/__tests__/composables/useRedirect.spec.js deleted file mode 100644 index ce56189b9..000000000 --- a/test/vitest/__tests__/composables/useRedirect.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import { vi, describe, expect, it, beforeEach, beforeAll } from 'vitest'; -import useRedirect from 'src/composables/useRedirect'; -import { useRouter } from 'vue-router'; - -vi.mock('vue-router'); - -describe('useRedirect', () => { - useRouter.mockReturnValue({ - push: vi.fn(), - currentRoute: { - value: { - matched: [ - { path: '/' }, - { path: '/customer' }, - { path: '/customer/:id' }, - { path: '/customer/:id/basic-data' }, - ], - }, - }, - }); - const data = []; - let navigate; - let spy; - - beforeAll(() => { - const { navigate: navigateFn } = useRedirect(); - navigate = navigateFn; - spy = useRouter().push; - }); - - beforeEach(() => { - data.length = 0; - spy.mockReset(); - }); - - it('should redirect to list page if there are several results', async () => { - data.push({ id: 1, name: 'employee' }, { id: 2, name: 'boss' }); - navigate(data, {}); - expect(spy).toHaveBeenCalledWith({ path: '/customer/' }); - }); - - it('should redirect to list page if there is no results', async () => { - navigate(data, {}); - expect(spy).toHaveBeenCalledWith({ path: '/customer/' }); - }); - - it('should redirect to basic-data page if there is only one result', async () => { - data.push({ id: 1, name: 'employee' }); - navigate(data, {}); - expect(spy).toHaveBeenCalledWith({ path: '/customer/1/basic-data' }); - }); -});