1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 23:50:12 +00:00

Merge branch '3028-prevent-the-recordselector-from-paging-if-the-api-s-dispatch-returns-a-404' into 'develop'

Resolve "Prevent the RecordSelector from paging if the API's dispatch returns a 404"

Closes 

See merge request 
This commit is contained in:
Afonso Silva 2024-10-07 14:53:20 +00:00
commit d383884c32
8 changed files with 789 additions and 4 deletions
changelog/entries/unreleased/bug
web-frontend
modules/builder
test
helpers
unit/builder/components/elements/components

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "[Builder] Prevent excessive API requests in collection elements",
"issue_number": 3028,
"bullet_points": [],
"created_at": "2024-10-03"
}

View file

@ -309,7 +309,7 @@ export default {
canFetch() {
// We want to fetch data only if the dropdown have been opened at least once.
// It's not necessary otherwise
return this.openedOnce
return this.openedOnce && this.contentFetchEnabled
},
getErrorMessage() {
return this.displayFormDataError ? this.$t('error.requiredField') : ''

View file

@ -98,7 +98,7 @@
<ABButton
v-if="hasMorePage && children.length > 0"
:style="getStyleOverride('button')"
:disabled="contentLoading"
:disabled="contentLoading || !contentFetchEnabled"
:loading="contentLoading"
@click="loadMore()"
>

View file

@ -45,7 +45,7 @@
<ABButton
v-if="hasMorePage"
:style="getStyleOverride('button')"
:disabled="contentLoading"
:disabled="contentLoading || !contentFetchEnabled"
:loading="contentLoading"
@click="loadMore()"
>

View file

@ -9,6 +9,7 @@ export default {
currentOffset: 0,
errorNotified: false,
resetTimeout: null,
contentFetchEnabled: true,
}
},
computed: {
@ -120,6 +121,9 @@ export default {
})
this.currentOffset += this.element.items_per_page
} catch (error) {
// Handle the HTTP error if needed
this.onContentFetchError(error)
// We need to only launch one toast error message per element,
// not one per element fetch, or we can end up with many error
// toasts per element sharing a datasource.
@ -137,7 +141,17 @@ export default {
},
/** Overrides this if you want to prevent data fetching */
canFetch() {
return true
return this.contentFetchEnabled
},
/** Override this if you want to handle content fetch errors */
onContentFetchError(error) {
// If the request failed without reaching the server, `error.response`
// will be `undefined`, so we need to check that before checking the
// HTTP status code
if (error.response && [400, 404].includes(error.response.status)) {
this.contentFetchEnabled = false
}
},
},
}

View file

@ -7,6 +7,7 @@ import setupClient, {
import setupDatabasePlugin from '@baserow/modules/database/plugin'
import setupBuilderPlugin from '@baserow/modules/builder/plugin'
import setupIntegrationPlugin from '@baserow/modules/integrations/plugin'
import { bootstrapVueContext } from '@baserow/test/helpers/components'
import MockAdapter from 'axios-mock-adapter'
import _ from 'lodash'
@ -40,6 +41,7 @@ function _createBaserowStoreAndRegistry(app, vueContext, extraPluginSetupFunc) {
app,
}
setupDatabasePlugin(setupContext)
setupIntegrationPlugin(setupContext)
setupBuilderPlugin(setupContext)
setupHasFeaturePlugin(setupContext, (name, dep) => {
app[`$${name}`] = dep

View file

@ -0,0 +1,119 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import RecordSelectorElement from '@baserow/modules/builder/components/elements/components/RecordSelectorElement.vue'
import flushPromises from 'flush-promises'
// Ignore `notifyIf` and `notifyIf404` function calls
jest.mock('@baserow/modules/core/utils/error.js')
describe('RecordSelectorElement', () => {
let testApp = null
let store = null
let mockServer = null
beforeAll(() => {
// NOTE: TestApp wraps any exception raised by the axios mock adapter and
// re-raises it as a Jest error.
// This mutates the error object and make some properties not available.
// In this case `collectionElement` mixin needs to access the response
// object when the server returns a 400/404 error, so we disable
// `failTestOnErrorResponse`.
testApp = new TestApp()
testApp.failTestOnErrorResponse = false
store = testApp.store
mockServer = testApp.mockServer
})
afterEach(() => {
testApp.afterEach()
})
const mountComponent = ({ props = {}, slots = {}, provide = {} }) => {
return testApp.mount(RecordSelectorElement, {
propsData: props,
slots,
provide,
})
}
test('does not paginate if API returns 400/404', async () => {
const builder = { id: 1, theme: { primary_color: '#ccc' } }
const page = {
id: 1,
dataSources: [{ id: 1, type: 'local_baserow_list_rows', table_id: 1 }],
elements: [],
}
const workspace = {}
const mode = 'public'
const element = {
id: 1,
type: 'record_selector',
data_source_id: page.dataSources[0].id,
items_Per_page: 5,
}
store.dispatch('element/forceCreate', { page, element })
const wrapper = await mountComponent({
props: {
element,
},
provide: {
builder,
page,
mode,
applicationContext: { builder, page, mode },
element,
workspace,
},
})
// A mock server that mimics the data source dispatch endpoints.
// The first time it is called it returns a successful message, but the
// second time it returns a 400 response.
const url = `builder/domains/published/data-source/${page.dataSources[0].id}/dispatch/`
mockServer.mock
.onPost(url)
.replyOnce(200, {
results: [
{ id: 1, order: 1, name: 'First' },
{ id: 2, order: 1, name: 'Second' },
{ id: 3, order: 1, name: 'Third' },
{ id: 4, order: 1, name: 'Fourth' },
{ id: 5, order: 1, name: 'Fifth' },
],
has_next_page: true,
})
.onPost(url)
.reply(400, { message: 'Bad Request' })
// The first time we trigger a next page, the server responds with 200
// therefore we should be able to fetch more content
await wrapper
.findAllComponents({ name: 'ABDropdown' })
.at(0)
.find('.ab-dropdown__selected')
.trigger('click')
await flushPromises()
expect(wrapper.element).toMatchSnapshot()
expect(mockServer.mock.history.post.length).toBe(1)
// Then we trigger a few scroll events in the record selector element and
// confirm that the API is only called once
await wrapper
.findAllComponents({ name: 'ABDropdown' })
.at(0)
.find('.select__items')
.trigger('scroll')
await flushPromises()
expect(wrapper.element).toMatchSnapshot()
expect(mockServer.mock.history.post.length).toBe(2)
await wrapper
.findAllComponents({ name: 'ABDropdown' })
.at(0)
.find('.select__items')
.trigger('scroll')
await flushPromises()
expect(wrapper.element).toMatchSnapshot()
expect(mockServer.mock.history.post.length).toBe(2)
})
})

View file

@ -0,0 +1,643 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecordSelectorElement does not paginate if API returns 400/404 1`] = `
<div
class="control ab-form-group"
>
<!---->
<!---->
<div
class="control__wrapper"
>
<div
class="control__elements"
>
<div
class="flex-grow-1"
>
<!---->
<div
class="ab-form-group__children"
>
<div
class="ab-dropdown choice-element"
tabindex="0"
>
<a
class="ab-dropdown__selected"
>
<!---->
<span
class="ab-dropdown__selected-placeholder"
>
action.makeChoice
</span>
<i
class="ab-dropdown__toggle-icon iconoir-nav-arrow-down"
/>
</a>
<div
class="ab-dropdown__items"
>
<!---->
<ul
class="select__items"
tabindex="-1"
>
<section
class="infinite-scroll"
>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="First"
>
First
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Second"
>
Second
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Third"
>
Third
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fourth"
>
Fourth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fifth"
>
Fifth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<div
class="infinite-scroll__loading-wrapper"
style=""
>
<!---->
</div>
<!---->
</section>
</ul>
<!---->
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
`;
exports[`RecordSelectorElement does not paginate if API returns 400/404 2`] = `
<div
class="control ab-form-group"
>
<!---->
<!---->
<div
class="control__wrapper"
>
<div
class="control__elements"
>
<div
class="flex-grow-1"
>
<!---->
<div
class="ab-form-group__children"
>
<div
class="ab-dropdown choice-element"
tabindex="0"
>
<a
class="ab-dropdown__selected"
>
<!---->
<span
class="ab-dropdown__selected-placeholder"
>
action.makeChoice
</span>
<i
class="ab-dropdown__toggle-icon iconoir-nav-arrow-down"
/>
</a>
<div
class="ab-dropdown__items"
>
<!---->
<ul
class="select__items"
tabindex="-1"
>
<section
class="infinite-scroll"
>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="First"
>
First
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Second"
>
Second
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Third"
>
Third
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fourth"
>
Fourth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fifth"
>
Fifth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<div
class="infinite-scroll__loading-wrapper"
style=""
>
<!---->
</div>
<!---->
</section>
</ul>
<!---->
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
`;
exports[`RecordSelectorElement does not paginate if API returns 400/404 3`] = `
<div
class="control ab-form-group"
>
<!---->
<!---->
<div
class="control__wrapper"
>
<div
class="control__elements"
>
<div
class="flex-grow-1"
>
<!---->
<div
class="ab-form-group__children"
>
<div
class="ab-dropdown choice-element"
tabindex="0"
>
<a
class="ab-dropdown__selected"
>
<!---->
<span
class="ab-dropdown__selected-placeholder"
>
action.makeChoice
</span>
<i
class="ab-dropdown__toggle-icon iconoir-nav-arrow-down"
/>
</a>
<div
class="ab-dropdown__items"
>
<!---->
<ul
class="select__items"
tabindex="-1"
>
<section
class="infinite-scroll"
>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="First"
>
First
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Second"
>
Second
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Third"
>
Third
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fourth"
>
Fourth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<li
class="ab-dropdownitem__item ab-dropdownitem__item--no-options"
>
<a
class="ab-dropdownitem__item-link"
>
<div
class="ab-dropdownitem__item-name"
>
<!---->
<span
class="ab-dropdownitem__item-name-text"
title="Fifth"
>
Fifth
</span>
</div>
<!---->
</a>
<i
class="ab-dropdownitem__item-active-icon iconoir-check"
/>
</li>
<div
class="infinite-scroll__loading-wrapper"
style=""
>
<!---->
</div>
<!---->
</section>
</ul>
<!---->
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
`;