1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 06:15:36 +00:00

moved the abstractions out of the stores, fixed grid unexisting data bug and made row data more consistent in de backend

This commit is contained in:
Bram Wiepjes 2020-04-07 19:49:45 +00:00
parent 0709cf0aec
commit f44b07c3b0
26 changed files with 371 additions and 247 deletions

View file

@ -28,23 +28,32 @@ class DatabaseApplicationType(ApplicationType):
"""
database = CoreHandler().create_application(user, group, type_name=self.type,
name='Company')
name=f"{user.first_name}'s company")
table = TableHandler().create_table(user, database, name='Customers')
ViewHandler().create_view(user, table, GridViewType.type, name='Grid')
FieldHandler().create_field(user, table, TextFieldType.type, name='Last name')
FieldHandler().create_field(user, table, BooleanFieldType.type, name='Active')
model = table.get_model(attribute_names=True)
model.objects.create(name='Elon', last_name='Musk', active=True)
model.objects.create(name='Bill', last_name='Gates', active=False)
model.objects.create(name='Mark', last_name='Zuckerburg', active=True)
model.objects.create(name='Jeffrey', last_name='Bezos', active=True)
table_2 = TableHandler().create_table(user, database, name='Projects')
ViewHandler().create_view(user, table_2, GridViewType.type, name='Grid')
FieldHandler().create_field(user, table_2, BooleanFieldType.type, name='Active')
model = table_2.get_model(attribute_names=True)
model.objects.create(name='Tesla', active=True)
model.objects.create(name='SpaceX', active=False)
model.objects.create(name='Amazon', active=False)
def pre_delete(self, user, database):
"""
When a database is deleted we must also delete the related tables via the table
handler.
"""
database_tables = database.table_set.all().select_related('database__group')
table_handler = TableHandler()

View file

@ -18,10 +18,11 @@ class TextFieldType(FieldType):
serializer_field_names = ['text_default']
def get_serializer_field(self, instance, **kwargs):
return serializers.CharField(required=False, allow_blank=True, **kwargs)
return serializers.CharField(required=False, allow_null=True, allow_blank=True,
default=instance.text_default, **kwargs)
def get_model_field(self, instance, **kwargs):
return models.TextField(default=instance.text_default, null=True, blank=True,
return models.TextField(default=instance.text_default, blank=True, null=True,
**kwargs)
def random_value(self, instance, fake):
@ -37,15 +38,16 @@ class NumberFieldType(FieldType):
serializer_field_names = ['number_type', 'number_decimal_places', 'number_negative']
def prepare_value_for_db(self, instance, value):
if instance.number_type == NUMBER_TYPE_DECIMAL:
if value and instance.number_type == NUMBER_TYPE_DECIMAL:
value = Decimal(value)
if not instance.number_negative and value < 0:
if value and not instance.number_negative and value < 0:
raise ValidationError(f'The value for field {instance.id} cannot be '
f'negative.')
return value
def get_serializer_field(self, instance, **kwargs):
kwargs['required'] = False
kwargs['allow_null'] = True
if not instance.number_negative:
kwargs['min_value'] = 0
if instance.number_type == NUMBER_TYPE_INTEGER:
@ -89,7 +91,7 @@ class BooleanFieldType(FieldType):
model_class = BooleanField
def get_serializer_field(self, instance, **kwargs):
return serializers.BooleanField(required=False, **kwargs)
return serializers.BooleanField(required=False, default=False, **kwargs)
def get_model_field(self, instance, **kwargs):
return models.BooleanField(default=False, **kwargs)

View file

@ -11,27 +11,44 @@ def test_get_table_serializer(data_fixture):
text_default='white')
data_fixture.create_number_field(table=table, order=1, name='Horsepower')
data_fixture.create_boolean_field(table=table, order=2, name='For sale')
data_fixture.create_number_field(table=table, order=2, name='Price',
number_type='DECIMAL', number_negative=True,
number_decimal_places=2)
model = table.get_model(attribute_names=True)
serializer_class = get_row_serializer_class(model=model)
# expect the result to be empty if not provided
serializer_instance = serializer_class(data={})
assert serializer_instance.is_valid()
assert serializer_instance.data == {
'color': 'white',
'horsepower': None,
'for_sale': False,
'price': None
}
# text field
serializer_instance = serializer_class(data={'color': 'Green'})
assert serializer_instance.is_valid()
assert serializer_instance.data == {'color': 'Green'}
assert serializer_instance.data['color'] == 'Green'
serializer_instance = serializer_class(data={'color': 123})
assert serializer_instance.is_valid()
assert serializer_instance.data == {'color': '123'}
assert serializer_instance.data['color'] == '123'
serializer_instance = serializer_class(data={'color': None})
assert not serializer_instance.is_valid()
assert len(serializer_instance.errors['color']) == 1
assert serializer_instance.is_valid()
assert serializer_instance.data['color'] == None
# number field
serializer_instance = serializer_class(data={'horsepower': 120})
assert serializer_instance.is_valid()
assert serializer_instance.data == {'horsepower': 120}
assert serializer_instance.data['horsepower'] == 120
serializer_instance = serializer_class(data={'horsepower': None})
assert serializer_instance.is_valid()
assert serializer_instance.data['horsepower'] == None
serializer_instance = serializer_class(data={'horsepower': 'abc'})
assert not serializer_instance.is_valid()
@ -44,30 +61,63 @@ def test_get_table_serializer(data_fixture):
# boolean field
serializer_instance = serializer_class(data={'for_sale': True})
assert serializer_instance.is_valid()
assert serializer_instance.data == {'for_sale': True}
assert serializer_instance.data['for_sale'] == True
serializer_instance = serializer_class(data={'for_sale': False})
assert serializer_instance.is_valid()
assert serializer_instance.data['for_sale'] == False
serializer_instance = serializer_class(data={'for_sale': None})
assert not serializer_instance.is_valid()
assert len(serializer_instance.errors['for_sale']) == 1
# boolean field
serializer_instance = serializer_class(data={'for_sale': 'abc'})
assert not serializer_instance.is_valid()
assert len(serializer_instance.errors['for_sale']) == 1
# price field
serializer_instance = serializer_class(data={'price': 120})
assert serializer_instance.is_valid()
assert serializer_instance.data['price'] == '120.00'
serializer_instance = serializer_class(data={'price': '-10.22'})
assert serializer_instance.is_valid()
assert serializer_instance.data['price'] == '-10.22'
serializer_instance = serializer_class(data={'price': 'abc'})
assert not serializer_instance.is_valid()
assert len(serializer_instance.errors['price']) == 1
serializer_instance = serializer_class(data={'price': None})
assert serializer_instance.is_valid()
assert serializer_instance.data['price'] == None
# not existing value
serializer_instance = serializer_class(data={'NOT_EXISTING': True})
assert serializer_instance.is_valid()
assert serializer_instance.data == {}
assert serializer_instance.data == {
'color': 'white',
'horsepower': None,
'for_sale': False,
'price': None
}
# all fields
serializer_instance = serializer_class(data={
'color': 'green',
'horsepower': 120,
'for_sale': True
'for_sale': True,
'price': 120.22
})
assert serializer_instance.is_valid()
assert serializer_instance.data == {
'color': 'green',
'horsepower': 120,
'for_sale': True
'for_sale': True,
'price': '120.22'
}
# adding an extra field and only use that one.
price_field = data_fixture.create_number_field(
table=table_2, order=0, name='Sale price', number_type='DECIMAL',
number_decimal_places=3, number_negative=True

View file

@ -16,6 +16,8 @@ def test_create_row(api_client, data_fixture):
name='Horsepower')
boolean_field = data_fixture.create_boolean_field(table=table, order=2,
name='For sale')
text_field_2 = data_fixture.create_text_field(table=table, order=3,
name='Description')
response = api_client.post(
reverse('api_v0:database:rows:list', kwargs={'table_id': 99999}),
@ -39,7 +41,8 @@ def test_create_row(api_client, data_fixture):
{
f'field_{text_field.id}': 'Green',
f'field_{number_field.id}': -10,
f'field_{boolean_field.id}': None
f'field_{boolean_field.id}': None,
f'field_{text_field_2.id}': None
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
@ -61,39 +64,68 @@ def test_create_row(api_client, data_fixture):
assert response.status_code == 200
assert response_json_row_1[f'field_{text_field.id}'] == 'white'
assert not response_json_row_1[f'field_{number_field.id}']
assert not response_json_row_1[f'field_{boolean_field.id}']
assert response_json_row_1[f'field_{boolean_field.id}'] == False
assert response_json_row_1[f'field_{text_field_2.id}'] == None
response = api_client.post(
reverse('api_v0:database:rows:list', kwargs={'table_id': table.id}),
{
f'field_{text_field.id}': 'Green',
f'field_{number_field.id}': 120,
f'field_{boolean_field.id}': True
f'field_{number_field.id}': None,
f'field_{boolean_field.id}': False,
f'field_{text_field_2.id}': '',
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json_row_2 = response.json()
assert response.status_code == 200
assert response_json_row_2[f'field_{text_field.id}'] == 'Green'
assert response_json_row_2[f'field_{number_field.id}'] == 120
assert response_json_row_2[f'field_{boolean_field.id}']
assert response_json_row_2[f'field_{text_field.id}'] == 'white'
assert not response_json_row_2[f'field_{number_field.id}']
assert response_json_row_2[f'field_{boolean_field.id}'] == False
assert response_json_row_2[f'field_{text_field_2.id}'] == ''
response = api_client.post(
reverse('api_v0:database:rows:list', kwargs={'table_id': table.id}),
{
f'field_{text_field.id}': 'Green',
f'field_{number_field.id}': 120,
f'field_{boolean_field.id}': True,
f'field_{text_field_2.id}': 'Not important',
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json_row_3 = response.json()
assert response.status_code == 200
assert response_json_row_3[f'field_{text_field.id}'] == 'Green'
assert response_json_row_3[f'field_{number_field.id}'] == 120
assert response_json_row_3[f'field_{boolean_field.id}']
assert response_json_row_3[f'field_{text_field_2.id}'] == 'Not important'
model = table.get_model()
assert model.objects.all().count() == 2
assert model.objects.all().count() == 3
rows = model.objects.all().order_by('id')
row_1 = rows[0]
assert row_1.id == response_json_row_1['id']
assert getattr(row_1, f'field_{text_field.id}') == 'white'
assert not getattr(row_1, f'field_{number_field.id}')
assert not getattr(row_1, f'field_{boolean_field.id}')
assert getattr(row_1, f'field_{number_field.id}') == None
assert getattr(row_1, f'field_{boolean_field.id}') == False
assert getattr(row_1, f'field_{text_field_2.id}') == None
row_2 = rows[1]
assert row_2.id == response_json_row_2['id']
assert getattr(row_2, f'field_{text_field.id}') == 'Green'
assert getattr(row_2, f'field_{number_field.id}') == 120
assert getattr(row_2, f'field_{boolean_field.id}')
assert getattr(row_2, f'field_{text_field.id}') == 'white'
assert getattr(row_2, f'field_{number_field.id}') == None
assert getattr(row_2, f'field_{boolean_field.id}') == False
assert getattr(row_1, f'field_{text_field_2.id}') == None
row_3 = rows[2]
assert row_3.id == response_json_row_3['id']
assert getattr(row_3, f'field_{text_field.id}') == 'Green'
assert getattr(row_3, f'field_{number_field.id}') == 120
assert getattr(row_3, f'field_{boolean_field.id}') == True
assert getattr(row_3, f'field_{text_field_2.id}') == 'Not important'
@pytest.mark.django_db
@ -191,12 +223,12 @@ def test_update_row(api_client, data_fixture):
assert response_json_row_1['id'] == row_1.id
assert response_json_row_1[f'field_{text_field.id}'] == 'Green'
assert response_json_row_1[f'field_{number_field.id}'] == 120
assert response_json_row_1[f'field_{boolean_field.id}']
assert response_json_row_1[f'field_{boolean_field.id}'] == True
row_1.refresh_from_db()
assert getattr(row_1, f'field_{text_field.id}') == 'Green'
assert getattr(row_1, f'field_{number_field.id}') == 120
assert getattr(row_1, f'field_{boolean_field.id}')
assert getattr(row_1, f'field_{boolean_field.id}') == True
response = api_client.patch(
url,
@ -215,7 +247,7 @@ def test_update_row(api_client, data_fixture):
row_1.refresh_from_db()
assert getattr(row_1, f'field_{text_field.id}') == 'Orange'
assert getattr(row_1, f'field_{number_field.id}') == 120
assert getattr(row_1, f'field_{boolean_field.id}')
assert getattr(row_1, f'field_{boolean_field.id}') == True
url = reverse('api_v0:database:rows:item', kwargs={
'table_id': table.id,
@ -236,12 +268,38 @@ def test_update_row(api_client, data_fixture):
assert response_json_row_2['id'] == row_2.id
assert response_json_row_2[f'field_{text_field.id}'] == 'Blue'
assert response_json_row_2[f'field_{number_field.id}'] == 50
assert not response_json_row_2[f'field_{boolean_field.id}']
assert response_json_row_2[f'field_{boolean_field.id}'] == False
row_2.refresh_from_db()
assert getattr(row_2, f'field_{text_field.id}') == 'Blue'
assert getattr(row_2, f'field_{number_field.id}') == 50
assert not getattr(row_2, f'field_{boolean_field.id}')
assert getattr(row_2, f'field_{boolean_field.id}') == False
url = reverse('api_v0:database:rows:item', kwargs={
'table_id': table.id,
'row_id': row_2.id
})
response = api_client.patch(
url,
{
f'field_{text_field.id}': None,
f'field_{number_field.id}': None,
f'field_{boolean_field.id}': False
},
format='json',
HTTP_AUTHORIZATION=f'JWT {token}'
)
response_json_row_2 = response.json()
assert response.status_code == 200
assert response_json_row_2['id'] == row_2.id
assert response_json_row_2[f'field_{text_field.id}'] == None
assert response_json_row_2[f'field_{number_field.id}'] == None
assert response_json_row_2[f'field_{boolean_field.id}'] == False
row_2.refresh_from_db()
assert getattr(row_2, f'field_{text_field.id}') == None
assert getattr(row_2, f'field_{number_field.id}') == None
assert getattr(row_2, f'field_{boolean_field.id}') == False
table_3 = data_fixture.create_database_table(user=user)
decimal_field = data_fixture.create_number_field(

View file

@ -167,7 +167,7 @@ def test_update_table(api_client, data_fixture):
@pytest.mark.django_db
def test_delete_group(api_client, data_fixture):
def test_delete_table(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table_1 = data_fixture.create_database_table(user=user)
table_2 = data_fixture.create_database_table()

View file

@ -50,13 +50,11 @@ def test_get_table_model(data_fixture):
assert color_field.db_column == f'field_{text_field.id}'
assert color_field.default == 'white'
assert color_field.null
assert color_field.blank
assert isinstance(horsepower_field, models.IntegerField)
assert horsepower_field.verbose_name == 'Horsepower'
assert horsepower_field.db_column == f'field_{number_field.id}'
assert horsepower_field.null
assert horsepower_field.blank
assert isinstance(for_sale_field, models.BooleanField)
assert for_sale_field.verbose_name == 'For sale'
@ -73,7 +71,6 @@ def test_get_table_model(data_fixture):
assert isinstance(sale_price_field, models.DecimalField)
assert sale_price_field.decimal_places == 3
assert sale_price_field.null
assert sale_price_field.blank
model_2 = table.get_model(fields=[number_field], field_ids=[text_field.id],
attribute_names=True)

View file

@ -24,14 +24,18 @@ def test_create_user():
assert group.name == "Test1's group"
assert Database.objects.all().count() == 1
assert Table.objects.all().count() == 1
assert GridView.objects.all().count() == 1
assert TextField.objects.all().count() == 2
assert BooleanField.objects.all().count() == 1
assert Table.objects.all().count() == 2
assert GridView.objects.all().count() == 2
assert TextField.objects.all().count() == 3
assert BooleanField.objects.all().count() == 2
table = Table.objects.first()
model = table.get_model()
assert model.objects.all().count() == 4
tables = Table.objects.all().order_by('id')
model_1 = tables[0].get_model()
assert model_1.objects.all().count() == 4
model_2 = tables[1].get_model()
assert model_2.objects.all().count() == 3
with pytest.raises(UserAlreadyExist):
user_handler.create_user('Test1', 'test@test.nl', 'password')

View file

@ -1,18 +1,11 @@
import { Registerable } from '@baserow/modules/core/registry'
import ApplicationForm from '@baserow/modules/core/components/application/ApplicationForm'
/**
* The application type base class that can be extended when creating a plugin
* for the frontend.
*/
export class ApplicationType {
/**
* Must return a string with the unique name, this must be the same as the
* type used in the backend.
*/
getType() {
return null
}
export class ApplicationType extends Registerable {
/**
* The font awesome 5 icon name that is used as convenience for the user to
* recognize certain application types. If you for example want the database
@ -58,6 +51,7 @@ export class ApplicationType {
}
constructor() {
super()
this.type = this.getType()
this.iconClass = this.getIconClass()
this.name = this.getName()

View file

@ -24,8 +24,6 @@
</template>
<script>
import { mapState } from 'vuex'
import CreateApplicationModal from '@baserow/modules/core/components/application/CreateApplicationModal'
import context from '@baserow/modules/core/mixins/context'
@ -42,9 +40,9 @@ export default {
},
},
computed: {
...mapState({
applications: (state) => state.application.types,
}),
applications() {
return this.$registry.getAll('application')
},
},
methods: {
toggleCreateApplicationModal(type) {

View file

@ -97,7 +97,7 @@ export default {
},
methods: {
selectApplication(application) {
const type = this.$store.getters['application/getType'](application.type)
const type = this.$registry.get('application', application.type)
type.select(application, this)
},
},

View file

@ -139,7 +139,7 @@ export default {
this.setLoading(application, false)
},
getSelectedApplicationComponent(application) {
const type = this.$store.getters['application/getType'](application.type)
const type = this.$registry.get('application', application.type)
return type.getSelectedSidebarComponent()
},
},

View file

@ -1,10 +1,14 @@
import { Registry } from '@baserow/modules/core/registry'
import applicationStore from '@baserow/modules/core/store/application'
import authStore from '@baserow/modules/core/store/auth'
import groupStore from '@baserow/modules/core/store/group'
import notificationStore from '@baserow/modules/core/store/notification'
import sidebarStore from '@baserow/modules/core/store/sidebar'
export default ({ store }) => {
export default ({ store }, inject) => {
inject('registry', new Registry())
store.registerModule('application', applicationStore)
store.registerModule('auth', authStore)
store.registerModule('group', groupStore)

View file

@ -0,0 +1,90 @@
/**
* Only instances that are children of a Registerable can be registered into the
* registry.
*/
export class Registerable {
/**
* Must return a string with the unique name, this must be the same as the
* type used in the backend.
*/
static getType() {
throw new Error('The type of a registry must be set.')
}
getType() {
return this.constructor.getType()
}
}
/**
* The registry is an class where Registerable instances can be registered under a
* namespace. This is used for plugins to register extra functionality to Baserow. For
* example the database plugin registers itself as an application to the core, but
* it is also possible to register fields and views to the database plugin.
*/
export class Registry {
constructor() {
this.registry = {}
}
/**
* Registers a new Registerable object under the provided namespace in the registry.
* If the namespace doesn't exist it will be created. It is common to register
* instantiated classes here.
*/
register(namespace, object) {
if (!(object instanceof Registerable)) {
throw new TypeError(
'The registered object must be an instance of Registrable.'
)
}
const type = object.getType()
if (!Object.prototype.hasOwnProperty.call(this.registry, namespace)) {
this.registry[namespace] = {}
}
this.registry[namespace][type] = object
}
/**
* Returns a registered object with the given type in the provided namespace.
*/
get(namespace, type) {
if (!Object.prototype.hasOwnProperty.call(this.registry, namespace)) {
throw new Error(
`The namespace ${namespace} is not found in the registry.`
)
}
if (!Object.prototype.hasOwnProperty.call(this.registry[namespace], type)) {
throw new Error(
`The type ${type} is not found under namespace ${namespace} in the registry.`
)
}
return this.registry[namespace][type]
}
/**
* Returns all the objects that are in the given namespace.
*/
getAll(namespace) {
if (!Object.prototype.hasOwnProperty.call(this.registry, namespace)) {
throw new Error(
`The namespace ${namespace} is not found in the registry.`
)
}
return this.registry[namespace]
}
/**
* Returns true if the object of the given type exists in the namespace.
*/
exists(namespace, type) {
if (!Object.prototype.hasOwnProperty.call(this.registry, namespace)) {
return false
}
if (!Object.prototype.hasOwnProperty.call(this.registry[namespace], type)) {
return false
}
return true
}
}

View file

@ -1,9 +1,8 @@
import { ApplicationType } from '@baserow/modules/core/applicationTypes'
import ApplicationService from '@baserow/modules/core/services/application'
import { clone } from '@baserow/modules/core/utils/object'
function populateApplication(application, getters) {
const type = getters.getType(application.type)
function populateApplication(application, registry) {
const type = registry.get('application', application.type)
application._ = {
type: type.serialize(),
@ -14,7 +13,6 @@ function populateApplication(application, getters) {
}
export const state = () => ({
types: {},
loading: false,
loaded: false,
items: [],
@ -22,9 +20,6 @@ export const state = () => ({
})
export const mutations = {
REGISTER(state, application) {
state.types[application.type] = application
},
SET_ITEMS(state, applications) {
state.items = applications
},
@ -67,19 +62,6 @@ export const mutations = {
}
export const actions = {
/**
* Register a new application within the registry. The is commonly used when
* creating an extension.
*/
register({ commit, getters }, application) {
if (!(application instanceof ApplicationType)) {
throw new TypeError(
'The application must be an instance of ApplicationType.'
)
}
commit('REGISTER', application)
},
/**
* Changes the loading state of a specific item.
*/
@ -95,7 +77,7 @@ export const actions = {
try {
const { data } = await ApplicationService.fetchAll()
data.forEach((part, index, d) => {
populateApplication(data[index], getters)
populateApplication(data[index], this.$registry)
})
commit('SET_ITEMS', data)
commit('SET_LOADING', false)
@ -122,7 +104,7 @@ export const actions = {
*/
clearChildrenSelected({ commit, getters }) {
Object.values(getters.getAll).forEach((application) => {
const type = getters.getType(application.type)
const type = this.$registry.get('application', application.type)
commit('CLEAR_CHILDREN_SELECTED', { type, application })
})
},
@ -141,7 +123,7 @@ export const actions = {
)
}
if (!getters.typeExists(type)) {
if (!this.$registry.exists('application', type)) {
throw new Error(`An application type with type "${type}" doesn't exist.`)
}
@ -149,7 +131,7 @@ export const actions = {
postData.type = type
const { data } = await ApplicationService.create(group.id, postData)
populateApplication(data, getters)
populateApplication(data, this.$registry)
commit('ADD_ITEM', data)
},
/**
@ -170,7 +152,7 @@ export const actions = {
async delete({ commit, dispatch, getters }, application) {
try {
await ApplicationService.delete(application.id)
const type = getters.getType(application.type)
const type = this.$registry.get('application', application.type)
type.delete(application, this)
commit('DELETE_ITEM', application.id)
} catch (error) {
@ -224,15 +206,6 @@ export const getters = {
(application) => application.group.id === group.id
)
},
typeExists: (state) => (type) => {
return Object.prototype.hasOwnProperty.call(state.types, type)
},
getType: (state) => (type) => {
if (!Object.prototype.hasOwnProperty.call(state.types, type)) {
throw new Error(`An application type with type "${type}" doesn't exist.`)
}
return state.types[type]
},
}
export default {

View file

@ -149,7 +149,7 @@ export const actions = {
forceDelete({ commit, dispatch, rootGetters }, group) {
const applications = rootGetters['application/getAllOfGroup'](group)
applications.forEach((application) => {
const type = rootGetters['application/getType'](application.type)
const type = this.$registry.get('application', application.type)
type.delete(application, this)
})

View file

@ -7,10 +7,6 @@ export class DatabaseApplicationType extends ApplicationType {
return 'database'
}
getType() {
return DatabaseApplicationType.getType()
}
getIconClass() {
return 'database'
}

View file

@ -48,7 +48,6 @@
</template>
<script>
import { mapState } from 'vuex'
import { required } from 'vuelidate/lib/validators'
import form from '@baserow/modules/core/mixins/form'
@ -67,9 +66,9 @@ export default {
}
},
computed: {
...mapState({
fieldTypes: (state) => state.field.types,
}),
fieldTypes() {
return this.$registry.getAll('field')
},
},
validations: {
values: {
@ -79,7 +78,7 @@ export default {
},
methods: {
getFormComponent(type) {
return this.$store.getters['field/getType'](type).getFormComponent()
return this.$registry.get('field', type).getFormComponent()
},
},
}

View file

@ -78,10 +78,12 @@ export default {
}
},
computed: {
viewTypes() {
return this.$registry.getAll('view')
},
...mapState({
isLoading: (state) => state.view.loading,
isLoaded: (state) => state.view.loaded,
viewTypes: (state) => state.view.types,
views: (state) => state.view.items,
}),
},

View file

@ -55,27 +55,27 @@ export default {
},
methods: {
getFieldComponent(type) {
return this.$store.getters['field/getType'](
type
).getGridViewFieldComponent()
return this.$registry.get('field', type).getGridViewFieldComponent()
},
/**
* If the grid field component emits an update event this method will be called
* which will actually update the value via the store.
*/
async update(value, oldValue) {
try {
await this.$store.dispatch('view/grid/updateValue', {
update(value, oldValue) {
this.$store
.dispatch('view/grid/updateValue', {
table: this.table,
row: this.row,
field: this.field,
value,
oldValue,
})
} catch (error) {
this.$forceUpdate()
notifyIf(error, 'column')
}
.catch((error) => {
notifyIf(error, 'column')
})
.then(() => {
this.$forceUpdate()
})
// This is needed because in some cases we do have a value yet, so a watcher of
// the value is not guaranteed. This will make sure the component shows the

View file

@ -1,3 +1,5 @@
import { Registerable } from '@baserow/modules/core/registry'
import FieldNumberSubForm from '@baserow/modules/database/components/field/FieldNumberSubForm'
import FieldTextSubForm from '@baserow/modules/database/components/field/FieldTextSubForm'
@ -5,15 +7,7 @@ import GridViewFieldText from '@baserow/modules/database/components/view/grid/Gr
import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/GridViewFieldNumber'
import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/GridViewFieldBoolean'
export class FieldType {
/**
* Must return a string with the unique name, this must be the same as the
* type used in the backend.
*/
getType() {
return null
}
export class FieldType extends Registerable {
/**
* The font awesome 5 icon name that is used as convenience for the user to
* recognize certain view types. If you for example want the database
@ -60,6 +54,7 @@ export class FieldType {
}
constructor() {
super()
this.type = this.getType()
this.iconClass = this.getIconClass()
this.name = this.getName()
@ -102,10 +97,6 @@ export class TextFieldType extends FieldType {
return 'text'
}
getType() {
return TextFieldType.getType()
}
getIconClass() {
return 'font'
}
@ -121,6 +112,10 @@ export class TextFieldType extends FieldType {
getGridViewFieldComponent() {
return GridViewFieldText
}
getEmptyValue(field) {
return field.text_default
}
}
export class NumberFieldType extends FieldType {
@ -128,10 +123,6 @@ export class NumberFieldType extends FieldType {
return 'number'
}
getType() {
return NumberFieldType.getType()
}
getIconClass() {
return 'hashtag'
}
@ -154,10 +145,6 @@ export class BooleanFieldType extends FieldType {
return 'boolean'
}
getType() {
return BooleanFieldType.getType()
}
getIconClass() {
return 'check-square'
}

View file

@ -73,7 +73,7 @@ export default {
* Prepares all the table, field and view data for the provided database, table and
* view id.
*/
async asyncData({ store, params, error }) {
async asyncData({ store, params, error, app }) {
// @TODO figure out why the id's aren't converted to an int in the route.
const databaseId = parseInt(params.databaseId)
const tableId = parseInt(params.tableId)
@ -105,7 +105,7 @@ export default {
// It might be possible that the view also has some stores that need to be
// filled with initial data so we're going to call the fetch function here.
const type = store.getters['view/getType'](view.type)
const type = app.$registry.get('view', view.type)
await type.fetch({ store }, view)
} catch {
return error({ statusCode: 404, message: 'View not found.' })
@ -130,11 +130,11 @@ export default {
},
methods: {
getViewComponent(view) {
const type = this.$store.getters['view/getType'](view.type)
const type = this.$registry.get('view', view.type)
return type.getComponent()
},
getViewHeaderComponent(view) {
const type = this.$store.getters['view/getType'](view.type)
const type = this.$registry.get('view', view.type)
return type.getHeaderComponent()
},
},

View file

@ -11,27 +11,15 @@ import viewStore from '@baserow/modules/database/store/view'
import fieldStore from '@baserow/modules/database/store/field'
import gridStore from '@baserow/modules/database/store/view/grid'
/**
* Note that this method is actually called on the server and client side, but
* for registering the application this is intentional. The table and view
* types must actually be registered on both sides. Since we're adding an
* initialized class object, which cannot be stringified we need to override the
* object on the client side. Because both register actions don't check if the
* type already exists, but just overrides this works for now. In the future we
* must find a way to properly serialize the object and pass it from the server
* to the client in order to get rid off the warning 'Cannot stringify arbitrary
* non-POJOs DatabaseApplicationType' on the server side. There is an issue for
* that on the backlog with id 15.
*/
export default ({ store }) => {
export default ({ store, app }) => {
store.registerModule('table', tableStore)
store.registerModule('view', viewStore)
store.registerModule('field', fieldStore)
store.registerModule('view/grid', gridStore)
store.dispatch('application/register', new DatabaseApplicationType())
store.dispatch('view/register', new GridViewType())
store.dispatch('field/register', new TextFieldType())
store.dispatch('field/register', new NumberFieldType())
store.dispatch('field/register', new BooleanFieldType())
app.$registry.register('application', new DatabaseApplicationType())
app.$registry.register('view', new GridViewType())
app.$registry.register('field', new TextFieldType())
app.$registry.register('field', new NumberFieldType())
app.$registry.register('field', new BooleanFieldType())
}

View file

@ -1,9 +1,8 @@
import { FieldType } from '@baserow/modules/database/fieldTypes'
import FieldService from '@baserow/modules/database/services/field'
import { clone } from '@baserow/modules/core/utils/object'
export function populateField(field, getters) {
const type = getters.getType(field.type)
export function populateField(field, registry) {
const type = registry.get('field', field.type)
field._ = {
type: type.serialize(),
@ -21,9 +20,6 @@ export const state = () => ({
})
export const mutations = {
REGISTER(state, field) {
state.types[field.type] = field
},
SET_ITEMS(state, applications) {
state.items = applications
},
@ -69,17 +65,6 @@ export const mutations = {
}
export const actions = {
/**
* Register a new field type with the registry. This is used for creating a new
* field type that users can create.
*/
register({ commit, getters }, field) {
if (!(field instanceof FieldType)) {
throw new TypeError('The field must be an instance of fieldType.')
}
commit('REGISTER', field)
},
/**
* Changes the loading state of a specific field.
*/
@ -98,7 +83,7 @@ export const actions = {
try {
const { data } = await FieldService.fetchAll(table.id)
data.forEach((part, index, d) => {
populateField(data[index], getters)
populateField(data[index], this.$registry)
})
const primaryIndex = data.findIndex((item) => item.primary === true)
@ -119,10 +104,9 @@ export const actions = {
/**
* Creates a new field with the provided type for the given table.
*/
async create(
{ commit, getters, rootGetters, dispatch },
{ type, table, values }
) {
async create(context, { type, table, values }) {
const { commit } = context
if (Object.prototype.hasOwnProperty.call(values, 'type')) {
throw new Error(
'The key "type" is a reserved, but is already set on the ' +
@ -130,16 +114,25 @@ export const actions = {
)
}
if (!getters.typeExists(type)) {
if (!this.$registry.exists('field', type)) {
throw new Error(`A field with type "${type}" doesn't exist.`)
}
const fieldType = this.$registry.get('field', type)
const postData = clone(values)
postData.type = type
let { data } = await FieldService.create(table.id, postData)
data = populateField(data, getters)
data = populateField(data, this.$registry)
commit('ADD_ITEM', data)
// Call the field created event on all the registered views because they might
// 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.
Object.values(this.$registry.getAll('view')).forEach((viewType) => {
viewType.fieldCreated(context, table, data, fieldType)
})
},
/**
* Updates the values of the provided field.
@ -152,7 +145,7 @@ export const actions = {
)
}
if (!getters.typeExists(type)) {
if (!this.$registry.exists('field', type)) {
throw new Error(`A field with type "${type}" doesn't exist.`)
}
@ -160,7 +153,7 @@ export const actions = {
postData.type = type
let { data } = await FieldService.update(field.id, postData)
data = populateField(data, getters)
data = populateField(data, this.$registry)
if (field.primary) {
commit('SET_PRIMARY', data)
} else {
@ -199,15 +192,6 @@ export const getters = {
get: (state) => (id) => {
return state.items.find((item) => item.id === id)
},
typeExists: (state) => (type) => {
return Object.prototype.hasOwnProperty.call(state.types, type)
},
getType: (state) => (type) => {
if (!Object.prototype.hasOwnProperty.call(state.types, type)) {
throw new Error(`A field with type "${type}" doesn't exist.`)
}
return state.types[type]
},
}
export default {

View file

@ -1,10 +1,9 @@
import { ViewType } from '@baserow/modules/database/viewTypes'
import ViewService from '@baserow/modules/database/services/view'
import { clone } from '@baserow/modules/core/utils/object'
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
export function populateView(view, getters) {
const type = getters.getType(view.type)
export function populateView(view, registry) {
const type = registry.get('view', view.type)
view._ = {
type: type.serialize(),
@ -23,9 +22,6 @@ export const state = () => ({
})
export const mutations = {
REGISTER(state, view) {
state.types[view.type] = view
},
SET_ITEMS(state, applications) {
state.items = applications
},
@ -68,17 +64,6 @@ export const mutations = {
}
export const actions = {
/**
* Register a new view type with the registry. This is used for creating a new
* view type that users can create.
*/
register({ dispatch, commit }, view) {
if (!(view instanceof ViewType)) {
throw new TypeError('The view must be an instance of ViewType.')
}
commit('REGISTER', view)
},
/**
* Changes the loading state of a specific view.
*/
@ -97,7 +82,7 @@ export const actions = {
try {
const { data } = await ViewService.fetchAll(table.id)
data.forEach((part, index, d) => {
populateView(data[index], getters)
populateView(data[index], this.$registry)
})
commit('SET_ITEMS', data)
commit('SET_LOADING', false)
@ -123,7 +108,7 @@ export const actions = {
)
}
if (!getters.typeExists(type)) {
if (!this.$registry.exists('view', type)) {
throw new Error(`A view with type "${type}" doesn't exist.`)
}
@ -131,7 +116,7 @@ export const actions = {
postData.type = type
const { data } = await ViewService.create(table.id, postData)
populateView(data, getters)
populateView(data, this.$registry)
commit('ADD_ITEM', data)
},
/**
@ -229,15 +214,6 @@ export const getters = {
get: (state) => (id) => {
return state.items.find((item) => item.id === id)
},
typeExists: (state) => (type) => {
return Object.prototype.hasOwnProperty.call(state.types, type)
},
getType: (state) => (type) => {
if (!Object.prototype.hasOwnProperty.call(state.types, type)) {
throw new Error(`A view with type "${type}" doesn't exist.`)
}
return state.types[type]
},
}
export default {

View file

@ -1,4 +1,5 @@
import axios from 'axios'
import _ from 'lodash'
import GridService from '@baserow/modules/database/services/view/grid'
import RowService from '@baserow/modules/database/services/row'
@ -106,6 +107,12 @@ export const mutations = {
SET_VALUE(state, { row, field, value }) {
row[`field_${field.id}`] = value
},
ADD_FIELD(state, { field, value }) {
const name = `field_${field.id}`
state.rows.forEach((row) => {
row[name] = value
})
},
}
// Contains the timeout needed for the delayed delayed scroll top action.
@ -414,22 +421,21 @@ export const actions = {
fields.forEach((field) => {
const name = `field_${field.id}`
if (!(name in values)) {
const fieldType = rootGetters['field/getType'](field._.type.type)
const fieldType = this.$registry.get('field', field._.type.type)
const empty = fieldType.getEmptyValue(field)
if (empty !== null) {
values[name] = fieldType.getEmptyValue(field)
}
values[name] = empty
}
})
// Populate the row and set the loading state to indicate that the row has not
// yet been added.
populateRow(values)
values.id = 0
values._.loading = true
const row = _.assign({}, values)
populateRow(row)
row.id = 0
row._.loading = true
commit('ADD_ROWS', {
rows: [values],
rows: [row],
prependToRows: 0,
appendToRows: 1,
count: getters.getCount + 1,
@ -446,6 +452,12 @@ export const actions = {
const { data } = await RowService.create(table.id, values)
commit('FINALIZE_ROW', { index, id: data.id })
},
/**
* Adds a field with a provided value to the rows in memory.
*/
addField({ commit }, { field, value = null }) {
commit('ADD_FIELD', { field, value })
},
}
export const getters = {

View file

@ -1,15 +1,8 @@
import { Registerable } from '@baserow/modules/core/registry'
import ViewForm from '@baserow/modules/database/components/view/ViewForm'
import GridView from '@baserow/modules/database/components/view/grid/GridView'
export class ViewType {
/**
* Must return a string with the unique name, this must be the same as the
* type used in the backend.
*/
getType() {
return null
}
export class ViewType extends Registerable {
/**
* The font awesome 5 icon name that is used as convenience for the user to
* recognize certain view types. If you for example want the database
@ -28,6 +21,7 @@ export class ViewType {
}
constructor() {
super()
this.type = this.getType()
this.iconClass = this.getIconClass()
this.name = this.getName()
@ -85,6 +79,12 @@ export class ViewType {
*/
fetch() {}
/**
* 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) {}
/**
* @return object
*/
@ -102,10 +102,6 @@ export class GridViewType extends ViewType {
return 'grid'
}
getType() {
return GridViewType.getType()
}
getIconClass() {
return 'th'
}
@ -121,4 +117,9 @@ export class GridViewType extends ViewType {
async fetch({ store }, view) {
await store.dispatch('view/grid/fetchInitial', { gridId: view.id })
}
fieldCreated({ dispatch }, table, field, fieldType) {
const value = fieldType.getEmptyValue(field)
dispatch('view/grid/addField', { field, value }, { root: true })
}
}