mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Refactored custom integration creation and limits modals
refs https://github.com/TryGhost/Team/issues/559 - switched to new ember-promise-modals pattern - removed controller and template in favor of opening modals directly from the route - removed unused `mousedown` event handlers - they are only necessary when an input blur would trigger validation errors - fixed Enter key not triggering create action by adding an `{{on-key "Enter"}}` event handler to the name input - fixed scroll not resetting to top of integrations screens when navigating between them by adding `{{scroll-top}}` element modifier to the main content sections
This commit is contained in:
parent
6a43cb27c3
commit
d0f6dd7fef
@ -1,47 +0,0 @@
|
||||
<header class="modal-header" data-test-modal="new-integration">
|
||||
<h1>New custom integration</h1>
|
||||
</header>
|
||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
||||
<button class="close" href title="Close" {{action "closeModal"}} {{action (optional this.noop) on="mouseDown"}}>
|
||||
{{svg-jar "close"}}
|
||||
</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<GhFormGroup @errors={{this.integration.errors}} @hasValidated={{this.integration.hasValidated}} @property="name">
|
||||
<label for="new-integration-name" class="fw6">Name</label>
|
||||
<input type="text"
|
||||
value={{this.integration.name}}
|
||||
oninput={{action "updateName" value="target.value"}}
|
||||
id="new-integration-name"
|
||||
class="gh-input mt1"
|
||||
name="integration-name"
|
||||
autofocus="autofocus"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
data-test-input="new-integration-name">
|
||||
<GhErrorMessage @errors={{this.integration.errors}} @property="name" data-test-error="new-integration-name" />
|
||||
</GhFormGroup>
|
||||
</fieldset>
|
||||
|
||||
{{#if this.errorMessage}}
|
||||
<p class="error"><strong class="response">{{this.errorMessage}}</strong></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="gh-btn"
|
||||
{{action "closeModal"}}
|
||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
||||
{{action (optional this.noop) on="mouseDown"}}
|
||||
data-test-button="cancel-new-integration"
|
||||
>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
<GhTaskButton @buttonText="Create"
|
||||
@successText="Created"
|
||||
@task={{this.createIntegration}}
|
||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||
data-test-button="create-integration" />
|
||||
</div>
|
@ -1,59 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {A as emberA} from '@ember/array';
|
||||
import {isHostLimitError} from 'ghost-admin/services/ajax';
|
||||
import {isInvalidError} from 'ember-ajax/errors';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
feature: service(),
|
||||
|
||||
errorMessage: null,
|
||||
|
||||
confirm() {},
|
||||
|
||||
integration: alias('model'),
|
||||
actions: {
|
||||
updateName(name) {
|
||||
this.integration.set('name', name);
|
||||
this.integration.set('hasValidated', emberA());
|
||||
this.integration.errors.clear();
|
||||
},
|
||||
|
||||
confirm() {
|
||||
return this.createIntegration.perform();
|
||||
}
|
||||
},
|
||||
|
||||
createIntegration: task(function* () {
|
||||
try {
|
||||
let integration = yield this.confirm();
|
||||
this.router.transitionTo('settings.integration', integration);
|
||||
} catch (error) {
|
||||
// TODO: server-side validation errors should be serialized
|
||||
// properly so that errors are added to model.errors automatically
|
||||
if (error && isInvalidError(error)) {
|
||||
let [firstError] = error.payload.errors;
|
||||
let {message} = firstError;
|
||||
|
||||
if (message && message.match(/name/i)) {
|
||||
this.get('integration.errors').add('name', message);
|
||||
this.get('integration.hasValidated').pushObject('name');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHostLimitError(error)) {
|
||||
this.set('errorMessage', error.payload.errors[0].context);
|
||||
return;
|
||||
}
|
||||
|
||||
// bubble up to the global error handler
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
@ -1,20 +0,0 @@
|
||||
<header class="modal-header" data-test-modal="delete-user">
|
||||
<h1>Upgrade to enable custom integrations</h1>
|
||||
</header>
|
||||
<button class="close" title="Close" {{on "click" this.closeModal}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{html-safe this.model.message}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{on "click" this.closeModal}} class="gh-btn" data-test-button="cancel-upgrade">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<button {{on "click" (action "upgrade")}} class="gh-btn gh-btn-green" data-test-button="upgrade-plan">
|
||||
<span>Upgrade my plan</span>
|
||||
</button>
|
||||
</div>
|
@ -1,16 +0,0 @@
|
||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
|
||||
actions: {
|
||||
upgrade() {
|
||||
this.router.transitionTo('pro');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
this.send('upgrade');
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
<div class="modal-content" data-test-modal="limits/custom-integration">
|
||||
<header class="modal-header">
|
||||
<h1>Upgrade to enable custom integrations</h1>
|
||||
</header>
|
||||
<button class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
{{html-safe @data.message}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button {{on "click" @close}} class="gh-btn" data-test-button="cancel-upgrade">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<LinkTo @route="pro" class="gh-btn gh-btn-green" {{on "click" @close}} data-test-button="upgrade-plan">
|
||||
<span>Upgrade my plan</span>
|
||||
</LinkTo>
|
||||
</div>
|
||||
</div>
|
48
ghost/admin/app/components/modals/new-custom-integration.hbs
Normal file
48
ghost/admin/app/components/modals/new-custom-integration.hbs
Normal file
@ -0,0 +1,48 @@
|
||||
<div class="modal-content">
|
||||
<header class="modal-header" data-test-modal="new-integration">
|
||||
<h1>New custom integration</h1>
|
||||
</header>
|
||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
||||
<button class="close" href title="Close" {{on "click" @close}}>
|
||||
{{svg-jar "close"}}
|
||||
</button>
|
||||
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<GhFormGroup @errors={{this.integration.errors}} @hasValidated={{this.integration.hasValidated}} @property="name">
|
||||
<label for="new-integration-name" class="fw6">Name</label>
|
||||
<input type="text"
|
||||
value={{this.integration.name}}
|
||||
{{on "input" this.updateName}}
|
||||
{{on-key "Enter" (perform this.createIntegrationTask)}}
|
||||
id="new-integration-name"
|
||||
class="gh-input mt1"
|
||||
name="integration-name"
|
||||
autofocus="autofocus"
|
||||
{{autofocus}}
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
data-test-input="new-integration-name">
|
||||
<GhErrorMessage @errors={{this.integration.errors}} @property="name" data-test-error="new-integration-name" />
|
||||
</GhFormGroup>
|
||||
</fieldset>
|
||||
|
||||
{{#if this.errorMessage}}
|
||||
<p class="error"><strong class="response">{{this.errorMessage}}</strong></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="gh-btn" {{on "click" @close}} data-test-button="cancel-new-integration">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
|
||||
<GhTaskButton
|
||||
@buttonText="Create"
|
||||
@successText="Created"
|
||||
@task={{this.createIntegrationTask}}
|
||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||
data-test-button="create-integration" />
|
||||
</div>
|
||||
|
||||
</div>
|
64
ghost/admin/app/components/modals/new-custom-integration.js
Normal file
64
ghost/admin/app/components/modals/new-custom-integration.js
Normal file
@ -0,0 +1,64 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {A} from '@ember/array';
|
||||
import {action} from '@ember/object';
|
||||
import {isHostLimitError} from 'ghost-admin/services/ajax';
|
||||
import {isInvalidError} from 'ember-ajax/errors';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class NewCustomIntegrationModalComponent extends Component {
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@tracked errorMessage;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.integration = this.store.createRecord('integration');
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.integration.rollbackAttributes();
|
||||
}
|
||||
|
||||
@action
|
||||
updateName(inputEvent) {
|
||||
this.integration.set('name', inputEvent.target.value);
|
||||
this.integration.set('hasValidated', A());
|
||||
this.integration.errors.clear();
|
||||
}
|
||||
|
||||
@task({drop: true})
|
||||
*createIntegrationTask() {
|
||||
try {
|
||||
const integration = yield this.integration.save();
|
||||
this.router.transitionTo('settings.integration', integration);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// TODO: server-side validation errors should be serialized
|
||||
// properly so that errors are added to model.errors automatically
|
||||
if (error && isInvalidError(error)) {
|
||||
let [firstError] = error.payload.errors;
|
||||
let {message} = firstError;
|
||||
|
||||
if (message && message.match(/name/i)) {
|
||||
this.integration.errors.add('name', message);
|
||||
this.integration.hasValidated.pushObject('name');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHostLimitError(error)) {
|
||||
this.errorMessage = error.payload.errors[0].context;
|
||||
return;
|
||||
}
|
||||
|
||||
// bubble up to the global error handler
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
|
||||
export default Controller.extend({
|
||||
integration: alias('model.integration'),
|
||||
hostLimitError: alias('model.hostLimitError'),
|
||||
|
||||
showUpgradeModal: computed('hostLimitError', function () {
|
||||
if (this.hostLimitError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
return this.integration.save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
// 'new' route's dectivate hook takes care of rollback
|
||||
this.transitionToRoute('settings.integrations');
|
||||
}
|
||||
}
|
||||
});
|
@ -1,31 +1,45 @@
|
||||
import RSVP from 'rsvp';
|
||||
import Route from '@ember/routing/route';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Route.extend({
|
||||
limit: service(),
|
||||
export default class NewIntegrationRoute extends Route {
|
||||
@service limit;
|
||||
@service modals;
|
||||
|
||||
model() {
|
||||
if (this.limit.limiter
|
||||
&& this.limit.limiter.isLimited('customIntegrations')) {
|
||||
return RSVP.hash({
|
||||
integration: this.store.createRecord('integration'),
|
||||
hostLimitError: this.limit.limiter.errorIfWouldGoOverLimit('customIntegrations')
|
||||
.then(() => null)
|
||||
.catch((error) => {
|
||||
return error;
|
||||
})
|
||||
});
|
||||
} else {
|
||||
return RSVP.hash({
|
||||
integration: this.store.createRecord('integration'),
|
||||
hostLimitError: null
|
||||
});
|
||||
modal = null;
|
||||
|
||||
async model() {
|
||||
if (this.limit.limiter?.isLimited('customIntegrations')) {
|
||||
try {
|
||||
await this.limit.limiter.errorIfWouldGoOverLimit('customIntegrations');
|
||||
} catch (error) {
|
||||
this.modal = this.modals.open('modals/limits/custom-integration', {
|
||||
message: error.message
|
||||
}, {
|
||||
beforeClose: this.beforeModalClose
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.modal = this.modals.open('modals/new-custom-integration', {}, {
|
||||
beforeClose: this.beforeModalClose
|
||||
});
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.controller.integration.rollbackAttributes();
|
||||
// ensure we don't try to redirect on modal close if we're already transitioning away
|
||||
this.isLeaving = true;
|
||||
this.modal?.close();
|
||||
|
||||
this.modal = null;
|
||||
this.isLeaving = false;
|
||||
}
|
||||
});
|
||||
|
||||
@action
|
||||
beforeModalClose() {
|
||||
if (!this.isLeaving) {
|
||||
this.transitionTo('settings.integrations');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<div class="gh-main-section" {{scroll-top}}>
|
||||
<h4 class="gh-main-section-header small bn">Configuration</h4>
|
||||
<section class="gh-main-section-block">
|
||||
<div class="gh-main-section-content padding-top-s grey">
|
||||
|
@ -7,7 +7,7 @@
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<div class="gh-main-section" {{scroll-top}}>
|
||||
<div class="integrations-directory">
|
||||
<a class="id-item" href="https://ghost.org/integrations/disqus/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-disqus">
|
||||
|
@ -1,14 +0,0 @@
|
||||
{{#if showUpgradeModal}}
|
||||
<GhFullscreenModal @modal="upgrade-custom-integrations-host-limit"
|
||||
@model={{hash
|
||||
message=this.hostLimitError.message
|
||||
}}
|
||||
@close={{action "cancel"}}
|
||||
@modifier="action wide" />
|
||||
{{else}}
|
||||
<GhFullscreenModal @modal="new-integration"
|
||||
@model={{this.integration}}
|
||||
@confirm={{action "save"}}
|
||||
@close={{action "cancel"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
Loading…
Reference in New Issue
Block a user