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 Route from '@ember/routing/route';
|
||||||
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
export default Route.extend({
|
export default class NewIntegrationRoute extends Route {
|
||||||
limit: service(),
|
@service limit;
|
||||||
|
@service modals;
|
||||||
|
|
||||||
model() {
|
modal = null;
|
||||||
if (this.limit.limiter
|
|
||||||
&& this.limit.limiter.isLimited('customIntegrations')) {
|
async model() {
|
||||||
return RSVP.hash({
|
if (this.limit.limiter?.isLimited('customIntegrations')) {
|
||||||
integration: this.store.createRecord('integration'),
|
try {
|
||||||
hostLimitError: this.limit.limiter.errorIfWouldGoOverLimit('customIntegrations')
|
await this.limit.limiter.errorIfWouldGoOverLimit('customIntegrations');
|
||||||
.then(() => null)
|
} catch (error) {
|
||||||
.catch((error) => {
|
this.modal = this.modals.open('modals/limits/custom-integration', {
|
||||||
return error;
|
message: error.message
|
||||||
})
|
}, {
|
||||||
});
|
beforeClose: this.beforeModalClose
|
||||||
} else {
|
});
|
||||||
return RSVP.hash({
|
return;
|
||||||
integration: this.store.createRecord('integration'),
|
}
|
||||||
hostLimitError: null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
this.modal = this.modals.open('modals/new-custom-integration', {}, {
|
||||||
|
beforeClose: this.beforeModalClose
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
this._super(...arguments);
|
// ensure we don't try to redirect on modal close if we're already transitioning away
|
||||||
this.controller.integration.rollbackAttributes();
|
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>
|
</section>
|
||||||
</GhCanvasHeader>
|
</GhCanvasHeader>
|
||||||
|
|
||||||
<div class="gh-main-section">
|
<div class="gh-main-section" {{scroll-top}}>
|
||||||
<h4 class="gh-main-section-header small bn">Configuration</h4>
|
<h4 class="gh-main-section-header small bn">Configuration</h4>
|
||||||
<section class="gh-main-section-block">
|
<section class="gh-main-section-block">
|
||||||
<div class="gh-main-section-content padding-top-s grey">
|
<div class="gh-main-section-content padding-top-s grey">
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</GhCanvasHeader>
|
</GhCanvasHeader>
|
||||||
|
|
||||||
<div class="gh-main-section">
|
<div class="gh-main-section" {{scroll-top}}>
|
||||||
<div class="integrations-directory">
|
<div class="integrations-directory">
|
||||||
<a class="id-item" href="https://ghost.org/integrations/disqus/" target="_blank" rel="noopener noreferrer">
|
<a class="id-item" href="https://ghost.org/integrations/disqus/" target="_blank" rel="noopener noreferrer">
|
||||||
<div class="id-item-logo id-disqus">
|
<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