1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 18:32:35 +00:00

Implemented premium license check per group for the web-frontend

This commit is contained in:
Bram Wiepjes 2022-06-20 19:42:00 +00:00
parent 40014ab4ea
commit 8935b66c23
36 changed files with 344 additions and 55 deletions

View file

@ -1,6 +1,6 @@
from baserow.api.user.registries import UserDataType
from baserow_premium.license.handler import has_active_premium_license
from baserow_premium.license.handler import has_active_premium_license_for
class PremiumUserDataType(UserDataType):
@ -12,4 +12,6 @@ class PremiumUserDataType(UserDataType):
user has a valid license for the premioum version.
"""
return {"valid_license": has_active_premium_license(user)}
return {
"valid_license": has_active_premium_license_for(user),
}

View file

@ -3,7 +3,7 @@ import binascii
import hashlib
import json
import logging
from typing import Union, List
from typing import Union, List, Dict, Any
from os.path import dirname, join
from dateutil import parser
@ -75,7 +75,7 @@ def has_active_premium_license(user: DjangoUser) -> bool:
return False
def check_active_premium_license(user):
def check_active_premium_license(user: DjangoUser):
"""
Raises the `NoPremiumLicenseError` if the user does not have an active premium
license.
@ -85,6 +85,35 @@ def check_active_premium_license(user):
raise NoPremiumLicenseError()
def has_active_premium_license_for(
user: DjangoUser,
) -> Union[bool, List[Dict[str, Any]]]:
"""
Check for which objects the user has an active license. If `True` is returned it
means that the user has premium access to everything. If an object is returned,
it means that the user only has access to the specific objects. For now,
it's only possible to grant access to specific groups.
Example complex return value:
[
{
"type": "group",
"id": 1,
},
{
"type": "group",
"id": 2,
}
]
:param user: The user for whom must be checked if it has an active license.
:return: To which groups the user has an active premium license for.
"""
return has_active_premium_license(user)
def get_public_key():
"""
Returns the public key instance that can be used to verify licenses. A different

View file

@ -77,6 +77,10 @@ export default {
RowComment,
},
props: {
database: {
type: Object,
required: true,
},
table: {
required: true,
type: Object,
@ -97,7 +101,10 @@ export default {
},
computed: {
validPremiumLicense() {
return PremiumPlugin.hasValidPremiumLicense(this.additionalUserData)
return PremiumPlugin.hasValidPremiumLicense(
this.additionalUserData,
this.database.group.id
)
},
...mapGetters({
comments: 'row_comments/getSortedRowComments',

View file

@ -84,6 +84,7 @@
></RowCreateModal>
<RowEditModal
ref="rowEditModal"
:database="database"
:table="table"
:primary="primary"
:primary-is-sortable="true"

View file

@ -6,8 +6,23 @@ export class PremiumPlugin extends BaserowPlugin {
return 'premium'
}
static hasValidPremiumLicense(additionalUserData) {
return additionalUserData?.premium?.valid_license
/**
* @param additionalUserData The additional user data that contains to which group
* the user has a premium license for.
* @param forGroup If provided, the user must explicitly have an active license
* for that group or for all groups.
* @return boolean
*/
static hasValidPremiumLicense(additionalUserData, forGroupId = undefined) {
const validLicense = additionalUserData?.premium?.valid_license
const groups = Array.isArray(validLicense)
? validLicense.filter((o) => o.type === 'group').map((o) => o.id)
: []
return (
validLicense === true ||
(Array.isArray(validLicense) && !forGroupId) ||
(Array.isArray(validLicense) && groups.includes(forGroupId))
)
}
getSidebarTopComponent() {

View file

@ -9,9 +9,10 @@ class PremiumTableExporterType extends TableExporterType {
return this.app.i18n.t('premium.deactivated')
}
isDeactivated() {
isDeactivated(groupId) {
return !PremiumPlugin.hasValidPremiumLicense(
this.app.store.getters['auth/getAdditionalUserData']
this.app.store.getters['auth/getAdditionalUserData'],
groupId
)
}
}

View file

@ -42,12 +42,12 @@ export class LeftBorderColorViewDecoratorType extends ViewDecoratorType {
return i18n.t('viewDecoratorType.onlyForPremium')
}
isDeactivated() {
isDeactivated(groupId) {
const { store } = this.app
const additionalUserData = store.getters['auth/getAdditionalUserData']
if (PremiumPlugin.hasValidPremiumLicense(additionalUserData)) {
if (PremiumPlugin.hasValidPremiumLicense(additionalUserData, groupId)) {
return false
}
return true
@ -141,12 +141,12 @@ export class BackgroundColorViewDecoratorType extends ViewDecoratorType {
return i18n.t('viewDecoratorType.onlyForPremium')
}
isDeactivated() {
isDeactivated(groupId) {
const { store } = this.app
const additionalUserData = store.getters['auth/getAdditionalUserData']
if (PremiumPlugin.hasValidPremiumLicense(additionalUserData)) {
if (PremiumPlugin.hasValidPremiumLicense(additionalUserData, groupId)) {
return false
}
return true

View file

@ -12,9 +12,10 @@ class PremiumViewType extends ViewType {
return this.app.i18n.t('premium.deactivated')
}
isDeactivated() {
isDeactivated(groupId) {
return !PremiumPlugin.hasValidPremiumLicense(
this.app.store.getters['auth/getAdditionalUserData']
this.app.store.getters['auth/getAdditionalUserData'],
groupId
)
}
}

View file

@ -0,0 +1,120 @@
import { PremiumPlugin } from '@baserow_premium/plugins'
describe('Test premium Baserow plugin', () => {
test('Test hasValidPremiumLicense method', () => {
expect(
PremiumPlugin.hasValidPremiumLicense({ premium: { valid_license: true } })
).toBe(true)
expect(
PremiumPlugin.hasValidPremiumLicense({
premium: { valid_license: false },
})
).toBe(false)
expect(PremiumPlugin.hasValidPremiumLicense({ premium: {} })).toBe(false)
expect(PremiumPlugin.hasValidPremiumLicense({})).toBe(false)
// If the `valid_license` is `true`, the user has premium access features
// enabled for all groups.
expect(
PremiumPlugin.hasValidPremiumLicense(
{ premium: { valid_license: true } },
1
)
).toBe(true)
expect(
PremiumPlugin.hasValidPremiumLicense(
{ premium: { valid_license: false } },
1
)
).toBe(false)
// If the user only has premium access to certain groups, we expect false
// for other groups.
expect(
PremiumPlugin.hasValidPremiumLicense(
{ premium: { valid_license: [] } },
1
)
).toBe(false)
expect(
PremiumPlugin.hasValidPremiumLicense(
{ premium: { valid_license: [{ type: 'group', id: 2 }] } },
1
)
).toBe(false)
expect(
PremiumPlugin.hasValidPremiumLicense(
{
premium: {
valid_license: [
{ type: 'group', id: 2 },
{ type: 'group', id: 3 },
],
},
},
1
)
).toBe(false)
expect(
PremiumPlugin.hasValidPremiumLicense(
{
premium: {
valid_license: [
{ type: 'group', id: 2 },
{ type: 'group', id: 3 },
],
},
},
4
)
).toBe(false)
// If the user only has premium access to certain groups, he will have
// access to the premium features that are not related to a group.
expect(
PremiumPlugin.hasValidPremiumLicense({
premium: { valid_license: [{ type: 'group', id: 1 }] },
})
).toBe(true)
expect(
PremiumPlugin.hasValidPremiumLicense({
premium: { valid_license: [{ type: 'group', id: 2 }] },
})
).toBe(true)
// If the user only has premium access to certain groups, we expect the
// matches group to be true.
expect(
PremiumPlugin.hasValidPremiumLicense(
{ premium: { valid_license: [{ type: 'group', id: 1 }] } },
1
)
).toBe(true)
expect(
PremiumPlugin.hasValidPremiumLicense(
{
premium: {
valid_license: [
{ type: 'group', id: 1 },
{ type: 'group', id: 2 },
],
},
},
1
)
).toBe(true)
expect(
PremiumPlugin.hasValidPremiumLicense(
{
premium: {
valid_license: [
{ type: 'group', id: 1 },
{ type: 'group', id: 2 },
],
},
},
2
)
).toBe(true)
})
})

View file

@ -19,6 +19,7 @@
v-model="values.exporter_type"
:exporter-types="exporterTypes"
:loading="loading"
:database="database"
></ExporterTypeChoices>
<div v-if="$v.values.exporter_type.$error" class="error">
{{ $t('exportTableForm.typeError') }}
@ -51,6 +52,10 @@ export default {
},
mixins: [form],
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,

View file

@ -6,6 +6,7 @@
<ExportTableForm
ref="form"
v-slot="{ filename }"
:database="database"
:table="table"
:view="view"
:views="views"
@ -39,6 +40,10 @@ export default {
components: { ExportTableForm, ExportTableLoadingBar },
mixins: [modal, error],
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,

View file

@ -19,6 +19,10 @@
export default {
name: 'ExporterTypeChoice',
props: {
database: {
type: Object,
required: true,
},
exporterType: {
required: true,
type: Object,
@ -41,7 +45,7 @@ export default {
deactivated() {
return this.$registry
.get('exporter', this.exporterType.type)
.isDeactivated()
.isDeactivated(this.database.group.id)
},
},
methods: {

View file

@ -12,6 +12,7 @@
:exporter-type="exporterType"
:active="value !== null && value === exporterType.type"
:disabled="loading"
:database="database"
@selected="switchToExporterType(exporterType.type)"
>
</ExporterTypeChoice>
@ -28,6 +29,10 @@ export default {
name: 'ExporterTypeChoices',
components: { ExporterTypeChoice },
props: {
database: {
type: Object,
required: true,
},
exporterTypes: {
required: true,
type: Array,

View file

@ -2,6 +2,7 @@
<RowEditModal
ref="modal"
:read-only="true"
:database="database"
:table="table"
:rows="[]"
:visible-fields="fields"
@ -39,6 +40,26 @@ export default {
primary: undefined,
}
},
computed: {
database() {
const databaseType = DatabaseApplicationType.getType()
for (const application of this.$store.getters['application/getAll']) {
if (application.type !== databaseType) {
continue
}
const foundTable = application.tables.find(
({ id }) => id === this.tableId
)
if (foundTable) {
return application
}
}
return undefined
},
},
methods: {
async fetchTableAndFields() {
// Find the table in the applications to prevent a request to the backend and to

View file

@ -68,6 +68,7 @@
:row="row"
:read-only="readOnly"
:table="table"
:database="database"
></component>
</template>
</Modal>
@ -89,6 +90,10 @@ export default {
},
mixins: [modal],
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,

View file

@ -51,7 +51,11 @@
</a>
</li>
</ul>
<ExportTableModal ref="exportTableModal" :table="table" />
<ExportTableModal
ref="exportTableModal"
:database="database"
:table="table"
/>
<WebhookModal ref="webhookModal" :table="table" />
</Context>
</li>

View file

@ -39,6 +39,7 @@
<ViewsContext
v-if="views !== null"
ref="viewsContext"
:database="database"
:table="table"
:views="views"
:read-only="readOnly"
@ -65,6 +66,7 @@
</a>
<ViewContext
ref="viewContext"
:database="database"
:view="view"
:table="table"
@enable-rename="$refs.rename.edit()"
@ -108,6 +110,7 @@
class="header__filter-item"
>
<ViewDecoratorMenu
:database="database"
:view="view"
:table="table"
:fields="fields"

View file

@ -31,6 +31,10 @@ export default {
CreateViewModal,
},
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,
@ -45,7 +49,7 @@ export default {
return this.viewType.getDeactivatedText()
},
deactivated() {
return this.viewType.isDeactivated()
return this.viewType.isDeactivated(this.database.group.id)
},
},
}

View file

@ -1,6 +1,10 @@
<template>
<Context ref="context">
<ViewDecoratorList :view="view" @select="$emit('select', $event)" />
<ViewDecoratorList
:database="database"
:view="view"
@select="$emit('select', $event)"
/>
</Context>
</template>
@ -15,6 +19,10 @@ export default {
},
mixins: [context],
props: {
database: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,

View file

@ -26,7 +26,12 @@
</a>
</li>
</ul>
<ExportTableModal ref="exportViewModal" :table="table" :view="view" />
<ExportTableModal
ref="exportViewModal"
:database="database"
:table="table"
:view="view"
/>
<WebhookModal ref="webhookModal" :table="table" />
</Context>
</template>
@ -44,6 +49,10 @@ export default {
components: { ExportTableModal, WebhookModal },
mixins: [context, error],
props: {
database: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,

View file

@ -2,6 +2,7 @@
<Context>
<ViewDecoratorList
v-if="activeDecorations.length === 0"
:database="database"
:view="view"
@select="addDecoration($event)"
/>
@ -92,6 +93,7 @@
</a>
<SelectViewDecoratorContext
ref="selectDecoratorContext"
:database="database"
:view="view"
@select="
;[$refs.selectDecoratorContext.hide(), addDecoration($event)]
@ -123,6 +125,10 @@ export default {
},
mixins: [context, viewDecoration],
props: {
database: {
type: Object,
required: true,
},
primary: {
type: Object,
required: true,

View file

@ -20,6 +20,10 @@ export default {
name: 'ViewDecoratorList',
components: { ViewDecoratorItem },
props: {
database: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,
@ -35,17 +39,12 @@ export default {
methods: {
isDisabled(decoratorType) {
return (
decoratorType.isDeactivated({
view: this.view,
}) || !decoratorType.canAdd({ view: this.view })[0]
decoratorType.isDeactivated(this.database.group.id) ||
!decoratorType.canAdd({ view: this.view })[0]
)
},
getTooltip(decoratorType) {
if (
decoratorType.isDeactivated({
view: this.view,
})
) {
if (decoratorType.isDeactivated(this.database.group.id)) {
return decoratorType.getDeactivatedText()
}
const [canAdd, disabledReason] = decoratorType.canAdd({ view: this.view })

View file

@ -17,6 +17,7 @@
</a>
<ViewDecoratorContext
ref="context"
:database="database"
:view="view"
:table="table"
:fields="fields"
@ -34,6 +35,10 @@ export default {
name: 'ViewDecoratorMenu',
components: { ViewDecoratorContext },
props: {
database: {
type: Object,
required: true,
},
primary: {
type: Object,
required: true,
@ -60,7 +65,7 @@ export default {
return this.view.decorations.filter(({ type }) => {
return !this.$registry
.get('viewDecorator', type)
.isDeactivated({ view: this.view })
.isDeactivated(this.database.group.id)
}).length
},
},

View file

@ -28,6 +28,7 @@
update: order,
marginTop: -1.5,
}"
:database="database"
:view="view"
:table="table"
:read-only="readOnly"
@ -42,6 +43,7 @@
<CreateViewLink
v-for="(viewType, type) in viewTypes"
:key="type"
:database="database"
:table="table"
:view-type="viewType"
@created="selectedView"
@ -69,6 +71,10 @@ export default {
},
mixins: [context, dropdownHelpers],
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,

View file

@ -36,6 +36,7 @@
</a>
<ViewContext
ref="context"
:database="database"
:table="table"
:view="view"
@enable-rename="enableRename"
@ -54,6 +55,10 @@ export default {
components: { EditableViewName, ViewContext },
mixins: [context],
props: {
database: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,
@ -79,7 +84,7 @@ export default {
!this.readOnly &&
this.$registry
.get('view', this.view.type)
.isDeactivated({ view: this.view })
.isDeactivated(this.database.group.id)
)
},
},

View file

@ -72,6 +72,7 @@
></RowCreateModal>
<RowEditModal
ref="rowEditModal"
:database="database"
:table="table"
:primary="primary"
:primary-is-sortable="true"

View file

@ -182,6 +182,7 @@
</Context>
<RowEditModal
ref="rowEditModal"
:database="database"
:table="table"
:primary="primary"
:visible-fields="[primary].concat(visibleFields)"

View file

@ -75,7 +75,7 @@ export class TableExporterType extends Registerable {
/**
* Indicates if the exporter type is disabled.
*/
isDeactivated() {
isDeactivated(groupId) {
return false
}
}

View file

@ -5,6 +5,11 @@
*/
export default {
props: {
database: {
type: Object,
required: false,
default: undefined,
},
view: {
type: Object,
required: false,
@ -57,7 +62,7 @@ export default {
})
.filter(
({ decoratorType }) =>
!decoratorType.isDeactivated({ view: this.view })
!decoratorType.isDeactivated(this.database.group.id)
)
},
decorationsByPlace() {

View file

@ -97,7 +97,7 @@ export default {
// filled with initial data so we're going to call the fetch function here.
const type = app.$registry.get('view', view.type)
if (type.isDeactivated()) {
if (type.isDeactivated(data.database.group.id)) {
return error({ statusCode: 400, message: type.getDeactivatedText() })
}

View file

@ -30,7 +30,7 @@ export class ViewDecoratorType extends Registerable {
/**
* Indicates if the decorator type is disabled.
*/
isDeactivated({ view }) {
isDeactivated(groupId) {
return false
}

View file

@ -299,7 +299,7 @@ export class ViewType extends Registerable {
/**
* Indicates if the view type is disabled.
*/
isDeactivated() {
isDeactivated(groupId) {
return false
}

View file

@ -16,19 +16,20 @@ describe('Preview exportTableModal', () => {
async function givenThereIsATableWithView() {
const table = mockServer.createTable()
const database = { tables: [] }
const database = { tables: [], group: { id: 0 } }
await testApp.store.dispatch('table/forceCreate', {
database,
data: table,
})
const view = mockServer.createGridView(database, table, {})
return { table, view }
return { database, table, view }
}
test('Modal with no view', async () => {
const { table } = await givenThereIsATableWithView()
const { table, database } = await givenThereIsATableWithView()
const wrapper = await testApp.mount(ExportTableModal, {
propsData: {
database,
table,
view: null,
},
@ -42,9 +43,10 @@ describe('Preview exportTableModal', () => {
})
test('Modal with view', async () => {
const { table, view } = await givenThereIsATableWithView()
const { table, view, database } = await givenThereIsATableWithView()
const wrapper = await testApp.mount(ExportTableModal, {
propsData: {
database,
table,
view,
},

View file

@ -12,7 +12,7 @@ export class FakeDecoratorType extends ViewDecoratorType {
return 'first_cell'
}
isDeactivated({ view }) {
isDeactivated(groupId) {
return false
}
@ -138,11 +138,11 @@ describe('GalleryView component with decoration', () => {
})
await store.dispatch('view/fetchAll', { id: 1 })
return { table, primary, fields, view }
return { application, table, primary, fields, view }
}
test('Default component with first_cell decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -162,6 +162,7 @@ describe('GalleryView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,
@ -174,7 +175,7 @@ describe('GalleryView component with decoration', () => {
})
test('Default component with row wrapper decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -210,6 +211,7 @@ describe('GalleryView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,
@ -222,7 +224,7 @@ describe('GalleryView component with decoration', () => {
})
test('Default component with unavailable decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -239,6 +241,7 @@ describe('GalleryView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,

View file

@ -12,7 +12,7 @@ export class FakeDecoratorType extends ViewDecoratorType {
return 'first_cell'
}
isDeactivated({ view }) {
isDeactivated(groupId) {
return false
}
@ -137,11 +137,11 @@ describe('GridView component with decoration', () => {
primary,
})
await store.dispatch('view/fetchAll', { id: 1 })
return { table, primary, fields, view }
return { application, table, primary, fields, view }
}
test('Default component with first_cell decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -161,6 +161,7 @@ describe('GridView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,
@ -173,7 +174,7 @@ describe('GridView component with decoration', () => {
})
test('Default component with row wrapper decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -209,6 +210,7 @@ describe('GridView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,
@ -221,7 +223,7 @@ describe('GridView component with decoration', () => {
})
test('Default component with unavailable decoration', async () => {
const { table, primary, fields, view } = await populateStore([
const { application, table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -238,6 +240,7 @@ describe('GridView component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
database: application,
table,
view,
primary,

View file

@ -24,7 +24,7 @@ export class FakeDecoratorType extends ViewDecoratorType {
return true
}
isDeactivated({ view }) {
isDeactivated(groupId) {
return false
}
@ -163,7 +163,7 @@ describe('GridViewRows component with decoration', () => {
primary,
})
await store.dispatch('view/fetchAll', { id: table.id })
return { table, primary, fields, view }
return { application, table, primary, fields, view }
}
const mountComponent = async (props) => {
@ -181,7 +181,7 @@ describe('GridViewRows component with decoration', () => {
}
test('Default component', async () => {
const { table, primary, fields, view } = await populateStore()
const { application, table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -190,6 +190,7 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper = await mountComponent({
database: application,
view,
table,
primary,
@ -201,7 +202,7 @@ describe('GridViewRows component with decoration', () => {
})
test('View with decoration configured', async () => {
const { table, primary, fields, view } = await populateStore({
const { application, table, primary, fields, view } = await populateStore({
decorations: [
{
type: 'fake_decorator',
@ -225,6 +226,7 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper = await mountComponent({
database: application,
view,
table,
primary,
@ -236,7 +238,7 @@ describe('GridViewRows component with decoration', () => {
})
test('Should show unavailable decorator tooltip', async () => {
const { table, primary, fields, view } = await populateStore()
const { application, table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -247,6 +249,7 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper = await mountComponent({
database: application,
view,
table,
primary,
@ -262,7 +265,7 @@ describe('GridViewRows component with decoration', () => {
})
test('Should show cant add decorator tooltip', async () => {
const { table, primary, fields, view } = await populateStore()
const { application, table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -273,6 +276,7 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper = await mountComponent({
database: application,
view,
table,
primary,