diff --git a/changelog/entries/unreleased/feature/3170_added_a_shortcut_in_the_publishing_modal_so_that_domains_can.json b/changelog/entries/unreleased/feature/3170_added_a_shortcut_in_the_publishing_modal_so_that_domains_can.json new file mode 100644 index 000000000..2661fedbf --- /dev/null +++ b/changelog/entries/unreleased/feature/3170_added_a_shortcut_in_the_publishing_modal_so_that_domains_can.json @@ -0,0 +1,8 @@ +{ + "type": "feature", + "message": "Added a shortcut in the publishing modal so that domains can be created if an application doesn't yet have any.", + "domain": "builder", + "issue_number": 3170, + "bullet_points": [], + "created_at": "2025-03-13" +} \ No newline at end of file diff --git a/web-frontend/modules/builder/components/domain/CustomDomainForm.vue b/web-frontend/modules/builder/components/domain/CustomDomainForm.vue index 450a7ea96..742b956d1 100644 --- a/web-frontend/modules/builder/components/domain/CustomDomainForm.vue +++ b/web-frontend/modules/builder/components/domain/CustomDomainForm.vue @@ -7,6 +7,7 @@ :error-message="getFirstErrorMessage('domain_name') || serverErrorMessage" > <FormInput + ref="domainName" v-model="v$.values.domain_name.$model" size="large" @input="handleInput" @@ -45,6 +46,9 @@ export default { : '' }, }, + mounted() { + this.$refs.domainName.focus() + }, methods: { handleInput() { this.serverErrors.domain_name = null diff --git a/web-frontend/modules/builder/components/domain/DomainCard.vue b/web-frontend/modules/builder/components/domain/DomainCard.vue index a5d5e61b9..557f18d92 100644 --- a/web-frontend/modules/builder/components/domain/DomainCard.vue +++ b/web-frontend/modules/builder/components/domain/DomainCard.vue @@ -26,6 +26,13 @@ /> </div> </div> + <Alert + v-if="!domain.last_published" + type="warning" + class="margin-bottom-0" + > + <p>{{ $t('domainCard.unpublishedDomainWarning') }}</p> + </Alert> </template> <component :is="domainType.detailsComponent" diff --git a/web-frontend/modules/builder/components/domain/DomainForm.vue b/web-frontend/modules/builder/components/domain/DomainForm.vue index 2d15118f1..89058994a 100644 --- a/web-frontend/modules/builder/components/domain/DomainForm.vue +++ b/web-frontend/modules/builder/components/domain/DomainForm.vue @@ -105,6 +105,7 @@ export default { }) this.hideError() this.hideForm() + this.$emit('created') } catch (error) { this.handleAnyError(error) } diff --git a/web-frontend/modules/builder/components/page/header/PublishActionModal.vue b/web-frontend/modules/builder/components/page/header/PublishActionModal.vue index e9a83f1c7..46cbb5cb0 100644 --- a/web-frontend/modules/builder/components/page/header/PublishActionModal.vue +++ b/web-frontend/modules/builder/components/page/header/PublishActionModal.vue @@ -14,11 +14,12 @@ :key="domain.id" class="publish-action-modal__container" > - <Radio v-model="selectedDomain" :value="domain.id"> + <Radio v-model="selectedDomainId" :value="domain.id"> <span class="publish-action-modal__domain-name">{{ domain.domain_name }}</span> <a + v-if="domain.last_published" v-tooltip="$t('action.copyToClipboard')" class="publish-action-modal__copy-domain" tooltip-position="top" @@ -30,6 +31,7 @@ <Copied ref="domainCopied" /> </a> <a + v-if="domain.last_published" v-tooltip="$t('publishActionModal.openInNewTab')" tooltip-position="top" class="publish-action-modal__domain-link" @@ -46,6 +48,7 @@ /> </div> </template> + <div v-else-if="fetchingDomains" class="loading-spinner"></div> <p v-else>{{ $t('publishActionModal.noDomain') }}</p> </template> @@ -54,6 +57,11 @@ $t('publishActionModal.publishSucceedTitle') }}</template> <p>{{ $t('publishActionModal.publishSucceedDescription') }}</p> + <template #actions> + <Button tag="a" :href="getDomainUrl(selectedDomain)" target="_blank">{{ + $t('publishActionModal.publishSucceedLink') + }}</Button> + </template> </Alert> <div class="modal-progress__actions"> @@ -64,13 +72,24 @@ /> <div class="align-right"> <Button + v-if="domains.length" size="large" :loading="jobIsRunning || loading" - :disabled="loading || jobIsRunning || !selectedDomain" + :disabled="loading || jobIsRunning || !selectedDomainId" @click="publishSite()" > {{ $t('publishActionModal.publish') }} </Button> + <template v-else-if="!fetchingDomains"> + <Button tag="a" @click="openDomainSettings"> + {{ $t('publishActionModal.addDomain') }} + </Button> + </template> + <BuilderSettingsModal + ref="domainSettingsModal" + hide-after-create + :builder="builder" + /> </div> </div> </Modal> @@ -85,10 +104,12 @@ import PublishedDomainService from '@baserow/modules/builder/services/publishedB import { notifyIf } from '@baserow/modules/core/utils/error' import { copyToClipboard } from '@baserow/modules/database/utils/clipboard' import LastPublishedDomainDate from '@baserow/modules/builder/components/domain/LastPublishedDomainDate' +import BuilderSettingsModal from '@baserow/modules/builder/components/settings/BuilderSettingsModal' +import { DomainsBuilderSettingsType } from '@baserow/modules/builder/builderSettingTypes' export default { name: 'PublishActionModal', - components: { LastPublishedDomainDate }, + components: { BuilderSettingsModal, LastPublishedDomainDate }, mixins: [modal, error, jobProgress], props: { builder: { @@ -97,15 +118,23 @@ export default { }, }, data() { - return { selectedDomain: null, loading: false } + return { selectedDomainId: null, loading: false, fetchingDomains: false } }, computed: { ...mapGetters({ domains: 'domain/getDomains' }), + selectedDomain() { + return this.domains.find((domain) => domain.id === this.selectedDomainId) + }, }, watch: { - selectedDomain() { + selectedDomainId() { this.job = null }, + domains() { + if (!this.selectedDomainId) { + this.selectedDomainId = this.domains.length ? this.domains[0].id : null + } + }, }, beforeDestroy() { this.stopPollIfRunning() @@ -119,19 +148,22 @@ export default { this.hideError() this.job = null this.loading = false - this.selectedDomain = null + this.selectedDomainId = null + this.fetchingDomains = true try { await this.actionFetchDomains({ builderId: this.builder.id }) this.hideError() } catch (error) { this.handleError(error) + } finally { + this.fetchingDomains = false } }, async publishSite() { this.loading = true this.hideError() const { data: job } = await PublishedDomainService(this.$client).publish({ - id: this.selectedDomain, + id: this.selectedDomainId, }) this.startJobPoller(job) @@ -145,7 +177,7 @@ export default { }, onJobDone() { this.actionForceUpdateDomain({ - domainId: this.selectedDomain, + domainId: this.selectedDomainId, values: { last_published: new Date() }, }) this.loading = false @@ -169,6 +201,15 @@ export default { } return '' }, + openDomainSettings() { + // Open the builder settings modal, which is instructed to select the domain + // settings instance first, and pass `DomainsBuilderSettingsType.getType()` into + // `show` so that the create domain form is immediately presented. + this.$refs.domainSettingsModal.show( + DomainsBuilderSettingsType.getType(), + true + ) + }, }, } </script> diff --git a/web-frontend/modules/builder/components/settings/BuilderSettingsModal.vue b/web-frontend/modules/builder/components/settings/BuilderSettingsModal.vue index bd1bfca49..9311a4aa7 100644 --- a/web-frontend/modules/builder/components/settings/BuilderSettingsModal.vue +++ b/web-frontend/modules/builder/components/settings/BuilderSettingsModal.vue @@ -26,7 +26,14 @@ </ul> </template> <template v-if="settingSelected" #content> - <component :is="settingSelected.component" :builder="builder"></component> + <component + :is="settingSelected.component" + ref="settingSelected" + :builder="builder" + :hide-after-create="hideAfterCreate" + :force-display-form="displaySelectedSettingForm" + @hide-modal="hide()" + ></component> </template> </Modal> </template> @@ -43,10 +50,20 @@ export default { type: Object, required: true, }, + /** + * If you want the selected setting form to hide the builder settings modal + * after a record is created, set this to `true`. + */ + hideAfterCreate: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { settingSelected: null, + displaySelectedSettingForm: false, } }, computed: { @@ -54,12 +71,44 @@ export default { return this.$registry.getOrderedList('builderSettings') }, }, + watch: { + // When the selected setting changes, and we've forcibly displayed + // the selected setting's form, then reset the display flag so that + // the next setting doesn't immediately display its form. + settingSelected(newSetting, oldSetting) { + if ( + oldSetting && + newSetting !== oldSetting && + this.displaySelectedSettingForm + ) { + this.displaySelectedSettingForm = false + } + }, + }, methods: { - show(...args) { + show( + selectSettingType = null, + displaySelectedSettingForm = false, + ...args + ) { + // If we've been instructed to show a specific setting component, + // then ensure it's displayed first. + if (selectSettingType) { + this.settingSelected = this.$registry.get( + 'builderSettings', + selectSettingType + ) + } + + // If no `selectSettingType` was provided then choose the first setting. if (!this.settingSelected) { this.settingSelected = this.registeredSettings[0] } + // If we've been instructed to show the modal, and make the + // selected setting component's form display, then do so. + this.displaySelectedSettingForm = displaySelectedSettingForm + const builderApplicationType = this.$registry.get( 'application', BuilderApplicationType.getType() diff --git a/web-frontend/modules/builder/components/settings/DomainsSettings.vue b/web-frontend/modules/builder/components/settings/DomainsSettings.vue index 05c596104..30322b53e 100644 --- a/web-frontend/modules/builder/components/settings/DomainsSettings.vue +++ b/web-frontend/modules/builder/components/settings/DomainsSettings.vue @@ -28,7 +28,12 @@ {{ $t('domainSettings.noDomainMessage') }} </p> </div> - <DomainForm v-else :builder="builder" :hide-form="hideForm" /> + <DomainForm + v-else + :builder="builder" + :hide-form="hideForm" + @created="hideModalIfRequired" + /> </template> <script> @@ -36,22 +41,12 @@ import { mapActions, mapGetters } from 'vuex' import error from '@baserow/modules/core/mixins/error' import DomainCard from '@baserow/modules/builder/components/domain/DomainCard' import DomainForm from '@baserow/modules/builder/components/domain/DomainForm' +import builderSetting from '@baserow/modules/builder/components/settings/mixins/builderSetting' export default { name: 'DomainsSettings', components: { DomainCard, DomainForm }, - mixins: [error], - props: { - builder: { - type: Object, - required: true, - }, - }, - data() { - return { - showForm: false, - } - }, + mixins: [error, builderSetting], async fetch() { try { await this.actionFetchDomains({ builderId: this.builder.id }) diff --git a/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue b/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue index 2b52f543e..4b3bb7899 100644 --- a/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue +++ b/web-frontend/modules/builder/components/settings/UserSourcesSettings.vue @@ -1,13 +1,13 @@ <template> <!-- Show user source list --> <div - v-if="!showCreateForm && editedUserSource === null" + v-if="!showForm && editedUserSource === null" class="user-sources-settings" > <h2 class="box__title">{{ $t('userSourceSettings.titleOverview') }}</h2> <Error :error="error"></Error> <div v-if="!error.visible" class="actions actions--right"> - <Button icon="iconoir-plus" @click="showForm()"> + <Button icon="iconoir-plus" @click="displayForm()"> {{ $t('userSourceSettings.addUserSource') }} </Button> </div> @@ -26,7 +26,7 @@ style="flex: 1" /> <div class="user-source-settings__user-source-actions"> - <ButtonIcon icon="iconoir-edit" @click="showForm(userSource)" /> + <ButtonIcon icon="iconoir-edit" @click="displayForm(userSource)" /> <ButtonIcon icon="iconoir-bin" @click="deleteUserSource(userSource)" /> </div> </div> @@ -116,23 +116,18 @@ import { clone } from '@baserow/modules/core/utils/object' import { notifyIf } from '@baserow/modules/core/utils/error' import CreateUserSourceForm from '@baserow/modules/builder/components/userSource/CreateUserSourceForm' import UpdateUserSourceForm from '@baserow/modules/builder/components/userSource/UpdateUserSourceForm' +import builderSetting from '@baserow/modules/builder/components/settings/mixins/builderSetting' export default { name: 'UserSourceSettings', components: { CreateUserSourceForm, UpdateUserSourceForm }, - mixins: [error], + mixins: [error, builderSetting], provide() { return { builder: this.builder } }, - props: { - builder: { - type: Object, - required: true, - }, - }, data() { return { - showCreateForm: false, + showForm: false, editedUserSource: null, actionInProgress: false, invalidForm: true, @@ -145,9 +140,6 @@ export default { userSources() { return this.$store.getters['userSource/getUserSources'](this.builder) }, - userSourceTypes() { - return this.$registry.getAll('userSource') - }, }, async mounted() { try { @@ -174,17 +166,17 @@ export default { onValueChange() { this.invalidForm = !this.$refs.userSourceForm.isFormValid() }, - async showForm(userSourceToEdit) { + async displayForm(userSourceToEdit) { if (userSourceToEdit) { this.editedUserSource = userSourceToEdit } else { - this.showCreateForm = true + this.showForm = true } await this.$nextTick() this.onValueChange() }, hideForm() { - this.showCreateForm = false + this.showForm = false this.editedUserSource = null this.hideError() this.invalidForm = true @@ -202,6 +194,7 @@ export default { this.hideForm() // immediately select this user source to edit it. this.editedUserSource = createdUserSource + this.hideModalIfRequired() } catch (error) { this.handleError(error) } diff --git a/web-frontend/modules/builder/components/settings/mixins/builderSetting.js b/web-frontend/modules/builder/components/settings/mixins/builderSetting.js new file mode 100644 index 000000000..e91fdfaf7 --- /dev/null +++ b/web-frontend/modules/builder/components/settings/mixins/builderSetting.js @@ -0,0 +1,41 @@ +/** + * A mixin for the builder setting components which have forms. This mixin makes + * it easier to make them immediately display their "create" form from the parent + * builder settings modal. When `force-display-form` is set, the end-user doesn't + * have to click a "New" button. + */ +export default { + props: { + builder: { + type: Object, + required: true, + }, + forceDisplayForm: { + type: Boolean, + required: false, + default: false, + }, + hideAfterCreate: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showForm: false, + } + }, + mounted() { + if (this.forceDisplayForm) { + this.showForm = true + } + }, + methods: { + hideModalIfRequired() { + if (this.hideAfterCreate) { + this.$emit('hide-modal') + } + }, + }, +} diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json index 12ee0a2cd..83157558b 100644 --- a/web-frontend/modules/builder/locales/en.json +++ b/web-frontend/modules/builder/locales/en.json @@ -65,11 +65,13 @@ "publish": "Publish", "publishSucceedTitle": "Site published", "publishSucceedDescription": "The site has been successfully published.", + "publishSucceedLink": "View site", "publishFailedTitle": "Site publishing failed", "publishFailedDescription": "The site publishing has failed. Please try again later.", "openInNewTab": "Open in a new tab", "importingState": "Importing", - "noDomain": "You need to have at least one domain in order to publish your application." + "noDomain": "You need to have at least one domain in order to publish your application.", + "addDomain": "Add domain" }, "lastPublishedDomainDate": { "neverPublished": "never", @@ -346,7 +348,8 @@ }, "domainCard": { "refresh": "Refresh settings", - "detailLabel": "Show details" + "detailLabel": "Show details", + "unpublishedDomainWarning": "Please publish the application to make it available on this domain." }, "domainTypes": { "customName": "Custom domain",