mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-15 09:34:13 +00:00
Resolve "Make it possible to search in the gallery view"
This commit is contained in:
parent
cb766fa074
commit
b96371a7ad
28 changed files with 647 additions and 88 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/api/views/gallery
premium/web-frontend/modules/baserow_premium
web-frontend
|
@ -120,8 +120,11 @@ class GalleryViewView(APIView):
|
|||
view.table.database.group.has_user(
|
||||
request.user, raise_error=True, allow_if_template=True
|
||||
)
|
||||
|
||||
search = request.GET.get("search")
|
||||
|
||||
model = view.table.get_model()
|
||||
queryset = view_handler.get_queryset(view, None, model)
|
||||
queryset = view_handler.get_queryset(view, search, model)
|
||||
|
||||
if "count" in request.GET:
|
||||
return Response({"count": queryset.count()})
|
||||
|
|
|
@ -240,7 +240,7 @@ class GridView(View):
|
|||
class GridViewFieldOptions(ParentFieldTrashableModelMixin, models.Model):
|
||||
grid_view = models.ForeignKey(GridView, on_delete=models.CASCADE)
|
||||
field = models.ForeignKey(Field, on_delete=models.CASCADE)
|
||||
# The defaults should be the same as in the `fieldCreated` of the `GridViewType`
|
||||
# The defaults should match the ones in `afterFieldCreated` of the `GridViewType`
|
||||
# abstraction in the web-frontend.
|
||||
width = models.PositiveIntegerField(
|
||||
default=200,
|
||||
|
|
|
@ -150,6 +150,43 @@ def test_list_rows_include_field_options(api_client, data_fixture):
|
|||
assert "filters_disabled" not in response_json
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_rows_search(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
email="test@test.nl", password="password", first_name="Test1"
|
||||
)
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
text_field = data_fixture.create_text_field(
|
||||
table=table, order=0, name="Name", text_default=""
|
||||
)
|
||||
gallery = data_fixture.create_gallery_view(table=table)
|
||||
search_term = "Smith"
|
||||
model = gallery.table.get_model()
|
||||
not_matching_row1 = model.objects.create(
|
||||
**{f"field_{text_field.id}": "Mark Spencer"}
|
||||
)
|
||||
matching_row1 = model.objects.create(
|
||||
**{f"field_{text_field.id}": f"Elon {search_term}"}
|
||||
)
|
||||
matching_row2 = model.objects.create(
|
||||
**{f"field_{text_field.id}": f"James {search_term}"}
|
||||
)
|
||||
not_matching_row2 = model.objects.create(
|
||||
**{f"field_{text_field.id}": "Robin Backham"}
|
||||
)
|
||||
|
||||
url = reverse("api:database:views:gallery:list", kwargs={"view_id": gallery.id})
|
||||
response = api_client.get(
|
||||
url, {"search": search_term}, **{"HTTP_AUTHORIZATION": f"JWT {token}"}
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["count"] == 2
|
||||
assert response_json["results"][0]["id"] == matching_row1.id
|
||||
assert response_json["results"][1]["id"] == matching_row2.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_gallery_view_field_options(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
* Fix missing translation when importing empty CSV
|
||||
* Fixed OpenAPI spec. The specification is now valid and can be used for imports to other
|
||||
tools, e.g. to various REST clients.
|
||||
* Added search to gallery views.
|
||||
* Views supporting search are properly updated when a column with a matching default value is added.
|
||||
|
||||
## Released (2022-01-13 1.8.2)
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
@update="updateValue"
|
||||
@field-updated="$emit('refresh', $event)"
|
||||
@field-deleted="$emit('refresh')"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="fieldCreated"
|
||||
></RowEditModal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -93,6 +93,7 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
|
||||
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
|
||||
import RowCreateModal from '@baserow/modules/database/components/row/RowCreateModal'
|
||||
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
|
||||
|
@ -110,7 +111,7 @@ export default {
|
|||
KanbanViewStackedBy,
|
||||
KanbanViewStack,
|
||||
},
|
||||
mixins: [kanbanViewHelper],
|
||||
mixins: [viewHelpers, kanbanViewHelper],
|
||||
props: {
|
||||
database: {
|
||||
type: Object,
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
ref="createFieldContext"
|
||||
:table="table"
|
||||
:forced-type="forcedFieldType"
|
||||
@field-created="$event.callback()"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -159,7 +159,13 @@ export class KanbanViewType extends PremiumViewType {
|
|||
}
|
||||
}
|
||||
|
||||
async fieldCreated({ dispatch }, table, field, fieldType, storePrefix = '') {
|
||||
async afterFieldCreated(
|
||||
{ dispatch },
|
||||
table,
|
||||
field,
|
||||
fieldType,
|
||||
storePrefix = ''
|
||||
) {
|
||||
await dispatch(
|
||||
storePrefix + 'view/kanban/setFieldOptionsOfField',
|
||||
{
|
||||
|
@ -175,7 +181,7 @@ export class KanbanViewType extends PremiumViewType {
|
|||
)
|
||||
}
|
||||
|
||||
fieldUpdated(context, field, oldField, fieldType, storePrefix) {
|
||||
afterFieldUpdated(context, field, oldField, fieldType, storePrefix) {
|
||||
// Make sure that all Kanban views don't depend on fields that
|
||||
// have been converted to another type
|
||||
const type = SingleSelectFieldType.getType()
|
||||
|
@ -185,7 +191,7 @@ export class KanbanViewType extends PremiumViewType {
|
|||
}
|
||||
}
|
||||
|
||||
fieldDeleted(context, field, fieldType, storePrefix = '') {
|
||||
afterFieldDeleted(context, field, fieldType, storePrefix = '') {
|
||||
// Make sure that all Kanban views don't depend on fields that
|
||||
// have been deleted
|
||||
this._setFieldToNull(context, field, 'single_select_field')
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
delete values.type
|
||||
|
||||
try {
|
||||
const { forceCreateCallback, refreshNeeded } =
|
||||
const { forceCreateCallback, fetchNeeded, newField } =
|
||||
await this.$store.dispatch('field/create', {
|
||||
type,
|
||||
values,
|
||||
|
@ -66,11 +66,7 @@ export default {
|
|||
this.$refs.form.reset()
|
||||
this.hide()
|
||||
}
|
||||
if (refreshNeeded) {
|
||||
this.$emit('refresh', { callback })
|
||||
} else {
|
||||
await callback()
|
||||
}
|
||||
this.$emit('field-created', { callback, newField, fetchNeeded })
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
const handledByForm = this.$refs.form.handleErrorByForm(error)
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<CreateFieldContext
|
||||
ref="createFieldContext"
|
||||
:table="table"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="$emit('field-created', $event)"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -310,13 +310,18 @@ export default {
|
|||
const includeFieldOptions =
|
||||
typeof event === 'object' ? event.includeFieldOptions : false
|
||||
|
||||
const fieldsToRefresh =
|
||||
typeof event === 'object' && event.newField
|
||||
? [...this.fields, event.newField]
|
||||
: this.fields
|
||||
|
||||
this.viewLoading = true
|
||||
const type = this.$registry.get('view', this.view.type)
|
||||
try {
|
||||
await type.refresh(
|
||||
{ store: this.$store },
|
||||
this.view,
|
||||
this.fields,
|
||||
fieldsToRefresh,
|
||||
this.primary,
|
||||
this.storePrefix,
|
||||
includeFieldOptions
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
:fields="fields"
|
||||
:primary="primary"
|
||||
:store-prefix="storePrefix"
|
||||
:always-hide-rows-not-matching-search="alwaysHideRowsNotMatchingSearch"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@search-changed="searchChanged"
|
||||
></ViewSearchContext>
|
||||
|
@ -46,6 +47,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
alwaysHideRowsNotMatchingSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
|
|
|
@ -22,7 +22,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control control--align-right margin-bottom-0">
|
||||
<div
|
||||
v-if="!alwaysHideRowsNotMatchingSearch"
|
||||
class="control control--align-right margin-bottom-0"
|
||||
>
|
||||
<SwitchInput
|
||||
v-model="hideRowsNotMatchingSearch"
|
||||
@input="searchIfChanged"
|
||||
|
@ -59,6 +62,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
alwaysHideRowsNotMatchingSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -103,15 +111,18 @@ export default {
|
|||
}
|
||||
},
|
||||
debouncedServerSearchRefresh: debounce(async function () {
|
||||
await this.$store.dispatch(this.storePrefix + 'view/grid/updateSearch', {
|
||||
activeSearchTerm: this.activeSearchTerm,
|
||||
hideRowsNotMatchingSearch: this.hideRowsNotMatchingSearch,
|
||||
// The refresh event we fire below will cause the table to refresh it state from
|
||||
// the server using the newly set search terms.
|
||||
refreshMatchesOnClient: false,
|
||||
fields: this.fields,
|
||||
primary: this.primary,
|
||||
})
|
||||
await this.$store.dispatch(
|
||||
`${this.storePrefix}view/${this.view.type}/updateSearch`,
|
||||
{
|
||||
activeSearchTerm: this.activeSearchTerm,
|
||||
hideRowsNotMatchingSearch: this.hideRowsNotMatchingSearch,
|
||||
// The refresh event we fire below will cause the table to refresh it state from
|
||||
// the server using the newly set search terms.
|
||||
refreshMatchesOnClient: false,
|
||||
fields: this.fields,
|
||||
primary: this.primary,
|
||||
}
|
||||
)
|
||||
this.$emit('refresh', {
|
||||
callback: this.finishedLoading,
|
||||
})
|
||||
|
@ -119,13 +130,16 @@ export default {
|
|||
// Debounce even the client side only refreshes as otherwise spamming the keyboard
|
||||
// can cause many refreshes to queue up quickly bogging down the UI.
|
||||
debouncedClientSideSearchRefresh: debounce(async function () {
|
||||
await this.$store.dispatch(this.storePrefix + 'view/grid/updateSearch', {
|
||||
activeSearchTerm: this.activeSearchTerm,
|
||||
hideRowsNotMatchingSearch: this.hideRowsNotMatchingSearch,
|
||||
refreshMatchesOnClient: true,
|
||||
fields: this.fields,
|
||||
primary: this.primary,
|
||||
})
|
||||
await this.$store.dispatch(
|
||||
`${this.storePrefix}view/${this.view.type}/updateSearch`,
|
||||
{
|
||||
activeSearchTerm: this.activeSearchTerm,
|
||||
hideRowsNotMatchingSearch: this.hideRowsNotMatchingSearch,
|
||||
refreshMatchesOnClient: true,
|
||||
fields: this.fields,
|
||||
primary: this.primary,
|
||||
}
|
||||
)
|
||||
this.finishedLoading()
|
||||
}, 10),
|
||||
finishedLoading() {
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<CreateFieldContext
|
||||
ref="createFieldContext"
|
||||
:table="table"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="$event.callback()"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
@update="updateValue"
|
||||
@field-updated="$emit('refresh', $event)"
|
||||
@field-deleted="$emit('refresh')"
|
||||
@field-created="fieldCreated"
|
||||
></RowEditModal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -91,11 +92,12 @@ import RowCard from '@baserow/modules/database/components/card/RowCard'
|
|||
import RowCreateModal from '@baserow/modules/database/components/row/RowCreateModal'
|
||||
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
|
||||
import bufferedRowsDragAndDrop from '@baserow/modules/database/mixins/bufferedRowsDragAndDrop'
|
||||
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
|
||||
|
||||
export default {
|
||||
name: 'GalleryView',
|
||||
components: { RowCard, RowCreateModal, RowEditModal },
|
||||
mixins: [bufferedRowsDragAndDrop],
|
||||
mixins: [viewHelpers, bufferedRowsDragAndDrop],
|
||||
props: {
|
||||
primary: {
|
||||
type: Object,
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
@update-cover-image-field="updateCoverImageField"
|
||||
></ViewFieldsContext>
|
||||
</li>
|
||||
<li class="header__filter-item header__filter-item--right">
|
||||
<ViewSearch
|
||||
:view="view"
|
||||
:fields="fields"
|
||||
:primary="primary"
|
||||
:store-prefix="storePrefix"
|
||||
:always-hide-rows-not-matching-search="true"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
></ViewSearch>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
@ -39,10 +49,11 @@ import { mapGetters, mapState } from 'vuex'
|
|||
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import ViewFieldsContext from '@baserow/modules/database/components/view/ViewFieldsContext'
|
||||
import ViewSearch from '@baserow/modules/database/components/view/ViewSearch'
|
||||
|
||||
export default {
|
||||
name: 'GalleryViewHeader',
|
||||
components: { ViewFieldsContext },
|
||||
components: { ViewFieldsContext, ViewSearch },
|
||||
props: {
|
||||
database: {
|
||||
type: Object,
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
:store-prefix="storePrefix"
|
||||
:style="{ width: leftWidth + 'px' }"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="fieldCreated"
|
||||
@row-hover="setRowHover($event.row, $event.value)"
|
||||
@row-context="showRowContext($event.event, $event.row)"
|
||||
@row-dragging="rowDragStart"
|
||||
|
@ -65,6 +66,7 @@
|
|||
:store-prefix="storePrefix"
|
||||
:style="{ left: leftWidth + 'px' }"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="fieldCreated"
|
||||
@row-hover="setRowHover($event.row, $event.value)"
|
||||
@row-context="showRowContext($event.event, $event.row)"
|
||||
@add-row="addRow()"
|
||||
|
@ -144,11 +146,11 @@
|
|||
:fields="fields"
|
||||
:rows="allRows"
|
||||
:read-only="readOnly"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@update="updateValue"
|
||||
@hidden="rowEditModalHidden"
|
||||
@field-updated="$emit('refresh', $event)"
|
||||
@field-deleted="$emit('refresh')"
|
||||
@field-created="fieldCreated"
|
||||
></RowEditModal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -163,6 +165,7 @@ import GridViewRowDragging from '@baserow/modules/database/components/view/grid/
|
|||
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
|
||||
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
||||
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
|
||||
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
|
||||
|
||||
export default {
|
||||
name: 'GridView',
|
||||
|
@ -172,7 +175,7 @@ export default {
|
|||
GridViewRowDragging,
|
||||
RowEditModal,
|
||||
},
|
||||
mixins: [gridViewHelpers],
|
||||
mixins: [viewHelpers, gridViewHelpers],
|
||||
props: {
|
||||
primary: {
|
||||
type: Object,
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<CreateFieldContext
|
||||
ref="createFieldContext"
|
||||
:table="table"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@field-created="$emit('field-created', $event)"
|
||||
></CreateFieldContext>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
:include-add-field="includeAddField"
|
||||
:read-only="readOnly"
|
||||
:store-prefix="storePrefix"
|
||||
@field-created="$emit('field-created', $event)"
|
||||
@refresh="$emit('refresh', $event)"
|
||||
@dragging="
|
||||
canOrderFields &&
|
||||
|
|
|
@ -466,11 +466,11 @@ export class FieldType extends Registerable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Determines whether a view refresh should be executed after the specific field
|
||||
* has been added to a table. This is for example needed when a value depends on
|
||||
* the backend and can't be guessed or calculated by the web-frontend.
|
||||
* Determines whether row data of the field should be fetched again after the
|
||||
* field has been created. This is for example needed when a value depends on the
|
||||
* backend and can't be guessed or calculated by the web-frontend.
|
||||
*/
|
||||
shouldRefreshWhenAdded() {
|
||||
shouldFetchDataWhenAdded() {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -1268,7 +1268,7 @@ export class CreatedOnLastModifiedBaseFieldType extends BaseDateFieldType {
|
|||
return moment().utc().format()
|
||||
}
|
||||
|
||||
shouldRefreshWhenAdded() {
|
||||
shouldFetchDataWhenAdded() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2118,7 +2118,7 @@ export class FormulaFieldType extends FieldType {
|
|||
return true
|
||||
}
|
||||
|
||||
shouldRefreshWhenAdded() {
|
||||
shouldFetchDataWhenAdded() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
26
web-frontend/modules/database/mixins/viewHelpers.js
Normal file
26
web-frontend/modules/database/mixins/viewHelpers.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
export default {
|
||||
methods: {
|
||||
/**
|
||||
* Must be called when a new field is created. It emits the refresh event when
|
||||
* needed. It expects the event parameter propagated from the
|
||||
* `CreateFieldContext` component.
|
||||
*/
|
||||
fieldCreated({ fetchNeeded, ...context }) {
|
||||
const viewType = this.$registry.get('view', this.view.type)
|
||||
|
||||
if (
|
||||
fetchNeeded ||
|
||||
viewType.shouldRefreshWhenFieldCreated(
|
||||
this.$registry,
|
||||
this.$store,
|
||||
context.newField,
|
||||
this.storePrefix
|
||||
)
|
||||
) {
|
||||
this.$emit('refresh', context)
|
||||
} else if (context.callback) {
|
||||
context.callback()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
import { anyFieldsNeedRefresh } from '@baserow/modules/database/store/field'
|
||||
import { anyFieldsNeedFetch } from '@baserow/modules/database/store/field'
|
||||
|
||||
/**
|
||||
* Registers the real time events related to the database module. When a message comes
|
||||
|
@ -58,17 +58,30 @@ export const registerRealtimeEvents = (realtime) => {
|
|||
relatedFields,
|
||||
})
|
||||
}
|
||||
const view = store.getters['view/getSelected']
|
||||
const viewMustBeRefreshed =
|
||||
view &&
|
||||
app.$registry
|
||||
.get('view', view.type)
|
||||
.shouldRefreshWhenFieldCreated(
|
||||
app.$registry,
|
||||
store,
|
||||
data.field,
|
||||
'page/'
|
||||
)
|
||||
if (
|
||||
!fieldType.shouldRefreshWhenAdded() &&
|
||||
!anyFieldsNeedRefresh(relatedFields, registry)
|
||||
fieldType.shouldFetchDataWhenAdded() ||
|
||||
anyFieldsNeedFetch(relatedFields, registry) ||
|
||||
viewMustBeRefreshed
|
||||
) {
|
||||
callback()
|
||||
} else {
|
||||
app.$bus.$emit('table-refresh', {
|
||||
tableId: store.getters['table/getSelectedId'],
|
||||
newField: data.field,
|
||||
includeFieldOptions: true,
|
||||
callback,
|
||||
})
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -135,15 +135,16 @@ export const actions = {
|
|||
}
|
||||
const fieldType = this.$registry.get('field', type)
|
||||
|
||||
const refreshNeeded =
|
||||
fieldType.shouldRefreshWhenAdded() ||
|
||||
anyFieldsNeedRefresh(data.related_fields, this.$registry)
|
||||
const fetchNeeded =
|
||||
fieldType.shouldFetchDataWhenAdded() ||
|
||||
anyFieldsNeedFetch(data.related_fields, this.$registry)
|
||||
const callback = forceCreate
|
||||
? await forceCreateCallback()
|
||||
: forceCreateCallback
|
||||
return {
|
||||
forceCreateCallback: callback,
|
||||
refreshNeeded,
|
||||
fetchNeeded,
|
||||
newField: data,
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
@ -186,7 +187,7 @@ export const actions = {
|
|||
// need to change things in loaded data. For example the grid field will add the
|
||||
// field to all of the rows that are in memory.
|
||||
for (const viewType of Object.values(this.$registry.getAll('view'))) {
|
||||
await viewType.fieldCreated(context, table, data, fieldType, 'page/')
|
||||
await viewType.afterFieldCreated(context, table, data, fieldType, 'page/')
|
||||
}
|
||||
|
||||
await dispatch('forceUpdateFields', {
|
||||
|
@ -251,7 +252,13 @@ export const actions = {
|
|||
// Call the field updated event on all the registered views because they might
|
||||
// need to change things in loaded data. For example the changed rows.
|
||||
for (const viewType of Object.values(this.$registry.getAll('view'))) {
|
||||
await viewType.fieldUpdated(context, data, oldField, fieldType, 'page/')
|
||||
await viewType.afterFieldUpdated(
|
||||
context,
|
||||
data,
|
||||
oldField,
|
||||
fieldType,
|
||||
'page/'
|
||||
)
|
||||
}
|
||||
|
||||
await dispatch('forceUpdateFields', {
|
||||
|
@ -315,7 +322,7 @@ export const actions = {
|
|||
// field options of that field.
|
||||
const fieldType = this.$registry.get('field', field.type)
|
||||
for (const viewType of Object.values(this.$registry.getAll('view'))) {
|
||||
await viewType.fieldDeleted(context, field, fieldType, 'page/')
|
||||
await viewType.afterFieldDeleted(context, field, fieldType, 'page/')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -351,9 +358,9 @@ export default {
|
|||
mutations,
|
||||
}
|
||||
|
||||
export function anyFieldsNeedRefresh(fields, registry) {
|
||||
export function anyFieldsNeedFetch(fields, registry) {
|
||||
return fields.some((f) => {
|
||||
const relatedFieldType = registry.get('field', f.type)
|
||||
return relatedFieldType.shouldRefreshWhenAdded()
|
||||
return relatedFieldType.shouldFetchDataWhenAdded()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { clone } from '@baserow/modules/core/utils/object'
|
|||
import {
|
||||
getRowSortFunction,
|
||||
matchSearchFilters,
|
||||
calculateSingleRowSearchMatches,
|
||||
} from '@baserow/modules/database/utils/view'
|
||||
import RowService from '@baserow/modules/database/services/row'
|
||||
|
||||
|
@ -39,9 +40,22 @@ import RowService from '@baserow/modules/database/services/row'
|
|||
* ]
|
||||
* ```
|
||||
*/
|
||||
export default ({ service, populateRow }) => {
|
||||
export default ({ service, customPopulateRow }) => {
|
||||
let lastRequestSource = null
|
||||
|
||||
const populateRow = (row) => {
|
||||
if (customPopulateRow) {
|
||||
customPopulateRow(row)
|
||||
}
|
||||
row._ ??= {}
|
||||
// Matching rows for front-end only search is not yet properly
|
||||
// supported and tested in this store mixin. Only server-side search
|
||||
// implementation is finished.
|
||||
row._.matchSearch = true
|
||||
row._.fieldSearchMatches = []
|
||||
return row
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function calculates the most optimal `limit` `offset` range of rows
|
||||
* that must be fetched. Based on the provided visible `startIndex` and `endIndex`
|
||||
|
@ -130,6 +144,7 @@ export default ({ service, populateRow }) => {
|
|||
// This is needed to revert the position if anything goes wrong or the escape
|
||||
// key was pressed.
|
||||
draggingOriginalBefore: null,
|
||||
activeSearchTerm: '',
|
||||
})
|
||||
|
||||
const mutations = {
|
||||
|
@ -207,6 +222,23 @@ export default ({ service, populateRow }) => {
|
|||
state.draggingRow = null
|
||||
state.draggingOriginalBefore = null
|
||||
},
|
||||
SET_SEARCH(state, { activeSearchTerm }) {
|
||||
state.activeSearchTerm = activeSearchTerm
|
||||
},
|
||||
SET_ROW_SEARCH_MATCHES(state, { row, matchSearch, fieldSearchMatches }) {
|
||||
row._.fieldSearchMatches.slice(0).forEach((value) => {
|
||||
if (!fieldSearchMatches.has(value)) {
|
||||
const index = row._.fieldSearchMatches.indexOf(value)
|
||||
row._.fieldSearchMatches.splice(index, 1)
|
||||
}
|
||||
})
|
||||
fieldSearchMatches.forEach((value) => {
|
||||
if (!row._.fieldSearchMatches.includes(value)) {
|
||||
row._.fieldSearchMatches.push(value)
|
||||
}
|
||||
})
|
||||
row._.matchSearch = matchSearch
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
@ -221,10 +253,14 @@ export default ({ service, populateRow }) => {
|
|||
) {
|
||||
const { commit, getters } = context
|
||||
commit('SET_VIEW_ID', viewId)
|
||||
commit('SET_SEARCH', {
|
||||
activeSearchTerm: '',
|
||||
})
|
||||
const { data } = await service(this.$client).fetchRows({
|
||||
viewId,
|
||||
offset: 0,
|
||||
limit: getters.getRequestSize,
|
||||
search: getters.getServerSearchTerm,
|
||||
...initialRowArguments,
|
||||
})
|
||||
const rows = Array(data.count).fill(null)
|
||||
|
@ -293,6 +329,7 @@ export default ({ service, populateRow }) => {
|
|||
offset: rangeToFetch.offset,
|
||||
limit: rangeToFetch.limit,
|
||||
cancelToken: lastRequestSource.token,
|
||||
search: getters.getServerSearchTerm,
|
||||
})
|
||||
commit('UPDATE_ROWS', {
|
||||
offset: rangeToFetch.offset,
|
||||
|
@ -345,6 +382,7 @@ export default ({ service, populateRow }) => {
|
|||
} = await service(this.$client).fetchCount({
|
||||
viewId: getters.getViewId,
|
||||
cancelToken: lastRequestSource.token,
|
||||
search: getters.getServerSearchTerm,
|
||||
})
|
||||
|
||||
// Create a new empty array containing un-fetched rows.
|
||||
|
@ -359,9 +397,9 @@ export default ({ service, populateRow }) => {
|
|||
startIndex = currentVisible.startIndex
|
||||
endIndex = currentVisible.endIndex
|
||||
const difference = count - endIndex
|
||||
|
||||
if (difference < 0) {
|
||||
startIndex += difference
|
||||
startIndex = startIndex >= 0 ? startIndex : 0
|
||||
endIndex += difference
|
||||
}
|
||||
|
||||
|
@ -384,6 +422,7 @@ export default ({ service, populateRow }) => {
|
|||
limit: rangeToFetch.limit,
|
||||
includeFieldOptions,
|
||||
cancelToken: lastRequestSource.token,
|
||||
search: getters.getServerSearchTerm,
|
||||
})
|
||||
|
||||
results.forEach((row, index) => {
|
||||
|
@ -570,11 +609,19 @@ export default ({ service, populateRow }) => {
|
|||
let row = clone(values)
|
||||
populateRow(row)
|
||||
|
||||
// Check if the row matches the filters. If not, we don't have to do anything
|
||||
// because we know this row does not exist in the view.
|
||||
if (
|
||||
!(await dispatch('rowMatchesFilters', { view, fields, primary, row }))
|
||||
) {
|
||||
const rowMatchesFilters = await dispatch('rowMatchesFilters', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row,
|
||||
})
|
||||
await dispatch('updateSearchMatchesForRow', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row,
|
||||
})
|
||||
if (!rowMatchesFilters || !row._.matchSearch) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -679,18 +726,33 @@ export default ({ service, populateRow }) => {
|
|||
populateRow(oldRow)
|
||||
populateRow(newRow)
|
||||
|
||||
const oldRowMatches = await dispatch('rowMatchesFilters', {
|
||||
const oldMatchesFilters = await dispatch('rowMatchesFilters', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row: oldRow,
|
||||
})
|
||||
const newRowMatches = await dispatch('rowMatchesFilters', {
|
||||
const newMatchesFilters = await dispatch('rowMatchesFilters', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row: newRow,
|
||||
})
|
||||
await dispatch('updateSearchMatchesForRow', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row: oldRow,
|
||||
})
|
||||
await dispatch('updateSearchMatchesForRow', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row: newRow,
|
||||
})
|
||||
|
||||
const oldRowMatches = oldMatchesFilters && oldRow._.matchSearch
|
||||
const newRowMatches = newMatchesFilters && newRow._.matchSearch
|
||||
|
||||
if (oldRowMatches && !newRowMatches) {
|
||||
// If the old row did match the filters, but after the update it does not
|
||||
|
@ -773,11 +835,19 @@ export default ({ service, populateRow }) => {
|
|||
row = clone(row)
|
||||
populateRow(row)
|
||||
|
||||
// Check if the row matches the filters. If not, we don't have to do anything
|
||||
// because we know this row does not exist in the view.
|
||||
if (
|
||||
!(await dispatch('rowMatchesFilters', { view, fields, primary, row }))
|
||||
) {
|
||||
const rowMatchesFilters = await dispatch('rowMatchesFilters', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row,
|
||||
})
|
||||
await dispatch('updateSearchMatchesForRow', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row,
|
||||
})
|
||||
if (!rowMatchesFilters || !row._.matchSearch) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -876,6 +946,55 @@ export default ({ service, populateRow }) => {
|
|||
|
||||
return false
|
||||
},
|
||||
/**
|
||||
* Changes the current search parameters if provided and optionally refreshes which
|
||||
* cells match the new search parameters by updating every rows row._.matchSearch and
|
||||
* row._.fieldSearchMatches attributes.
|
||||
*/
|
||||
updateSearch(
|
||||
{ commit, dispatch, getters, state },
|
||||
{
|
||||
fields,
|
||||
primary = null,
|
||||
activeSearchTerm = state.activeSearchTerm,
|
||||
refreshMatchesOnClient = true,
|
||||
}
|
||||
) {
|
||||
commit('SET_SEARCH', { activeSearchTerm })
|
||||
if (refreshMatchesOnClient) {
|
||||
getters.getRows.forEach((row) =>
|
||||
dispatch('updateSearchMatchesForRow', {
|
||||
row,
|
||||
fields,
|
||||
primary,
|
||||
forced: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Updates a single row's row._.matchSearch and row._.fieldSearchMatches based on the
|
||||
* current search parameters and row data. Overrides can be provided which can be used
|
||||
* to override a row's field values when checking if they match the search parameters.
|
||||
*/
|
||||
updateSearchMatchesForRow(
|
||||
{ commit, getters, rootGetters },
|
||||
{ row, fields, primary = null, overrides, forced = false }
|
||||
) {
|
||||
// Avoid computing search on table loading
|
||||
if (getters.getActiveSearchTerm || forced) {
|
||||
const rowSearchMatches = calculateSingleRowSearchMatches(
|
||||
row,
|
||||
getters.getActiveSearchTerm,
|
||||
getters.isHidingRowsNotMatchingSearch,
|
||||
[primary, ...fields],
|
||||
this.$registry,
|
||||
overrides
|
||||
)
|
||||
|
||||
commit('SET_ROW_SEARCH_MATCHES', rowSearchMatches)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
@ -903,6 +1022,15 @@ export default ({ service, populateRow }) => {
|
|||
getDraggingOriginalBefore(state) {
|
||||
return state.draggingOriginalBefore
|
||||
},
|
||||
getActiveSearchTerm(state) {
|
||||
return state.activeSearchTerm
|
||||
},
|
||||
getServerSearchTerm(state) {
|
||||
return state.activeSearchTerm
|
||||
},
|
||||
isHidingRowsNotMatchingSearch(state) {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -83,6 +83,17 @@ export const matchSearchFilters = (
|
|||
}
|
||||
}
|
||||
|
||||
export function valueMatchesActiveSearchTerm(
|
||||
registry,
|
||||
field,
|
||||
value,
|
||||
activeSearchTerm
|
||||
) {
|
||||
return registry
|
||||
.get('field', field.type)
|
||||
.containsFilter(value, activeSearchTerm, field)
|
||||
}
|
||||
|
||||
function _findFieldsInRowMatchingSearch(
|
||||
row,
|
||||
activeSearchTerm,
|
||||
|
@ -102,9 +113,12 @@ function _findFieldsInRowMatchingSearch(
|
|||
const rowValue =
|
||||
fieldName in overrides ? overrides[fieldName] : row[fieldName]
|
||||
if (rowValue) {
|
||||
const doesMatch = registry
|
||||
.get('field', field.type)
|
||||
.containsFilter(rowValue, activeSearchTerm, field)
|
||||
const doesMatch = valueMatchesActiveSearchTerm(
|
||||
registry,
|
||||
field,
|
||||
rowValue,
|
||||
activeSearchTerm
|
||||
)
|
||||
if (doesMatch) {
|
||||
fieldSearchMatches.add(field.id.toString())
|
||||
}
|
||||
|
@ -144,3 +158,25 @@ export function calculateSingleRowSearchMatches(
|
|||
!hideRowsNotMatchingSearch || searchIsBlank || fieldSearchMatches.size > 0
|
||||
return { row, matchSearch, fieldSearchMatches }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is the empty value of the provided field matches the active search term.
|
||||
*/
|
||||
export function newFieldMatchesActiveSearchTerm(
|
||||
registry,
|
||||
newField,
|
||||
activeSearchTerm
|
||||
) {
|
||||
if (newField && activeSearchTerm !== '') {
|
||||
const fieldType = registry.get('field', newField.type)
|
||||
const emptyValue = fieldType.getEmptyValue(newField)
|
||||
|
||||
return valueMatchesActiveSearchTerm(
|
||||
registry,
|
||||
newField,
|
||||
emptyValue,
|
||||
activeSearchTerm
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import GalleryViewHeader from '@baserow/modules/database/components/view/gallery
|
|||
import FormView from '@baserow/modules/database/components/view/form/FormView'
|
||||
import FormViewHeader from '@baserow/modules/database/components/view/form/FormViewHeader'
|
||||
import { FileFieldType } from '@baserow/modules/database/fieldTypes'
|
||||
import { newFieldMatchesActiveSearchTerm } from '@baserow/modules/database/utils/view'
|
||||
|
||||
export const maxPossibleOrderValue = 32767
|
||||
|
||||
|
@ -174,11 +175,20 @@ export class ViewType extends Registerable {
|
|||
includeFieldOptions = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Should return true if the view must be refreshed when a new field has been
|
||||
* created. This could for example be used to check whether the empty value matches
|
||||
* the active search term.
|
||||
*/
|
||||
shouldRefreshWhenFieldCreated(registry, store, field, storePrefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that is called when a field has been created. This can be useful to
|
||||
* maintain data integrity for example to add the field to the grid view store.
|
||||
*/
|
||||
fieldCreated(context, table, field, fieldType, storePrefix) {}
|
||||
afterFieldCreated(context, table, field, fieldType, storePrefix) {}
|
||||
|
||||
/**
|
||||
* Method that is called when a field has been restored . This can be useful to
|
||||
|
@ -190,13 +200,13 @@ export class ViewType extends Registerable {
|
|||
* Method that is called when a field has been deleted. This can be useful to
|
||||
* maintain data integrity.
|
||||
*/
|
||||
fieldDeleted(context, field, fieldType, storePrefix) {}
|
||||
afterFieldDeleted(context, field, fieldType, storePrefix) {}
|
||||
|
||||
/**
|
||||
* Method that is called when a field has been changed. This can be useful to
|
||||
* maintain data integrity by updating the values.
|
||||
*/
|
||||
fieldUpdated(context, field, oldField, fieldType, storePrefix) {}
|
||||
afterFieldUpdated(context, field, oldField, fieldType, storePrefix) {}
|
||||
|
||||
/**
|
||||
* Method that is called when the field options of a view are updated.
|
||||
|
@ -387,7 +397,19 @@ export class GridViewType extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldCreated({ dispatch }, table, field, fieldType, storePrefix = '') {
|
||||
shouldRefreshWhenFieldCreated(registry, store, field, storePrefix) {
|
||||
const searchTerm =
|
||||
store.getters[storePrefix + 'view/grid/getActiveSearchTerm']
|
||||
return newFieldMatchesActiveSearchTerm(registry, field, searchTerm)
|
||||
}
|
||||
|
||||
async afterFieldCreated(
|
||||
{ dispatch },
|
||||
table,
|
||||
field,
|
||||
fieldType,
|
||||
storePrefix = ''
|
||||
) {
|
||||
const value = fieldType.getEmptyValue(field)
|
||||
await dispatch(
|
||||
storePrefix + 'view/grid/addField',
|
||||
|
@ -410,7 +432,7 @@ export class GridViewType extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
async afterFieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
await dispatch(
|
||||
storePrefix + 'view/grid/forceDeleteFieldOptions',
|
||||
field.id,
|
||||
|
@ -420,7 +442,7 @@ export class GridViewType extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldUpdated(
|
||||
async afterFieldUpdated(
|
||||
{ dispatch, rootGetters },
|
||||
field,
|
||||
oldField,
|
||||
|
@ -590,7 +612,21 @@ class BaseBufferedRowView extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldCreated({ dispatch }, table, field, fieldType, storePrefix = '') {
|
||||
shouldRefreshWhenFieldCreated(registry, store, field, storePrefix) {
|
||||
const searchTerm =
|
||||
store.getters[
|
||||
storePrefix + 'view/' + this.getType() + '/getActiveSearchTerm'
|
||||
]
|
||||
return newFieldMatchesActiveSearchTerm(registry, field, searchTerm)
|
||||
}
|
||||
|
||||
async afterFieldCreated(
|
||||
{ dispatch },
|
||||
table,
|
||||
field,
|
||||
fieldType,
|
||||
storePrefix = ''
|
||||
) {
|
||||
const value = fieldType.getEmptyValue(field)
|
||||
await dispatch(
|
||||
storePrefix + 'view/' + this.getType() + '/addField',
|
||||
|
@ -607,7 +643,7 @@ class BaseBufferedRowView extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
async afterFieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
await dispatch(
|
||||
storePrefix + 'view/' + this.getType() + '/forceDeleteFieldOptions',
|
||||
field.id,
|
||||
|
@ -723,17 +759,41 @@ export class GalleryViewType extends BaseBufferedRowView {
|
|||
}
|
||||
}
|
||||
|
||||
fieldUpdated(context, field, oldField, fieldType, storePrefix) {
|
||||
async afterFieldUpdated(
|
||||
{ dispatch, rootGetters },
|
||||
field,
|
||||
oldField,
|
||||
fieldType,
|
||||
storePrefix
|
||||
) {
|
||||
// If the field type has changed from a file field to something else, it could
|
||||
// be that there are gallery views that depending on that field. So we need to
|
||||
// change to type to null if that's the case.
|
||||
const type = FileFieldType.getType()
|
||||
if (oldField.type === type && field.type !== type) {
|
||||
this._setFieldToNull(context, field, 'card_cover_image_field')
|
||||
this._setFieldToNull(
|
||||
{ dispatch, rootGetters },
|
||||
field,
|
||||
'card_cover_image_field'
|
||||
)
|
||||
}
|
||||
// The field changing may change which cells in the field should be highlighted so
|
||||
// we refresh them to ensure that they still correctly match. E.g. changing a date
|
||||
// fields date_format needs a search update as search string might no longer
|
||||
// match the new format.
|
||||
await dispatch(
|
||||
storePrefix + 'view/gallery/updateSearch',
|
||||
{
|
||||
fields: rootGetters['field/getAll'],
|
||||
primary: rootGetters['field/getPrimary'],
|
||||
},
|
||||
{
|
||||
root: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fieldDeleted(context, field, fieldType, storePrefix = '') {
|
||||
afterFieldDeleted(context, field, fieldType, storePrefix = '') {
|
||||
// We want to loop over all gallery views that we have in the store and check if
|
||||
// they were depending on this deleted field. If that's case, we can set it to null
|
||||
// because it doesn't exist anymore.
|
||||
|
@ -801,7 +861,13 @@ export class FormViewType extends ViewType {
|
|||
})
|
||||
}
|
||||
|
||||
async fieldCreated({ dispatch }, table, field, fieldType, storePrefix = '') {
|
||||
async afterFieldCreated(
|
||||
{ dispatch },
|
||||
table,
|
||||
field,
|
||||
fieldType,
|
||||
storePrefix = ''
|
||||
) {
|
||||
await dispatch(
|
||||
storePrefix + 'view/form/setFieldOptionsOfField',
|
||||
{
|
||||
|
@ -820,7 +886,7 @@ export class FormViewType extends ViewType {
|
|||
)
|
||||
}
|
||||
|
||||
async fieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
async afterFieldDeleted({ dispatch }, field, fieldType, storePrefix = '') {
|
||||
await dispatch(
|
||||
storePrefix + 'view/form/forceDeleteFieldOptions',
|
||||
field.id,
|
||||
|
|
14
web-frontend/test/fixtures/fields.js
vendored
14
web-frontend/test/fixtures/fields.js
vendored
|
@ -23,3 +23,17 @@ export function createFields(mock, application, table, fields) {
|
|||
mock.onGet(`/database/fields/table/${table.id}/`).reply(200, fieldsWithIds)
|
||||
return fieldsWithIds
|
||||
}
|
||||
|
||||
export function createPrimaryField(data) {
|
||||
const primaryField = {
|
||||
id: 1,
|
||||
name: 'Primary field',
|
||||
type: 'text',
|
||||
primary: true,
|
||||
}
|
||||
|
||||
return {
|
||||
...primaryField,
|
||||
...data,
|
||||
}
|
||||
}
|
||||
|
|
14
web-frontend/test/fixtures/view.js
vendored
14
web-frontend/test/fixtures/view.js
vendored
|
@ -1,5 +1,19 @@
|
|||
import { PUBLIC_PLACEHOLDER_ENTITY_ID } from '@baserow/modules/database/utils/constants'
|
||||
|
||||
export function createView(data) {
|
||||
const view = {
|
||||
id: 1,
|
||||
filters_disabled: false,
|
||||
filters: [],
|
||||
sortings: [],
|
||||
}
|
||||
|
||||
return {
|
||||
...view,
|
||||
...data,
|
||||
}
|
||||
}
|
||||
|
||||
export function createPublicGridView(
|
||||
mock,
|
||||
viewSlug,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import bufferedRows from '@baserow/modules/database/store/view/bufferedRows'
|
||||
import { TestApp } from '@baserow/test/helpers/testApp'
|
||||
import { ContainsViewFilterType } from '@baserow/modules/database/viewFilters'
|
||||
import { createPrimaryField } from '@baserow/test/fixtures/fields'
|
||||
import { createView } from '@baserow/test/fixtures/view'
|
||||
|
||||
describe('Buffered rows view store helper', () => {
|
||||
let testApp = null
|
||||
|
@ -1995,3 +1997,168 @@ describe('Buffered rows view store helper', () => {
|
|||
expect(rowsInStore[11]).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Buffered rows search', () => {
|
||||
let testApp = null
|
||||
let store = null
|
||||
let bufferedRowsModule = null
|
||||
let view = null
|
||||
const storeName = 'test'
|
||||
const activeSearchTerm = 'searchterm'
|
||||
|
||||
beforeEach(() => {
|
||||
testApp = new TestApp()
|
||||
store = testApp.store
|
||||
bufferedRowsModule = bufferedRows({ service: null, populateRow: null })
|
||||
view = createView()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
testApp.afterEach()
|
||||
})
|
||||
|
||||
test('Rows are fetched on refresh based on search term', async () => {
|
||||
const serviceStub = () => {
|
||||
return {
|
||||
fetchRows(params) {
|
||||
if (params.search === activeSearchTerm) {
|
||||
return {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_1: 'Row matching search',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_1: 'Row matching search',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_1: 'Row not search',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
fetchCount(params) {
|
||||
if (params.search === activeSearchTerm) {
|
||||
return { data: { count: 1 } }
|
||||
}
|
||||
return { data: { count: 2 } }
|
||||
},
|
||||
}
|
||||
}
|
||||
bufferedRowsModule = bufferedRows({
|
||||
service: serviceStub,
|
||||
populateRow: null,
|
||||
})
|
||||
const state = Object.assign(bufferedRowsModule.state(), {
|
||||
viewId: view.id,
|
||||
rows: [],
|
||||
activeSearchTerm,
|
||||
})
|
||||
bufferedRowsModule.state = () => state
|
||||
store.registerModule(storeName, bufferedRowsModule)
|
||||
|
||||
await store.dispatch(`${storeName}/refresh`, {
|
||||
fields: [],
|
||||
primary: createPrimaryField(),
|
||||
})
|
||||
|
||||
const rowsInStore = store.getters[`${storeName}/getRows`]
|
||||
expect(rowsInStore.length).toBe(1)
|
||||
})
|
||||
|
||||
test('A new row matching search has been added', async () => {
|
||||
const state = Object.assign(bufferedRowsModule.state(), {
|
||||
viewId: view.id,
|
||||
rows: [{ id: 2, order: '2.00000000000000000000', field_1: 'Row 2' }],
|
||||
activeSearchTerm,
|
||||
})
|
||||
bufferedRowsModule.state = () => state
|
||||
store.registerModule(storeName, bufferedRowsModule)
|
||||
|
||||
const newMatchingRow = {
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_1: `matching the ${activeSearchTerm}`,
|
||||
}
|
||||
|
||||
await store.dispatch(`${storeName}/afterNewRowCreated`, {
|
||||
view,
|
||||
fields: [],
|
||||
primary: createPrimaryField(),
|
||||
values: newMatchingRow,
|
||||
})
|
||||
|
||||
const rowsInStore = store.getters[`${storeName}/getRows`]
|
||||
expect(rowsInStore[0].id).toBe(newMatchingRow.id)
|
||||
})
|
||||
|
||||
test('A new row not matching search has not been added', async () => {
|
||||
const state = Object.assign(bufferedRowsModule.state(), {
|
||||
viewId: view.id,
|
||||
rows: [{ id: 2, order: '2.00000000000000000000', field_1: 'Row 2' }],
|
||||
activeSearchTerm,
|
||||
})
|
||||
bufferedRowsModule.state = () => state
|
||||
store.registerModule(storeName, bufferedRowsModule)
|
||||
|
||||
const newNotMatchingRow = {
|
||||
id: 1,
|
||||
order: '1.00000000000000000000',
|
||||
field_1: `not matching`,
|
||||
}
|
||||
|
||||
await store.dispatch(`${storeName}/afterNewRowCreated`, {
|
||||
view,
|
||||
fields: [],
|
||||
primary: createPrimaryField(),
|
||||
values: newNotMatchingRow,
|
||||
})
|
||||
|
||||
const rowsInStore = store.getters[`${storeName}/getRows`]
|
||||
expect(rowsInStore[0].id).not.toBe(newNotMatchingRow.id)
|
||||
})
|
||||
|
||||
test('A row not matching search anymore has been removed', async () => {
|
||||
const matchingRow = {
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_1: `matching the ${activeSearchTerm}`,
|
||||
}
|
||||
const state = Object.assign(bufferedRowsModule.state(), {
|
||||
viewId: view.id,
|
||||
rows: [matchingRow],
|
||||
activeSearchTerm,
|
||||
})
|
||||
bufferedRowsModule.state = () => state
|
||||
store.registerModule(storeName, bufferedRowsModule)
|
||||
|
||||
const newValues = {
|
||||
field_1: 'not matching',
|
||||
}
|
||||
|
||||
await store.dispatch(`${storeName}/afterExistingRowUpdated`, {
|
||||
view,
|
||||
fields: [],
|
||||
primary: createPrimaryField(),
|
||||
row: matchingRow,
|
||||
values: newValues,
|
||||
})
|
||||
|
||||
const rowsInStore = store.getters[`${storeName}/getRows`]
|
||||
expect(rowsInStore[0]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue