diff --git a/backend/src/baserow/contrib/database/application_types.py b/backend/src/baserow/contrib/database/application_types.py index d64d774ba..479c50433 100644 --- a/backend/src/baserow/contrib/database/application_types.py +++ b/backend/src/baserow/contrib/database/application_types.py @@ -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() diff --git a/backend/src/baserow/contrib/database/fields/field_types.py b/backend/src/baserow/contrib/database/fields/field_types.py index 38141a115..d5131f46c 100644 --- a/backend/src/baserow/contrib/database/fields/field_types.py +++ b/backend/src/baserow/contrib/database/fields/field_types.py @@ -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) diff --git a/backend/tests/baserow/contrib/database/api/v0/rows/test_row_serializers.py b/backend/tests/baserow/contrib/database/api/v0/rows/test_row_serializers.py index a5db71f83..6c970332f 100644 --- a/backend/tests/baserow/contrib/database/api/v0/rows/test_row_serializers.py +++ b/backend/tests/baserow/contrib/database/api/v0/rows/test_row_serializers.py @@ -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 diff --git a/backend/tests/baserow/contrib/database/api/v0/rows/test_row_views.py b/backend/tests/baserow/contrib/database/api/v0/rows/test_row_views.py index 996e459d8..ebf937c0a 100644 --- a/backend/tests/baserow/contrib/database/api/v0/rows/test_row_views.py +++ b/backend/tests/baserow/contrib/database/api/v0/rows/test_row_views.py @@ -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( diff --git a/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py index 631dfb1ad..83621ffef 100644 --- a/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py +++ b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py @@ -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() diff --git a/backend/tests/baserow/contrib/database/table/test_table_models.py b/backend/tests/baserow/contrib/database/table/test_table_models.py index dcd81127c..a3247aebf 100644 --- a/backend/tests/baserow/contrib/database/table/test_table_models.py +++ b/backend/tests/baserow/contrib/database/table/test_table_models.py @@ -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) diff --git a/backend/tests/baserow/user/test_user_handler.py b/backend/tests/baserow/user/test_user_handler.py index ae798a868..c8661f6e5 100644 --- a/backend/tests/baserow/user/test_user_handler.py +++ b/backend/tests/baserow/user/test_user_handler.py @@ -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') diff --git a/web-frontend/modules/core/applicationTypes.js b/web-frontend/modules/core/applicationTypes.js index 632685f79..ce7d152ce 100644 --- a/web-frontend/modules/core/applicationTypes.js +++ b/web-frontend/modules/core/applicationTypes.js @@ -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() diff --git a/web-frontend/modules/core/components/application/CreateApplicationContext.vue b/web-frontend/modules/core/components/application/CreateApplicationContext.vue index 70705456c..e3351d7f9 100644 --- a/web-frontend/modules/core/components/application/CreateApplicationContext.vue +++ b/web-frontend/modules/core/components/application/CreateApplicationContext.vue @@ -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) { diff --git a/web-frontend/modules/core/components/group/DashboardGroup.vue b/web-frontend/modules/core/components/group/DashboardGroup.vue index f15af26df..75ab50dd8 100644 --- a/web-frontend/modules/core/components/group/DashboardGroup.vue +++ b/web-frontend/modules/core/components/group/DashboardGroup.vue @@ -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) }, }, diff --git a/web-frontend/modules/core/components/sidebar/SidebarApplication.vue b/web-frontend/modules/core/components/sidebar/SidebarApplication.vue index 81125461a..7fb7e4b11 100644 --- a/web-frontend/modules/core/components/sidebar/SidebarApplication.vue +++ b/web-frontend/modules/core/components/sidebar/SidebarApplication.vue @@ -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() }, }, diff --git a/web-frontend/modules/core/plugin.js b/web-frontend/modules/core/plugin.js index 9ad5e8ceb..0e605a649 100644 --- a/web-frontend/modules/core/plugin.js +++ b/web-frontend/modules/core/plugin.js @@ -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) diff --git a/web-frontend/modules/core/registry.js b/web-frontend/modules/core/registry.js new file mode 100644 index 000000000..5616b2c0e --- /dev/null +++ b/web-frontend/modules/core/registry.js @@ -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 + } +} diff --git a/web-frontend/modules/core/store/application.js b/web-frontend/modules/core/store/application.js index c0b963002..174d7ca5a 100644 --- a/web-frontend/modules/core/store/application.js +++ b/web-frontend/modules/core/store/application.js @@ -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 { diff --git a/web-frontend/modules/core/store/group.js b/web-frontend/modules/core/store/group.js index 5cda83c7f..2d3de3a30 100644 --- a/web-frontend/modules/core/store/group.js +++ b/web-frontend/modules/core/store/group.js @@ -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) }) diff --git a/web-frontend/modules/database/applicationTypes.js b/web-frontend/modules/database/applicationTypes.js index b8b1ef2a2..09a1acc42 100644 --- a/web-frontend/modules/database/applicationTypes.js +++ b/web-frontend/modules/database/applicationTypes.js @@ -7,10 +7,6 @@ export class DatabaseApplicationType extends ApplicationType { return 'database' } - getType() { - return DatabaseApplicationType.getType() - } - getIconClass() { return 'database' } diff --git a/web-frontend/modules/database/components/field/FieldForm.vue b/web-frontend/modules/database/components/field/FieldForm.vue index 2044e6797..c357e8512 100644 --- a/web-frontend/modules/database/components/field/FieldForm.vue +++ b/web-frontend/modules/database/components/field/FieldForm.vue @@ -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() }, }, } diff --git a/web-frontend/modules/database/components/view/ViewsContext.vue b/web-frontend/modules/database/components/view/ViewsContext.vue index f913ca53d..5e4620c64 100644 --- a/web-frontend/modules/database/components/view/ViewsContext.vue +++ b/web-frontend/modules/database/components/view/ViewsContext.vue @@ -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, }), }, diff --git a/web-frontend/modules/database/components/view/grid/GridViewField.vue b/web-frontend/modules/database/components/view/grid/GridViewField.vue index 1fada7914..ef44ce6de 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewField.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewField.vue @@ -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 diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js index e0398544a..76534f361 100644 --- a/web-frontend/modules/database/fieldTypes.js +++ b/web-frontend/modules/database/fieldTypes.js @@ -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' } diff --git a/web-frontend/modules/database/pages/table.vue b/web-frontend/modules/database/pages/table.vue index 7415cef1f..af7613118 100644 --- a/web-frontend/modules/database/pages/table.vue +++ b/web-frontend/modules/database/pages/table.vue @@ -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() }, }, diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js index da1771939..e1097b334 100644 --- a/web-frontend/modules/database/plugin.js +++ b/web-frontend/modules/database/plugin.js @@ -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()) } diff --git a/web-frontend/modules/database/store/field.js b/web-frontend/modules/database/store/field.js index 592c2bd12..c45c943eb 100644 --- a/web-frontend/modules/database/store/field.js +++ b/web-frontend/modules/database/store/field.js @@ -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 { diff --git a/web-frontend/modules/database/store/view.js b/web-frontend/modules/database/store/view.js index 650dc0472..8f24da562 100644 --- a/web-frontend/modules/database/store/view.js +++ b/web-frontend/modules/database/store/view.js @@ -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 { diff --git a/web-frontend/modules/database/store/view/grid.js b/web-frontend/modules/database/store/view/grid.js index 52b05a8ff..27011c719 100644 --- a/web-frontend/modules/database/store/view/grid.js +++ b/web-frontend/modules/database/store/view/grid.js @@ -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 = { diff --git a/web-frontend/modules/database/viewTypes.js b/web-frontend/modules/database/viewTypes.js index c27468644..598c16320 100644 --- a/web-frontend/modules/database/viewTypes.js +++ b/web-frontend/modules/database/viewTypes.js @@ -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 }) + } }