1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 10:22:36 +00:00

Improve the publishing modal and domains management experience.

This commit is contained in:
Peter Evans 2025-03-18 10:51:03 +00:00
parent f069f6703a
commit 1411f56665
10 changed files with 184 additions and 42 deletions

View file

@ -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"
}

View file

@ -7,6 +7,7 @@
:error-message="getFirstErrorMessage('domain_name') || serverErrorMessage" :error-message="getFirstErrorMessage('domain_name') || serverErrorMessage"
> >
<FormInput <FormInput
ref="domainName"
v-model="v$.values.domain_name.$model" v-model="v$.values.domain_name.$model"
size="large" size="large"
@input="handleInput" @input="handleInput"
@ -45,6 +46,9 @@ export default {
: '' : ''
}, },
}, },
mounted() {
this.$refs.domainName.focus()
},
methods: { methods: {
handleInput() { handleInput() {
this.serverErrors.domain_name = null this.serverErrors.domain_name = null

View file

@ -26,6 +26,13 @@
/> />
</div> </div>
</div> </div>
<Alert
v-if="!domain.last_published"
type="warning"
class="margin-bottom-0"
>
<p>{{ $t('domainCard.unpublishedDomainWarning') }}</p>
</Alert>
</template> </template>
<component <component
:is="domainType.detailsComponent" :is="domainType.detailsComponent"

View file

@ -105,6 +105,7 @@ export default {
}) })
this.hideError() this.hideError()
this.hideForm() this.hideForm()
this.$emit('created')
} catch (error) { } catch (error) {
this.handleAnyError(error) this.handleAnyError(error)
} }

View file

@ -14,11 +14,12 @@
:key="domain.id" :key="domain.id"
class="publish-action-modal__container" 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">{{ <span class="publish-action-modal__domain-name">{{
domain.domain_name domain.domain_name
}}</span> }}</span>
<a <a
v-if="domain.last_published"
v-tooltip="$t('action.copyToClipboard')" v-tooltip="$t('action.copyToClipboard')"
class="publish-action-modal__copy-domain" class="publish-action-modal__copy-domain"
tooltip-position="top" tooltip-position="top"
@ -30,6 +31,7 @@
<Copied ref="domainCopied" /> <Copied ref="domainCopied" />
</a> </a>
<a <a
v-if="domain.last_published"
v-tooltip="$t('publishActionModal.openInNewTab')" v-tooltip="$t('publishActionModal.openInNewTab')"
tooltip-position="top" tooltip-position="top"
class="publish-action-modal__domain-link" class="publish-action-modal__domain-link"
@ -46,6 +48,7 @@
/> />
</div> </div>
</template> </template>
<div v-else-if="fetchingDomains" class="loading-spinner"></div>
<p v-else>{{ $t('publishActionModal.noDomain') }}</p> <p v-else>{{ $t('publishActionModal.noDomain') }}</p>
</template> </template>
@ -54,6 +57,11 @@
$t('publishActionModal.publishSucceedTitle') $t('publishActionModal.publishSucceedTitle')
}}</template> }}</template>
<p>{{ $t('publishActionModal.publishSucceedDescription') }}</p> <p>{{ $t('publishActionModal.publishSucceedDescription') }}</p>
<template #actions>
<Button tag="a" :href="getDomainUrl(selectedDomain)" target="_blank">{{
$t('publishActionModal.publishSucceedLink')
}}</Button>
</template>
</Alert> </Alert>
<div class="modal-progress__actions"> <div class="modal-progress__actions">
@ -64,13 +72,24 @@
/> />
<div class="align-right"> <div class="align-right">
<Button <Button
v-if="domains.length"
size="large" size="large"
:loading="jobIsRunning || loading" :loading="jobIsRunning || loading"
:disabled="loading || jobIsRunning || !selectedDomain" :disabled="loading || jobIsRunning || !selectedDomainId"
@click="publishSite()" @click="publishSite()"
> >
{{ $t('publishActionModal.publish') }} {{ $t('publishActionModal.publish') }}
</Button> </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>
</div> </div>
</Modal> </Modal>
@ -85,10 +104,12 @@ import PublishedDomainService from '@baserow/modules/builder/services/publishedB
import { notifyIf } from '@baserow/modules/core/utils/error' import { notifyIf } from '@baserow/modules/core/utils/error'
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard' import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
import LastPublishedDomainDate from '@baserow/modules/builder/components/domain/LastPublishedDomainDate' 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 { export default {
name: 'PublishActionModal', name: 'PublishActionModal',
components: { LastPublishedDomainDate }, components: { BuilderSettingsModal, LastPublishedDomainDate },
mixins: [modal, error, jobProgress], mixins: [modal, error, jobProgress],
props: { props: {
builder: { builder: {
@ -97,15 +118,23 @@ export default {
}, },
}, },
data() { data() {
return { selectedDomain: null, loading: false } return { selectedDomainId: null, loading: false, fetchingDomains: false }
}, },
computed: { computed: {
...mapGetters({ domains: 'domain/getDomains' }), ...mapGetters({ domains: 'domain/getDomains' }),
selectedDomain() {
return this.domains.find((domain) => domain.id === this.selectedDomainId)
},
}, },
watch: { watch: {
selectedDomain() { selectedDomainId() {
this.job = null this.job = null
}, },
domains() {
if (!this.selectedDomainId) {
this.selectedDomainId = this.domains.length ? this.domains[0].id : null
}
},
}, },
beforeDestroy() { beforeDestroy() {
this.stopPollIfRunning() this.stopPollIfRunning()
@ -119,19 +148,22 @@ export default {
this.hideError() this.hideError()
this.job = null this.job = null
this.loading = false this.loading = false
this.selectedDomain = null this.selectedDomainId = null
this.fetchingDomains = true
try { try {
await this.actionFetchDomains({ builderId: this.builder.id }) await this.actionFetchDomains({ builderId: this.builder.id })
this.hideError() this.hideError()
} catch (error) { } catch (error) {
this.handleError(error) this.handleError(error)
} finally {
this.fetchingDomains = false
} }
}, },
async publishSite() { async publishSite() {
this.loading = true this.loading = true
this.hideError() this.hideError()
const { data: job } = await PublishedDomainService(this.$client).publish({ const { data: job } = await PublishedDomainService(this.$client).publish({
id: this.selectedDomain, id: this.selectedDomainId,
}) })
this.startJobPoller(job) this.startJobPoller(job)
@ -145,7 +177,7 @@ export default {
}, },
onJobDone() { onJobDone() {
this.actionForceUpdateDomain({ this.actionForceUpdateDomain({
domainId: this.selectedDomain, domainId: this.selectedDomainId,
values: { last_published: new Date() }, values: { last_published: new Date() },
}) })
this.loading = false this.loading = false
@ -169,6 +201,15 @@ export default {
} }
return '' 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> </script>

View file

@ -26,7 +26,14 @@
</ul> </ul>
</template> </template>
<template v-if="settingSelected" #content> <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> </template>
</Modal> </Modal>
</template> </template>
@ -43,10 +50,20 @@ export default {
type: Object, type: Object,
required: true, 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() { data() {
return { return {
settingSelected: null, settingSelected: null,
displaySelectedSettingForm: false,
} }
}, },
computed: { computed: {
@ -54,12 +71,44 @@ export default {
return this.$registry.getOrderedList('builderSettings') 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: { 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) { if (!this.settingSelected) {
this.settingSelected = this.registeredSettings[0] 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( const builderApplicationType = this.$registry.get(
'application', 'application',
BuilderApplicationType.getType() BuilderApplicationType.getType()

View file

@ -28,7 +28,12 @@
{{ $t('domainSettings.noDomainMessage') }} {{ $t('domainSettings.noDomainMessage') }}
</p> </p>
</div> </div>
<DomainForm v-else :builder="builder" :hide-form="hideForm" /> <DomainForm
v-else
:builder="builder"
:hide-form="hideForm"
@created="hideModalIfRequired"
/>
</template> </template>
<script> <script>
@ -36,22 +41,12 @@ import { mapActions, mapGetters } from 'vuex'
import error from '@baserow/modules/core/mixins/error' import error from '@baserow/modules/core/mixins/error'
import DomainCard from '@baserow/modules/builder/components/domain/DomainCard' import DomainCard from '@baserow/modules/builder/components/domain/DomainCard'
import DomainForm from '@baserow/modules/builder/components/domain/DomainForm' import DomainForm from '@baserow/modules/builder/components/domain/DomainForm'
import builderSetting from '@baserow/modules/builder/components/settings/mixins/builderSetting'
export default { export default {
name: 'DomainsSettings', name: 'DomainsSettings',
components: { DomainCard, DomainForm }, components: { DomainCard, DomainForm },
mixins: [error], mixins: [error, builderSetting],
props: {
builder: {
type: Object,
required: true,
},
},
data() {
return {
showForm: false,
}
},
async fetch() { async fetch() {
try { try {
await this.actionFetchDomains({ builderId: this.builder.id }) await this.actionFetchDomains({ builderId: this.builder.id })

View file

@ -1,13 +1,13 @@
<template> <template>
<!-- Show user source list --> <!-- Show user source list -->
<div <div
v-if="!showCreateForm && editedUserSource === null" v-if="!showForm && editedUserSource === null"
class="user-sources-settings" class="user-sources-settings"
> >
<h2 class="box__title">{{ $t('userSourceSettings.titleOverview') }}</h2> <h2 class="box__title">{{ $t('userSourceSettings.titleOverview') }}</h2>
<Error :error="error"></Error> <Error :error="error"></Error>
<div v-if="!error.visible" class="actions actions--right"> <div v-if="!error.visible" class="actions actions--right">
<Button icon="iconoir-plus" @click="showForm()"> <Button icon="iconoir-plus" @click="displayForm()">
{{ $t('userSourceSettings.addUserSource') }} {{ $t('userSourceSettings.addUserSource') }}
</Button> </Button>
</div> </div>
@ -26,7 +26,7 @@
style="flex: 1" style="flex: 1"
/> />
<div class="user-source-settings__user-source-actions"> <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)" /> <ButtonIcon icon="iconoir-bin" @click="deleteUserSource(userSource)" />
</div> </div>
</div> </div>
@ -116,23 +116,18 @@ import { clone } from '@baserow/modules/core/utils/object'
import { notifyIf } from '@baserow/modules/core/utils/error' import { notifyIf } from '@baserow/modules/core/utils/error'
import CreateUserSourceForm from '@baserow/modules/builder/components/userSource/CreateUserSourceForm' import CreateUserSourceForm from '@baserow/modules/builder/components/userSource/CreateUserSourceForm'
import UpdateUserSourceForm from '@baserow/modules/builder/components/userSource/UpdateUserSourceForm' import UpdateUserSourceForm from '@baserow/modules/builder/components/userSource/UpdateUserSourceForm'
import builderSetting from '@baserow/modules/builder/components/settings/mixins/builderSetting'
export default { export default {
name: 'UserSourceSettings', name: 'UserSourceSettings',
components: { CreateUserSourceForm, UpdateUserSourceForm }, components: { CreateUserSourceForm, UpdateUserSourceForm },
mixins: [error], mixins: [error, builderSetting],
provide() { provide() {
return { builder: this.builder } return { builder: this.builder }
}, },
props: {
builder: {
type: Object,
required: true,
},
},
data() { data() {
return { return {
showCreateForm: false, showForm: false,
editedUserSource: null, editedUserSource: null,
actionInProgress: false, actionInProgress: false,
invalidForm: true, invalidForm: true,
@ -145,9 +140,6 @@ export default {
userSources() { userSources() {
return this.$store.getters['userSource/getUserSources'](this.builder) return this.$store.getters['userSource/getUserSources'](this.builder)
}, },
userSourceTypes() {
return this.$registry.getAll('userSource')
},
}, },
async mounted() { async mounted() {
try { try {
@ -174,17 +166,17 @@ export default {
onValueChange() { onValueChange() {
this.invalidForm = !this.$refs.userSourceForm.isFormValid() this.invalidForm = !this.$refs.userSourceForm.isFormValid()
}, },
async showForm(userSourceToEdit) { async displayForm(userSourceToEdit) {
if (userSourceToEdit) { if (userSourceToEdit) {
this.editedUserSource = userSourceToEdit this.editedUserSource = userSourceToEdit
} else { } else {
this.showCreateForm = true this.showForm = true
} }
await this.$nextTick() await this.$nextTick()
this.onValueChange() this.onValueChange()
}, },
hideForm() { hideForm() {
this.showCreateForm = false this.showForm = false
this.editedUserSource = null this.editedUserSource = null
this.hideError() this.hideError()
this.invalidForm = true this.invalidForm = true
@ -202,6 +194,7 @@ export default {
this.hideForm() this.hideForm()
// immediately select this user source to edit it. // immediately select this user source to edit it.
this.editedUserSource = createdUserSource this.editedUserSource = createdUserSource
this.hideModalIfRequired()
} catch (error) { } catch (error) {
this.handleError(error) this.handleError(error)
} }

View file

@ -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')
}
},
},
}

View file

@ -65,11 +65,13 @@
"publish": "Publish", "publish": "Publish",
"publishSucceedTitle": "Site published", "publishSucceedTitle": "Site published",
"publishSucceedDescription": "The site has been successfully published.", "publishSucceedDescription": "The site has been successfully published.",
"publishSucceedLink": "View site",
"publishFailedTitle": "Site publishing failed", "publishFailedTitle": "Site publishing failed",
"publishFailedDescription": "The site publishing has failed. Please try again later.", "publishFailedDescription": "The site publishing has failed. Please try again later.",
"openInNewTab": "Open in a new tab", "openInNewTab": "Open in a new tab",
"importingState": "Importing", "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": { "lastPublishedDomainDate": {
"neverPublished": "never", "neverPublished": "never",
@ -346,7 +348,8 @@
}, },
"domainCard": { "domainCard": {
"refresh": "Refresh settings", "refresh": "Refresh settings",
"detailLabel": "Show details" "detailLabel": "Show details",
"unpublishedDomainWarning": "Please publish the application to make it available on this domain."
}, },
"domainTypes": { "domainTypes": {
"customName": "Custom domain", "customName": "Custom domain",