diff --git a/backend/src/baserow/contrib/integrations/local_baserow/api/serializers.py b/backend/src/baserow/contrib/integrations/local_baserow/api/serializers.py index 2c0714a6b..c7c58179d 100644 --- a/backend/src/baserow/contrib/integrations/local_baserow/api/serializers.py +++ b/backend/src/baserow/contrib/integrations/local_baserow/api/serializers.py @@ -38,4 +38,7 @@ class LocalBaserowTableServiceFieldMappingSerializer(serializers.Serializer): field_id = serializers.IntegerField( help_text="The primary key of the associated database table field." ) + enabled = serializers.BooleanField( + help_text="Indicates whether the field mapping is enabled or not." + ) value = FormulaSerializerField(allow_blank=True) diff --git a/backend/src/baserow/contrib/integrations/local_baserow/models.py b/backend/src/baserow/contrib/integrations/local_baserow/models.py index 52eb61441..9fa828e2f 100644 --- a/backend/src/baserow/contrib/integrations/local_baserow/models.py +++ b/backend/src/baserow/contrib/integrations/local_baserow/models.py @@ -212,6 +212,12 @@ class LocalBaserowTableServiceFieldMapping(models.Model): on_delete=models.CASCADE, help_text="The Baserow field that this mapping relates to.", ) + enabled = models.BooleanField( + null=True, # TODO zdm remove me after v1.27 + default=True, + help_text="Indicates if the field mapping is enabled. If it is disabled, " + "we will not use the `value` when creating and updating rows.", + ) value = FormulaField(default="", help_text="The field mapping's value.") service = models.ForeignKey( Service, diff --git a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py index 537458a91..aacffb0ac 100644 --- a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py +++ b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py @@ -1261,7 +1261,10 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType): bulk_field_mappings.append( LocalBaserowTableServiceFieldMapping( - field=field, service=instance, value=field_mapping["value"] + field=field, + service=instance, + enabled=field_mapping["enabled"], + value=field_mapping["value"], ) ) LocalBaserowTableServiceFieldMapping.objects.bulk_create( @@ -1286,6 +1289,7 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType): { "field_id": m.field_id, "value": m.value, + "enabled": m.enabled, } for m in service.field_mappings.all() ] @@ -1321,6 +1325,7 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType): if prop_name == "field_mappings": return [ { + "enabled": item["enabled"], "value": import_formula(item["value"], id_mapping, **kwargs), "field_id": ( id_mapping["database_fields"][item["field_id"]] @@ -1490,7 +1495,9 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType): integration = service.integration.specific field_values = {} - field_mappings = service.field_mappings.select_related("field").all() + field_mappings = service.field_mappings.select_related("field").filter( + enabled=True + ) for field_mapping in field_mappings: if field_mapping.id not in resolved_values: diff --git a/backend/src/baserow/contrib/integrations/migrations/0007_field_mapping_enabled.py b/backend/src/baserow/contrib/integrations/migrations/0007_field_mapping_enabled.py new file mode 100644 index 000000000..709ff1985 --- /dev/null +++ b/backend/src/baserow/contrib/integrations/migrations/0007_field_mapping_enabled.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2024-07-15 09:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "integrations", + "0006_migrate_local_baserow_table_service_filter_formulas_to_value_is_formula", + ), + ] + + operations = [ + migrations.AddField( + model_name="localbaserowtableservicefieldmapping", + name="enabled", + field=models.BooleanField( + default=True, + help_text="Indicates if the field mapping is enabled. If it is disabled, we will not use the `value` when creating and updating rows.", + null=True, + ), + ), + ] diff --git a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py index 4f9b66bd5..052e9ef0f 100644 --- a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py +++ b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py @@ -390,7 +390,9 @@ def test_update_create_row_workflow_action(api_client, data_fixture): "table_id": table.id, "type": service_type.type, "integration_id": workflow_action.service.integration_id, - "field_mappings": [{"field_id": field.id, "value": "'Pony'"}], + "field_mappings": [ + {"field_id": field.id, "value": "'Pony'", "enabled": True} + ], } }, format="json", @@ -406,7 +408,7 @@ def test_update_create_row_workflow_action(api_client, data_fixture): assert response_json["service"]["table_id"] == service.table_id assert response_json["service"]["integration_id"] == service.integration_id assert response_json["service"]["field_mappings"] == [ - {"field_id": field.id, "value": "'Pony'"} + {"field_id": field.id, "value": "'Pony'", "enabled": True} ] @@ -487,7 +489,9 @@ def test_update_update_row_workflow_action(api_client, data_fixture): "row_id": first_row.id, "type": service_type.type, "integration_id": workflow_action.service.integration_id, - "field_mappings": [{"field_id": field.id, "value": "'Pony'"}], + "field_mappings": [ + {"field_id": field.id, "value": "'Pony'", "enabled": True} + ], }, }, format="json", @@ -508,7 +512,7 @@ def test_update_update_row_workflow_action(api_client, data_fixture): == workflow_action.service.integration_id ) assert response_json["service"]["field_mappings"] == [ - {"field_id": field.id, "value": "'Pony'"} + {"field_id": field.id, "value": "'Pony'", "enabled": True} ] diff --git a/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_types.py b/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_types.py index 6ed1b5156..7fdf33279 100644 --- a/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_types.py +++ b/backend/tests/baserow/contrib/builder/workflow_actions/test_workflow_action_types.py @@ -126,7 +126,9 @@ def test_export_import_upsert_row_workflow_action_type(data_fixture): "type": "local_baserow_upsert_row", "row_id": "", "table_id": table.id, - "field_mappings": [{"field_id": field.id, "value": field_mapping.value}], + "field_mappings": [ + {"field_id": field.id, "value": field_mapping.value, "enabled": True} + ], }, } diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/test_service_types.py b/backend/tests/baserow/contrib/integrations/local_baserow/test_service_types.py index 8a07e81e9..b127aa3f2 100644 --- a/backend/tests/baserow/contrib/integrations/local_baserow/test_service_types.py +++ b/backend/tests/baserow/contrib/integrations/local_baserow/test_service_types.py @@ -2091,6 +2091,64 @@ def test_local_baserow_upsert_row_service_dispatch_data_without_row_id( ) +@pytest.mark.django_db +def test_local_baserow_upsert_row_service_dispatch_data_disabled_field_mapping_fields( + data_fixture, +): + user = data_fixture.create_user() + page = data_fixture.create_builder_page(user=user) + integration = data_fixture.create_local_baserow_integration( + application=page.builder, user=user + ) + database = data_fixture.create_database_application( + workspace=page.builder.workspace + ) + table = TableHandler().create_table_and_fields( + user=user, + database=database, + name=data_fixture.fake.name(), + fields=[ + ("Name", "text", {}), + ("Last name", "text", {}), + ("Location", "text", {}), + ], + ) + + name_field = table.field_set.get(name="Name") + last_name_field = table.field_set.get(name="Last name") + location_field = table.field_set.get(name="Location") + + row = RowHandler().create_row( + user=user, + table=table, + values={ + name_field.id: "Peter", + last_name_field.id: "Evans", + location_field.id: "Cornwall", + }, + ) + + service = data_fixture.create_local_baserow_upsert_row_service( + table=table, + row_id=f"'{row.id}'", + integration=integration, + ) + service_type = service.get_type() + service.field_mappings.create(field=name_field, value="'Jeff'", enabled=True) + service.field_mappings.create(field=last_name_field, value="", enabled=False) + service.field_mappings.create(field=location_field, value="", enabled=False) + + fake_request = Mock() + dispatch_context = BuilderDispatchContext(fake_request, page) + dispatch_values = service_type.resolve_service_formulas(service, dispatch_context) + service_type.dispatch_data(service, dispatch_values, dispatch_context) + + row.refresh_from_db() + assert getattr(row, name_field.db_column) == "Jeff" + assert getattr(row, last_name_field.db_column) == "Evans" + assert getattr(row, location_field.db_column) == "Cornwall" + + @pytest.mark.django_db def test_local_baserow_upsert_row_service_dispatch_data_with_row_id( data_fixture, @@ -2540,7 +2598,9 @@ def test_local_baserow_upsert_row_service_after_update(data_fixture): { "table_id": table.id, "integration_id": integration.id, - "field_mappings": [{"field_id": field.id, "value": "'Horse'"}], + "field_mappings": [ + {"field_id": field.id, "value": "'Horse'", "enabled": True} + ], }, {}, ) @@ -2551,7 +2611,7 @@ def test_local_baserow_upsert_row_service_after_update(data_fixture): service, { "table_id": table.id, - "field_mappings": [{"value": "'Bread'"}], + "field_mappings": [{"value": "'Bread'", "enabled": True}], }, {}, ) @@ -2566,7 +2626,9 @@ def test_local_baserow_upsert_row_service_after_update(data_fixture): LocalBaserowUpsertRowServiceType().after_update( service, { - "field_mappings": [{"field_id": field.id, "value": "'Pony'"}], + "field_mappings": [ + {"field_id": field.id, "value": "'Pony'", "enabled": True} + ], }, {"table": (table, table2)}, ) diff --git a/changelog/entries/unreleased/feature/2620_made_it_easier_for_application_builder_page_designers_to_dis.json b/changelog/entries/unreleased/feature/2620_made_it_easier_for_application_builder_page_designers_to_dis.json new file mode 100644 index 000000000..4957b957d --- /dev/null +++ b/changelog/entries/unreleased/feature/2620_made_it_easier_for_application_builder_page_designers_to_dis.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "message": "[Builder] Made it easier for application builder page designers to disable fields in their create/update row actions.", + "issue_number": 2620, + "bullet_points": [], + "created_at": "2024-07-15" +} diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json index 54b9a87ed..94b5a647f 100644 --- a/web-frontend/modules/builder/locales/en.json +++ b/web-frontend/modules/builder/locales/en.json @@ -608,6 +608,10 @@ "fieldMappingPlaceholder": "Choose a field value", "noTableSelectedMessage": "Choose a table to begin configuring your fields." }, + "fieldMappingContext": { + "enableField": "Enable field", + "disableField": "Disable field" + }, "checkboxElementForm": { "labelTitle": "Label", "valueTitle": "Default value", diff --git a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss index f28cf8889..3266afd98 100644 --- a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss +++ b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss @@ -16,6 +16,10 @@ border-color: $color-primary-500; } +.formula-input-field--disabled { + user-select: none; +} + .formula-input-field__reset-button { width: 100%; } diff --git a/web-frontend/modules/core/components/formula/FormulaInputField.vue b/web-frontend/modules/core/components/formula/FormulaInputField.vue index f2371ab67..ad11dbe61 100644 --- a/web-frontend/modules/core/components/formula/FormulaInputField.vue +++ b/web-frontend/modules/core/components/formula/FormulaInputField.vue @@ -63,6 +63,11 @@ export default { type: String, default: '', }, + disabled: { + type: Boolean, + required: false, + default: false, + }, placeholder: { type: String, default: null, @@ -104,8 +109,10 @@ export default { }, classes() { return { + 'form-input--disabled': this.disabled, 'formula-input-field--small': this.small, - 'formula-input-field--focused': this.isFocused, + 'formula-input-field--focused': !this.disabled && this.isFocused, + 'formula-input-field--disabled': this.disabled, } }, placeHolderExt() { @@ -159,6 +166,9 @@ export default { }, }, watch: { + disabled(newValue) { + this.editor.setOptions({ editable: !newValue }) + }, isFocused(value) { if (!value) { this.$refs.dataExplorer.hide() @@ -220,7 +230,7 @@ export default { this.content = this.toContent(this.value) this.editor = new Editor({ content: this.htmlContent, - editable: true, + editable: !this.disabled, onUpdate: this.onUpdate, onFocus: this.onFocus, onBlur: this.onBlur, @@ -251,7 +261,9 @@ export default { this.emitChange() }, onFocus() { - this.formulaInputFocused = true + // If the input is disabled, we don't want users to be + // able to open the data explorer and select nodes. + this.formulaInputFocused = !this.disabled }, onBlur() { // We have to delay the browser here by just a bit, running the below will make diff --git a/web-frontend/modules/core/components/formula/FormulaInputGroup.vue b/web-frontend/modules/core/components/formula/FormulaInputGroup.vue index 38d65af91..57a4fe027 100644 --- a/web-frontend/modules/core/components/formula/FormulaInputGroup.vue +++ b/web-frontend/modules/core/components/formula/FormulaInputGroup.vue @@ -8,6 +8,7 @@ > <FormulaInputField :value="value" + :disabled="disabled" :placeholder="placeholder" :data-providers="dataProviders" :data-explorer-loading="dataExplorerLoading" @@ -23,6 +24,7 @@ <script> import FormulaInputField from '@baserow/modules/core/components/formula/FormulaInputField' + export default { components: { FormulaInputField }, props: { @@ -30,6 +32,11 @@ export default { type: String, required: true, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, label: { type: String, required: false, diff --git a/web-frontend/modules/core/components/formula/InjectedFormulaInputGroup.vue b/web-frontend/modules/core/components/formula/InjectedFormulaInputGroup.vue index 452402cc5..1f86a9162 100644 --- a/web-frontend/modules/core/components/formula/InjectedFormulaInputGroup.vue +++ b/web-frontend/modules/core/components/formula/InjectedFormulaInputGroup.vue @@ -4,7 +4,11 @@ :data-providers-allowed="dataProvidersAllowed || []" v-bind="$attrs" v-on="$listeners" - /> + > + <template #after-input> + <slot name="after-input"></slot> + </template> + </component> </template> <script> diff --git a/web-frontend/modules/integrations/localBaserow/components/services/FieldMapping.vue b/web-frontend/modules/integrations/localBaserow/components/services/FieldMapping.vue index d5c01d020..2763c3572 100644 --- a/web-frontend/modules/integrations/localBaserow/components/services/FieldMapping.vue +++ b/web-frontend/modules/integrations/localBaserow/components/services/FieldMapping.vue @@ -1,37 +1,62 @@ <template> - <InjectedFormulaInputGroup - v-model="fieldValue" - v-bind="$attrs" - class="margin-bottom-2" - /> + <div> + <InjectedFormulaInputGroup + v-model="fieldValue" + :disabled="!fieldMapping.enabled" + v-bind="$attrs" + class="margin-bottom-2" + > + <template #after-input> + <div ref="editFieldMappingOpener"> + <Button + type="secondary" + size="regular" + icon="iconoir-more-vert" + @click="openContext" + ></Button> + </div> + <FieldMappingContext + ref="fieldMappingContext" + :field-mapping="fieldMapping" + @edit="$emit('change', $event)" + ></FieldMappingContext> + </template> + </InjectedFormulaInputGroup> + </div> </template> <script> import InjectedFormulaInputGroup from '@baserow/modules/core/components/formula/InjectedFormulaInputGroup' +import FieldMappingContext from '@baserow/modules/integrations/localBaserow/components/services/FieldMappingContext' export default { name: 'FieldMapping', - components: { InjectedFormulaInputGroup }, + components: { FieldMappingContext, InjectedFormulaInputGroup }, props: { - field: { + fieldMapping: { type: Object, required: true, }, - value: { - type: String, - required: false, - default: () => '', - }, }, computed: { fieldValue: { get() { - return this.value + return this.fieldMapping.value }, set(value) { - this.$emit('change', value) + this.$emit('change', { value }) }, }, }, + methods: { + openContext() { + this.$refs.fieldMappingContext.toggle( + this.$refs.editFieldMappingOpener, + 'bottom', + 'left', + 4 + ) + }, + }, } </script> diff --git a/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingContext.vue b/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingContext.vue new file mode 100644 index 000000000..d503dfb62 --- /dev/null +++ b/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingContext.vue @@ -0,0 +1,48 @@ +<template> + <Context :overflow-scroll="true" :max-height-if-outside-viewport="true"> + <ul class="context__menu"> + <li class="context__menu-item"> + <a + class="context__menu-item-link" + @click.prevent=" + handleEditClick({ enabled: !fieldMapping.enabled, value: '' }) + " + > + <i class="context__menu-item-icon" :class="enabledClass"></i> + {{ toggleEnabledText }} + </a> + </li> + </ul> + </Context> +</template> + +<script> +import context from '@baserow/modules/core/mixins/context' + +export default { + name: 'FieldMappingContext', + mixins: [context], + props: { + fieldMapping: { + type: Object, + required: true, + }, + }, + computed: { + enabledClass() { + return this.fieldMapping.enabled ? 'iconoir-eye-off' : 'iconoir-eye-empty' + }, + toggleEnabledText() { + return this.fieldMapping.enabled + ? this.$t('fieldMappingContext.disableField') + : this.$t('fieldMappingContext.enableField') + }, + }, + methods: { + handleEditClick(change) { + this.$emit('edit', change) + this.hide() + }, + }, +} +</script> diff --git a/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingForm.vue b/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingForm.vue index 2fad4fe31..a63d76d71 100644 --- a/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingForm.vue +++ b/web-frontend/modules/integrations/localBaserow/components/services/FieldMappingForm.vue @@ -5,11 +5,10 @@ :key="field.id" small small-label - :field="field" :label="field.name" - :value="getFieldMappingValue(field.id)" + :field-mapping="getFieldMapping(field.id)" :placeholder="$t('upsertRowWorkflowActionForm.fieldMappingPlaceholder')" - @change="updateFieldMapping($event, field.id)" + @change="updateFieldMapping(field.id, $event)" ></FieldMapping> </div> </template> @@ -31,31 +30,31 @@ export default { }, }, methods: { - getFieldMappingValue(fieldId) { - const mapping = this.value.find( - (fieldMapping) => fieldMapping.field_id === fieldId + getFieldMapping(fieldId) { + return ( + this.value.find( + (fieldMapping) => fieldMapping.field_id === fieldId + ) || { enabled: true, field_id: fieldId, value: '' } ) - return mapping?.value || '' }, - updateFieldMapping(newValue, fieldId) { - const event = { field_id: fieldId, value: newValue } + updateFieldMapping(fieldId, changes) { const existingMapping = this.value.some( ({ field_id: existingId }) => existingId === fieldId ) if (existingMapping) { const newMapping = this.value.map((fieldMapping) => { if (fieldMapping.field_id === fieldId) { - return { ...fieldMapping, ...event } + return { ...fieldMapping, ...changes } } return fieldMapping }) this.$emit('input', newMapping) } else { - // It already exists const newMapping = [...this.value] newMapping.push({ + enabled: true, field_id: fieldId, - ...event, + ...changes, }) this.$emit('input', newMapping) }