mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Added Integrations as a subpage of Settings
- Restructured Admin so that Integrations becomes a subpage of Settings.
This commit is contained in:
parent
b8d1dc8deb
commit
737db37175
@ -5,6 +5,7 @@ import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
feature: service(),
|
||||
notifications: service(),
|
||||
integration: alias('model'),
|
||||
actions: {
|
||||
@ -15,7 +16,11 @@ export default ModalComponent.extend({
|
||||
deleteIntegration: task(function* () {
|
||||
try {
|
||||
yield this.confirm();
|
||||
this.router.transitionTo('integrations');
|
||||
if (this.feature.get('offers')) {
|
||||
this.router.transitionTo('settings.integrations');
|
||||
} else {
|
||||
this.router.transitionTo('integrations');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifications.showAPIError(error, {key: 'integration.delete.failed'});
|
||||
} finally {
|
||||
|
@ -8,6 +8,7 @@ import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
feature: service(),
|
||||
|
||||
errorMessage: null,
|
||||
|
||||
@ -29,7 +30,11 @@ export default ModalComponent.extend({
|
||||
createIntegration: task(function* () {
|
||||
try {
|
||||
let integration = yield this.confirm();
|
||||
this.router.transitionTo('integration', integration);
|
||||
if (this.feature.get('offers')) {
|
||||
this.router.transitionTo('settings.integration', integration);
|
||||
} else {
|
||||
this.router.transitionTo('integration', integration);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: server-side validation errors should be serialized
|
||||
// properly so that errors are added to model.errors automatically
|
||||
|
@ -8,6 +8,7 @@ import {task} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
router: service(),
|
||||
feature: service(),
|
||||
|
||||
availableEvents: null,
|
||||
error: null,
|
||||
@ -48,7 +49,11 @@ export default ModalComponent.extend({
|
||||
try {
|
||||
let webhook = yield this.confirm();
|
||||
let integration = yield webhook.get('integration');
|
||||
this.router.transitionTo('integration', integration);
|
||||
if (this.feature.get('offers')) {
|
||||
this.router.transitionTo('settings.integration', integration);
|
||||
} else {
|
||||
this.router.transitionTo('integration', integration);
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO: server-side validation errors should be serialized
|
||||
// properly so that errors are added to model.errors automatically
|
||||
|
186
ghost/admin/app/controllers/settings/integration.js
Normal file
186
ghost/admin/app/controllers/settings/integration.js
Normal file
@ -0,0 +1,186 @@
|
||||
import Controller from '@ember/controller';
|
||||
import config from 'ghost-admin/config/environment';
|
||||
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import {
|
||||
IMAGE_EXTENSIONS,
|
||||
IMAGE_MIME_TYPES
|
||||
} from 'ghost-admin/components/gh-image-uploader';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
|
||||
imageExtensions: IMAGE_EXTENSIONS,
|
||||
imageMimeTypes: IMAGE_MIME_TYPES,
|
||||
showRegenerateKeyModal: false,
|
||||
selectedApiKey: null,
|
||||
isApiKeyRegenerated: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (this.isTesting === undefined) {
|
||||
this.isTesting = config.environment === 'test';
|
||||
}
|
||||
},
|
||||
|
||||
integration: alias('model'),
|
||||
|
||||
apiUrl: computed(function () {
|
||||
let origin = window.location.origin;
|
||||
let subdir = this.ghostPaths.subdir;
|
||||
let url = this.ghostPaths.url.join(origin, subdir);
|
||||
|
||||
return url.replace(/\/$/, '');
|
||||
}),
|
||||
|
||||
regeneratedKeyType: computed('isApiKeyRegenerated', 'selectedApiKey', function () {
|
||||
if (this.isApiKeyRegenerated) {
|
||||
return this.get('selectedApiKey.type');
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
allWebhooks: computed(function () {
|
||||
return this.store.peekAll('webhook');
|
||||
}),
|
||||
|
||||
filteredWebhooks: computed('integration.id', 'allWebhooks.@each.{isNew,isDeleted}', function () {
|
||||
return this.allWebhooks.filter((webhook) => {
|
||||
let matchesIntegration = webhook.belongsTo('integration').id() === this.integration.id;
|
||||
|
||||
return matchesIntegration
|
||||
&& !webhook.isNew
|
||||
&& !webhook.isDeleted;
|
||||
});
|
||||
}),
|
||||
|
||||
iconImageStyle: computed('integration.iconImage', function () {
|
||||
let url = this.integration.iconImage;
|
||||
if (url) {
|
||||
let styles = [
|
||||
`background-image: url(${url})`,
|
||||
'background-size: 50%',
|
||||
'background-position: 50%',
|
||||
'background-repeat: no-repeat'
|
||||
];
|
||||
return htmlSafe(styles.join('; '));
|
||||
}
|
||||
|
||||
return htmlSafe('');
|
||||
}),
|
||||
|
||||
actions: {
|
||||
triggerIconFileDialog() {
|
||||
let input = document.querySelector('input[type="file"][name="iconImage"]');
|
||||
input.click();
|
||||
},
|
||||
|
||||
setIconImage([image]) {
|
||||
this.integration.set('iconImage', image.url);
|
||||
},
|
||||
|
||||
save() {
|
||||
return this.save.perform();
|
||||
},
|
||||
|
||||
toggleUnsavedChangesModal(transition) {
|
||||
let leaveTransition = this.leaveScreenTransition;
|
||||
|
||||
if (!transition && this.showUnsavedChangesModal) {
|
||||
this.set('leaveScreenTransition', null);
|
||||
this.set('showUnsavedChangesModal', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||
this.set('leaveScreenTransition', transition);
|
||||
|
||||
// if a save is running, wait for it to finish then transition
|
||||
if (this.save.isRunning) {
|
||||
return this.save.last.then(() => {
|
||||
transition.retry();
|
||||
});
|
||||
}
|
||||
|
||||
// we genuinely have unsaved data, show the modal
|
||||
this.set('showUnsavedChangesModal', true);
|
||||
}
|
||||
},
|
||||
|
||||
leaveScreen() {
|
||||
let transition = this.leaveScreenTransition;
|
||||
|
||||
if (!transition) {
|
||||
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// roll back changes on model props
|
||||
this.integration.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
},
|
||||
|
||||
deleteIntegration() {
|
||||
this.integration.destroyRecord();
|
||||
},
|
||||
|
||||
confirmIntegrationDeletion() {
|
||||
this.set('showDeleteIntegrationModal', true);
|
||||
},
|
||||
|
||||
cancelIntegrationDeletion() {
|
||||
this.set('showDeleteIntegrationModal', false);
|
||||
},
|
||||
|
||||
confirmRegenerateKeyModal(apiKey) {
|
||||
this.set('showRegenerateKeyModal', true);
|
||||
this.set('isApiKeyRegenerated', false);
|
||||
this.set('selectedApiKey', apiKey);
|
||||
},
|
||||
|
||||
cancelRegenerateKeyModal() {
|
||||
this.set('showRegenerateKeyModal', false);
|
||||
},
|
||||
|
||||
regenerateKey() {
|
||||
this.set('isApiKeyRegenerated', true);
|
||||
},
|
||||
|
||||
confirmWebhookDeletion(webhook) {
|
||||
this.set('webhookToDelete', webhook);
|
||||
},
|
||||
|
||||
cancelWebhookDeletion() {
|
||||
this.set('webhookToDelete', null);
|
||||
},
|
||||
|
||||
deleteWebhook() {
|
||||
return this.webhookToDelete.destroyRecord();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
return yield this.integration.save();
|
||||
}),
|
||||
|
||||
copyContentKey: task(function* () {
|
||||
copyTextToClipboard(this.integration.contentKey.secret);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
}),
|
||||
|
||||
copyAdminKey: task(function* () {
|
||||
copyTextToClipboard(this.integration.adminKey.secret);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
}),
|
||||
|
||||
copyApiUrl: task(function* () {
|
||||
copyTextToClipboard(this.apiUrl);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
})
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {alias} from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
webhook: alias('model'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
return this.webhook.save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
// 'new' route's dectivate hook takes care of rollback
|
||||
return this.webhook.get('integration').then((integration) => {
|
||||
this.transitionToRoute('integration', integration);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.webhook.rollbackAttributes();
|
||||
this.webhook.errors.clear();
|
||||
}
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {alias} from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
webhook: alias('model'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
return this.webhook.save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
// 'new' route's dectivate hook takes care of rollback
|
||||
return this.webhook.get('integration').then((integration) => {
|
||||
this.transitionToRoute('settings.integration', integration);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
57
ghost/admin/app/controllers/settings/integrations.js
Normal file
57
ghost/admin/app/controllers/settings/integrations.js
Normal file
@ -0,0 +1,57 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
settings: service(),
|
||||
store: service(),
|
||||
config: service(),
|
||||
|
||||
_allIntegrations: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this._allIntegrations = this.store.peekAll('integration');
|
||||
},
|
||||
|
||||
zapierDisabled: computed('config.hostSettings.limits', function () {
|
||||
return this.config.get('hostSettings.limits.customIntegrations.disabled');
|
||||
}),
|
||||
|
||||
// filter over the live query so that the list is automatically updated
|
||||
// as integrations are added/removed
|
||||
integrations: computed('_allIntegrations.@each.{isNew,type}', function () {
|
||||
return this._allIntegrations.reject((integration) => {
|
||||
return integration.isNew || integration.type !== 'custom';
|
||||
});
|
||||
}),
|
||||
|
||||
// use ember-concurrency so that we can use the derived state to show
|
||||
// a spinner only in the integrations list and avoid delaying the whole
|
||||
// screen display
|
||||
fetchIntegrations: task(function* () {
|
||||
return yield this.store.findAll('integration');
|
||||
}),
|
||||
|
||||
// used by individual integration routes' `model` hooks
|
||||
integrationModelHook(prop, value, route, transition) {
|
||||
let preloadedIntegration = this.store.peekAll('integration').findBy(prop, value);
|
||||
|
||||
if (preloadedIntegration) {
|
||||
return preloadedIntegration;
|
||||
}
|
||||
|
||||
return this.fetchIntegrations.perform().then((integrations) => {
|
||||
let integration = integrations.findBy(prop, value);
|
||||
|
||||
if (!integration) {
|
||||
let path = transition.intent.url.replace(/^\//, '');
|
||||
return route.replaceWith('error404', {path, status: 404});
|
||||
}
|
||||
|
||||
return integration;
|
||||
});
|
||||
}
|
||||
});
|
70
ghost/admin/app/controllers/settings/integrations/amp.js
Normal file
70
ghost/admin/app/controllers/settings/integrations/amp.js
Normal file
@ -0,0 +1,70 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
|
||||
actions: {
|
||||
update(value) {
|
||||
this.settings.set('amp', value);
|
||||
},
|
||||
|
||||
save() {
|
||||
this.save.perform();
|
||||
},
|
||||
|
||||
toggleLeaveSettingsModal(transition) {
|
||||
let leaveTransition = this.leaveSettingsTransition;
|
||||
|
||||
if (!transition && this.showLeaveSettingsModal) {
|
||||
this.set('leaveSettingsTransition', null);
|
||||
this.set('showLeaveSettingsModal', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||
this.set('leaveSettingsTransition', transition);
|
||||
|
||||
// if a save is running, wait for it to finish then transition
|
||||
if (this.save.isRunning) {
|
||||
return this.save.last.then(() => {
|
||||
transition.retry();
|
||||
});
|
||||
}
|
||||
|
||||
// we genuinely have unsaved data, show the modal
|
||||
this.set('showLeaveSettingsModal', true);
|
||||
}
|
||||
},
|
||||
|
||||
leaveSettings() {
|
||||
let transition = this.leaveSettingsTransition;
|
||||
let settings = this.settings;
|
||||
|
||||
if (!transition) {
|
||||
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// roll back changes on settings model
|
||||
settings.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
try {
|
||||
yield this.settings.validate();
|
||||
return yield this.settings.save();
|
||||
} catch (error) {
|
||||
this.notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}).drop()
|
||||
});
|
@ -0,0 +1,70 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
|
||||
actions: {
|
||||
update(value) {
|
||||
this.settings.set('firstpromoter', value);
|
||||
},
|
||||
|
||||
save() {
|
||||
this.save.perform();
|
||||
},
|
||||
|
||||
toggleLeaveSettingsModal(transition) {
|
||||
let leaveTransition = this.leaveSettingsTransition;
|
||||
|
||||
if (!transition && this.showLeaveSettingsModal) {
|
||||
this.set('leaveSettingsTransition', null);
|
||||
this.set('showLeaveSettingsModal', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||
this.set('leaveSettingsTransition', transition);
|
||||
|
||||
// if a save is running, wait for it to finish then transition
|
||||
if (this.save.isRunning) {
|
||||
return this.save.last.then(() => {
|
||||
transition.retry();
|
||||
});
|
||||
}
|
||||
|
||||
// we genuinely have unsaved data, show the modal
|
||||
this.set('showLeaveSettingsModal', true);
|
||||
}
|
||||
},
|
||||
|
||||
leaveSettings() {
|
||||
let transition = this.leaveSettingsTransition;
|
||||
let settings = this.settings;
|
||||
|
||||
if (!transition) {
|
||||
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// roll back changes on settings model
|
||||
settings.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
try {
|
||||
yield this.settings.validate();
|
||||
return yield this.settings.save();
|
||||
} catch (error) {
|
||||
this.notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}).drop()
|
||||
});
|
27
ghost/admin/app/controllers/settings/integrations/new.js
Normal file
27
ghost/admin/app/controllers/settings/integrations/new.js
Normal file
@ -0,0 +1,27 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
133
ghost/admin/app/controllers/settings/integrations/slack.js
Normal file
133
ghost/admin/app/controllers/settings/integrations/slack.js
Normal file
@ -0,0 +1,133 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import {empty} from '@ember/object/computed';
|
||||
import {isInvalidError} from 'ember-ajax/errors';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
ghostPaths: service(),
|
||||
ajax: service(),
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
slackArray: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.slackArray = [];
|
||||
},
|
||||
|
||||
slackSettings: boundOneWay('settings.slack.firstObject'),
|
||||
testNotificationDisabled: empty('slackSettings.url'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.save.perform();
|
||||
},
|
||||
|
||||
updateURL(value) {
|
||||
value = typeof value === 'string' ? value.trim() : value;
|
||||
this.set('slackSettings.url', value);
|
||||
this.get('slackSettings.errors').clear();
|
||||
},
|
||||
|
||||
updateUsername(value) {
|
||||
value = typeof value === 'string' ? value.trimLeft() : value;
|
||||
this.set('slackSettings.username', value);
|
||||
this.get('slackSettings.errors').clear();
|
||||
},
|
||||
|
||||
triggerDirtyState() {
|
||||
let slack = this.slackSettings;
|
||||
let slackArray = this.slackArray;
|
||||
let settings = this.settings;
|
||||
|
||||
// Hack to trigger the `isDirty` state on the settings model by setting a new Array
|
||||
// for slack rather that replacing the existing one which would still point to the
|
||||
// same reference and therfore not setting the model into a dirty state
|
||||
slackArray.clear().pushObject(slack);
|
||||
settings.set('slack', slackArray);
|
||||
},
|
||||
|
||||
toggleLeaveSettingsModal(transition) {
|
||||
let leaveTransition = this.leaveSettingsTransition;
|
||||
|
||||
if (!transition && this.showLeaveSettingsModal) {
|
||||
this.set('leaveSettingsTransition', null);
|
||||
this.set('showLeaveSettingsModal', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||
this.set('leaveSettingsTransition', transition);
|
||||
|
||||
// if a save is running, wait for it to finish then transition
|
||||
if (this.save.isRunning) {
|
||||
return this.save.last.then(() => {
|
||||
transition.retry();
|
||||
});
|
||||
}
|
||||
|
||||
// we genuinely have unsaved data, show the modal
|
||||
this.set('showLeaveSettingsModal', true);
|
||||
}
|
||||
},
|
||||
|
||||
leaveSettings() {
|
||||
let transition = this.leaveSettingsTransition;
|
||||
let settings = this.settings;
|
||||
let slackArray = this.slackArray;
|
||||
|
||||
if (!transition) {
|
||||
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// roll back changes on model props
|
||||
settings.rollbackAttributes();
|
||||
slackArray.clear();
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let slack = this.slackSettings;
|
||||
let settings = this.settings;
|
||||
let slackArray = this.slackArray;
|
||||
|
||||
try {
|
||||
yield slack.validate();
|
||||
// clear existing objects in slackArray to make sure we only push the validated one
|
||||
slackArray.clear().pushObject(slack);
|
||||
yield settings.set('slack', slackArray);
|
||||
return yield settings.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
sendTestNotification: task(function* () {
|
||||
let notifications = this.notifications;
|
||||
let slackApi = this.get('ghostPaths.url').api('slack', 'test');
|
||||
|
||||
try {
|
||||
yield this.save.perform();
|
||||
yield this.ajax.post(slackApi);
|
||||
notifications.showNotification('Test notification sent', {type: 'info', key: 'slack-test.send.success', description: 'Check your Slack channel for the test message'});
|
||||
return true;
|
||||
} catch (error) {
|
||||
notifications.showAPIError(error, {key: 'slack-test:send'});
|
||||
|
||||
if (!isInvalidError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}).drop()
|
||||
});
|
@ -0,0 +1,70 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
notifications: service(),
|
||||
settings: service(),
|
||||
|
||||
leaveSettingsTransition: null,
|
||||
|
||||
actions: {
|
||||
update(value) {
|
||||
this.settings.set('unsplash', value);
|
||||
},
|
||||
|
||||
save() {
|
||||
this.save.perform();
|
||||
},
|
||||
|
||||
toggleLeaveSettingsModal(transition) {
|
||||
let leaveTransition = this.leaveSettingsTransition;
|
||||
|
||||
if (!transition && this.showLeaveSettingsModal) {
|
||||
this.set('leaveSettingsTransition', null);
|
||||
this.set('showLeaveSettingsModal', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||
this.set('leaveSettingsTransition', transition);
|
||||
|
||||
// if a save is running, wait for it to finish then transition
|
||||
if (this.save.isRunning) {
|
||||
return this.save.last.then(() => {
|
||||
transition.retry();
|
||||
});
|
||||
}
|
||||
|
||||
// we genuinely have unsaved data, show the modal
|
||||
this.set('showLeaveSettingsModal', true);
|
||||
}
|
||||
},
|
||||
|
||||
leaveSettings() {
|
||||
let transition = this.leaveSettingsTransition;
|
||||
let settings = this.settings;
|
||||
|
||||
if (!transition) {
|
||||
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
||||
return;
|
||||
}
|
||||
|
||||
// roll back changes on settings model
|
||||
settings.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
try {
|
||||
yield this.settings.validate();
|
||||
return yield this.settings.save();
|
||||
} catch (error) {
|
||||
this.notifications.showAPIError(error);
|
||||
throw error;
|
||||
}
|
||||
}).drop()
|
||||
});
|
65
ghost/admin/app/controllers/settings/integrations/zapier.js
Normal file
65
ghost/admin/app/controllers/settings/integrations/zapier.js
Normal file
@ -0,0 +1,65 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import Controller from '@ember/controller';
|
||||
import config from 'ghost-admin/config/environment';
|
||||
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
ghostPaths: service(),
|
||||
|
||||
selectedApiKey: null,
|
||||
isApiKeyRegenerated: false,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
if (this.isTesting === undefined) {
|
||||
this.isTesting = config.environment === 'test';
|
||||
}
|
||||
},
|
||||
|
||||
integration: alias('model'),
|
||||
|
||||
apiUrl: computed(function () {
|
||||
let origin = window.location.origin;
|
||||
let subdir = this.ghostPaths.subdir;
|
||||
let url = this.ghostPaths.url.join(origin, subdir);
|
||||
|
||||
return url.replace(/\/$/, '');
|
||||
}),
|
||||
|
||||
regeneratedKeyType: computed('isApiKeyRegenerated', 'selectedApiKey', function () {
|
||||
if (this.isApiKeyRegenerated) {
|
||||
return this.get('selectedApiKey.type');
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
confirmRegenerateKeyModal(apiKey) {
|
||||
this.set('showRegenerateKeyModal', true);
|
||||
this.set('isApiKeyRegenerated', false);
|
||||
this.set('selectedApiKey', apiKey);
|
||||
},
|
||||
|
||||
cancelRegenerateKeyModal() {
|
||||
this.set('showRegenerateKeyModal', false);
|
||||
},
|
||||
|
||||
regenerateKey() {
|
||||
this.set('isApiKeyRegenerated', true);
|
||||
}
|
||||
},
|
||||
|
||||
copyAdminKey: task(function* () {
|
||||
copyTextToClipboard(this.integration.adminKey.secret);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
}),
|
||||
|
||||
copyApiUrl: task(function* () {
|
||||
copyTextToClipboard(this.apiUrl);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
})
|
||||
});
|
@ -58,6 +58,19 @@ Router.map(function () {
|
||||
this.route('user', {path: ':user_slug'});
|
||||
});
|
||||
|
||||
this.route('settings.integrations', {path: '/settings/integrations'}, function () {
|
||||
this.route('new');
|
||||
});
|
||||
this.route('settings.integration', {path: '/settings/integrations/:integration_id'}, function () {
|
||||
this.route('webhooks.new', {path: 'webhooks/new'});
|
||||
this.route('webhooks.edit', {path: 'webhooks/:webhook_id'});
|
||||
});
|
||||
this.route('settings.integrations.slack', {path: '/settings/integrations/slack'});
|
||||
this.route('settings.integrations.amp', {path: '/settings/integrations/amp'});
|
||||
this.route('settings.integrations.firstpromoter', {path: '/settings/integrations/firstpromoter'});
|
||||
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
|
||||
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
|
||||
|
||||
this.route('settings.theme', {path: '/settings/theme'}, function () {
|
||||
this.route('uploadtheme');
|
||||
this.route('install');
|
||||
|
@ -6,6 +6,7 @@ import {inject as service} from '@ember/service';
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
router: service(),
|
||||
config: service(),
|
||||
feature: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -40,7 +41,11 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
|
||||
transitionDisabled() {
|
||||
if (this.get('disabled')) {
|
||||
this.transitionTo('integrations');
|
||||
if (this.feature.get('offers')) {
|
||||
this.transitionTo('settings.integrations');
|
||||
} else {
|
||||
this.transitionTo('integrations');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
73
ghost/admin/app/routes/settings/integration.js
Normal file
73
ghost/admin/app/routes/settings/integration.js
Normal file
@ -0,0 +1,73 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
router: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.router.on('routeWillChange', (transition) => {
|
||||
this.showUnsavedChangesModal(transition);
|
||||
if (this.controller) {
|
||||
this.controller.set('selectedApiKey', null);
|
||||
this.controller.set('isApiKeyRegenerated', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
},
|
||||
|
||||
model(params, transition) {
|
||||
// use the integrations controller to fetch all integrations and pick
|
||||
// out the one we want. Allows navigation back to integrations screen
|
||||
// without a loading state
|
||||
return this
|
||||
.controllerFor('settings.integrations')
|
||||
.integrationModelHook('id', params.integration_id, this, transition);
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.controller.set('leaveScreenTransition', null);
|
||||
this.controller.set('showUnsavedChangesModal', false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.controller.send('save');
|
||||
}
|
||||
},
|
||||
|
||||
showUnsavedChangesModal(transition) {
|
||||
if (transition.from && transition.from.name.match(/^settings\.integration\./) && transition.targetName) {
|
||||
let {controller} = this;
|
||||
|
||||
// check to see if we're navigating away from the custom integration
|
||||
// route - we want to allow editing webhooks without showing the
|
||||
// "unsaved changes" confirmation modal
|
||||
let isExternalRoute =
|
||||
// allow sub-routes of integration
|
||||
!(transition.targetName || '').match(/^integration\./)
|
||||
// do not allow changes in integration
|
||||
// .to will be the index, so use .to.parent to get the route with the params
|
||||
|| transition.to.parent.params.integration_id !== controller.integration.id;
|
||||
|
||||
if (isExternalRoute && !controller.integration.isDeleted && controller.integration.hasDirtyAttributes) {
|
||||
transition.abort();
|
||||
controller.send('toggleUnsavedChangesModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Integrations'
|
||||
};
|
||||
}
|
||||
});
|
14
ghost/admin/app/routes/settings/integration/webhooks/edit.js
Normal file
14
ghost/admin/app/routes/settings/integration/webhooks/edit.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model(params) {
|
||||
let integration = this.modelFor('settings.integration');
|
||||
let webhook = integration.webhooks.findBy('id', params.webhook_id);
|
||||
return webhook;
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.controller.reset();
|
||||
}
|
||||
});
|
13
ghost/admin/app/routes/settings/integration/webhooks/new.js
Normal file
13
ghost/admin/app/routes/settings/integration/webhooks/new.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model() {
|
||||
let integration = this.modelFor('settings.integration');
|
||||
return this.store.createRecord('webhook', {integration});
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.controller.webhook.rollbackAttributes();
|
||||
}
|
||||
});
|
25
ghost/admin/app/routes/settings/integrations.js
Normal file
25
ghost/admin/app/routes/settings/integrations.js
Normal file
@ -0,0 +1,25 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
// kick off the background fetch of integrations so that we can
|
||||
// show the screen immediately
|
||||
controller.fetchIntegrations.perform();
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Settings - Integrations'
|
||||
};
|
||||
}
|
||||
});
|
39
ghost/admin/app/routes/settings/integrations/amp.js
Normal file
39
ghost/admin/app/routes/settings/integrations/amp.js
Normal file
@ -0,0 +1,39 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
|
||||
return this.settings.reload();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.controller.send('save');
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
let controller = this.controller;
|
||||
let modelIsDirty = this.settings.get('hasDirtyAttributes');
|
||||
|
||||
if (modelIsDirty) {
|
||||
transition.abort();
|
||||
controller.send('toggleLeaveSettingsModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'AMP'
|
||||
};
|
||||
}
|
||||
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
|
||||
return this.settings.reload();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.controller.send('save');
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
let controller = this.controller;
|
||||
let modelIsDirty = this.settings.get('hasDirtyAttributes');
|
||||
|
||||
if (modelIsDirty) {
|
||||
transition.abort();
|
||||
controller.send('toggleLeaveSettingsModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'FirstPromoter'
|
||||
};
|
||||
}
|
||||
|
||||
});
|
31
ghost/admin/app/routes/settings/integrations/new.js
Normal file
31
ghost/admin/app/routes/settings/integrations/new.js
Normal file
@ -0,0 +1,31 @@
|
||||
import RSVP from 'rsvp';
|
||||
import Route from '@ember/routing/route';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Route.extend({
|
||||
limit: service(),
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.controller.integration.rollbackAttributes();
|
||||
}
|
||||
});
|
39
ghost/admin/app/routes/settings/integrations/slack.js
Normal file
39
ghost/admin/app/routes/settings/integrations/slack.js
Normal file
@ -0,0 +1,39 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
|
||||
return this.settings.reload();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.controller.send('save');
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
let controller = this.controller;
|
||||
let settings = this.settings;
|
||||
let modelIsDirty = settings.get('hasDirtyAttributes');
|
||||
|
||||
if (modelIsDirty) {
|
||||
transition.abort();
|
||||
controller.send('toggleLeaveSettingsModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Slack'
|
||||
};
|
||||
}
|
||||
});
|
38
ghost/admin/app/routes/settings/integrations/unsplash.js
Normal file
38
ghost/admin/app/routes/settings/integrations/unsplash.js
Normal file
@ -0,0 +1,38 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
settings: service(),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
|
||||
return this.settings.reload();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.controller.send('save');
|
||||
},
|
||||
|
||||
willTransition(transition) {
|
||||
let controller = this.controller;
|
||||
let modelIsDirty = this.settings.get('hasDirtyAttributes');
|
||||
|
||||
if (modelIsDirty) {
|
||||
transition.abort();
|
||||
controller.send('toggleLeaveSettingsModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Unsplash'
|
||||
};
|
||||
}
|
||||
});
|
52
ghost/admin/app/routes/settings/integrations/zapier.js
Normal file
52
ghost/admin/app/routes/settings/integrations/zapier.js
Normal file
@ -0,0 +1,52 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
router: service(),
|
||||
config: service(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.router.on('routeWillChange', () => {
|
||||
if (this.controller) {
|
||||
this.controller.set('selectedApiKey', null);
|
||||
this.controller.set('isApiKeyRegenerated', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
disabled: computed('config.hostSettings.limits', function () {
|
||||
return this.config.get('hostSettings.limits.customIntegrations.disabled');
|
||||
}),
|
||||
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.transitionDisabled();
|
||||
this.transitionAuthor(this.session.user);
|
||||
this.transitionEditor(this.session.user);
|
||||
},
|
||||
|
||||
model(params, transition) {
|
||||
// use the integrations controller to fetch all integrations and pick
|
||||
// out the one we want. Allows navigation back to integrations screen
|
||||
// without a loading state
|
||||
return this
|
||||
.controllerFor('integrations')
|
||||
.integrationModelHook('slug', 'zapier', this, transition);
|
||||
},
|
||||
|
||||
transitionDisabled() {
|
||||
if (this.get('disabled')) {
|
||||
this.transitionTo('settings.integrations');
|
||||
}
|
||||
},
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Zapier'
|
||||
};
|
||||
}
|
||||
});
|
@ -79,7 +79,7 @@
|
||||
<div class="gh-setting-header">Advanced</div>
|
||||
<div class="gh-settings-main-grid">
|
||||
{{#if (feature "offers")}}
|
||||
<LinkTo class="gh-setting-group" @route="integrations" data-test-nav="navigation">
|
||||
<LinkTo class="gh-setting-group" @route="settings.integrations" data-test-nav="navigation">
|
||||
<span class="yellow">{{svg-jar "module"}}</span>
|
||||
<div>
|
||||
<h4>Integrations</h4>
|
||||
|
285
ghost/admin/app/templates/settings/integration.hbs
Normal file
285
ghost/admin/app/templates/settings/integration.hbs
Normal file
@ -0,0 +1,285 @@
|
||||
<section class="gh-canvas">
|
||||
<form {{action (perform "save") on="submit"}}>
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{this.integration.name}}
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @task={{this.save}} @class="gh-btn gh-btn-primary gh-btn-icon" data-test-button="save" />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<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">
|
||||
<div class="flex">
|
||||
<div class="flex flex-column items-start app-custom-icon-container">
|
||||
<label class="mb1">Icon</label>
|
||||
<figure class="app-custom-icon hide-child" style={{this.iconImageStyle}}>
|
||||
<AspectRatioBox @class="flex items-center h-100 justify-center" @ratio="1/1" @base="height">
|
||||
{{#unless this.integration.iconImage}}
|
||||
{{svg-jar "integration" class="w11 h11"}}
|
||||
{{/unless}}
|
||||
</AspectRatioBox>
|
||||
|
||||
<GhUploader
|
||||
@extensions={{this.imageExtensions}}
|
||||
@onComplete={{action "setIconImage"}}
|
||||
as |uploader|
|
||||
>
|
||||
{{#if uploader.isUploading}}
|
||||
<div class="absolute top-0 left-0 w-100 h-100 br4 bg-black-70 flex items-center">
|
||||
{{uploader.progressBar}}
|
||||
</div>
|
||||
{{else}}
|
||||
<button
|
||||
type="button"
|
||||
class="child app-custom-icon-uploadlabel"
|
||||
{{action "triggerIconFileDialog"}}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
{{/if}}
|
||||
<div style="display:none">
|
||||
<GhFileInput
|
||||
@name="iconImage"
|
||||
@multiple={{false}}
|
||||
@action={{uploader.setFiles}}
|
||||
@accept={{this.imageMimeTypes}} data-test-file-input="icon" />
|
||||
</div>
|
||||
</GhUploader>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="flex-auto">
|
||||
<GhValidationStatusContainer
|
||||
@class="flex flex-column w-100 mr3"
|
||||
@errors={{this.integration.errors}}
|
||||
@hasValidated={{this.integration.hasValidated}}
|
||||
@property="name"
|
||||
>
|
||||
<label for="integration_name">Name</label>
|
||||
<GhTextInput
|
||||
@id="integration_name"
|
||||
@class="gh-input mt1 mb1"
|
||||
@type="text"
|
||||
@value={{readonly this.integration.name}}
|
||||
@input={{action (mut this.integration.name) value="target.value"}}
|
||||
@focus-out={{action "validate" "name" target=this.integration}}
|
||||
data-test-input="name"
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.integration.errors}} @property="name" data-test-error="name" class="ma0" />
|
||||
</GhValidationStatusContainer>
|
||||
|
||||
<GhValidationStatusContainer
|
||||
@class="flex flex-column w-100 mr3"
|
||||
@errors={{this.integration.errors}}
|
||||
@hasValidated={{this.integration.hasValidated}}
|
||||
@property="decription"
|
||||
>
|
||||
<label for="integration_description" class="mt3">Description</label>
|
||||
<GhTextInput
|
||||
@id="integration_description"
|
||||
@class="gh-input mt1"
|
||||
@type="text"
|
||||
@value={{readonly this.integration.description}}
|
||||
@input={{action (mut this.integration.description) value="target.value"}}
|
||||
@focus-out={{action "validate" "description" target=this.integration}}
|
||||
data-test-input="description"
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.integration.errors}} @property="description" data-test-error="description" class="ma0" />
|
||||
</GhValidationStatusContainer>
|
||||
|
||||
<table class="app-custom-api-table list" style="table-layout: fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="data-label">Content API key</td>
|
||||
<td class="data highlight-hover">
|
||||
<div class="relative flex items-center {{unless this.copyContentKey.isRunning "hide-child-instant"}}">
|
||||
<span class="truncate" data-test-text="content-key">
|
||||
{{this.integration.contentKey.secret}}
|
||||
</span>
|
||||
<div class="app-api-buttons child">
|
||||
<button type="button" {{action "confirmRegenerateKeyModal" this.integration.contentKey}} class="app-button-regenerate" data-tooltip="Regenerate">
|
||||
{{svg-jar "reload" class="w4 h4 stroke-midgrey"}}
|
||||
</button>
|
||||
<button type="button" {{action (perform this.copyContentKey)}} class="app-button-copy">
|
||||
{{#if this.copyContentKey.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (eq this.regeneratedKeyType this.integration.contentKey.type)}}
|
||||
<div class="green nt3 mb2"> Content API Key was successfully regenerated </div>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="data-label">Admin API key</td>
|
||||
<td class="data highlight-hover">
|
||||
<div class="relative flex items-center {{unless this.copyAdminKey.isRunning "hide-child-instant"}}">
|
||||
<span class="truncate" data-test-text="admin-key">
|
||||
{{this.integration.adminKey.secret}}
|
||||
</span>
|
||||
<div class="app-api-buttons child">
|
||||
<button type="button" {{action "confirmRegenerateKeyModal" this.integration.adminKey}} class="app-button-regenerate" data-tooltip="Regenerate">
|
||||
{{svg-jar "reload" class="w4 h4 stroke-midgrey"}}
|
||||
</button>
|
||||
<button type="button" {{action (perform this.copyAdminKey)}} class="app-button-copy">
|
||||
{{#if this.copyAdminKey.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (eq this.regeneratedKeyType this.integration.adminKey.type)}}
|
||||
<div class="green"> Admin API key was successfully regenerated </div>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="data-label">API URL</td>
|
||||
<td class="data highlight-hover truncate">
|
||||
<div class="relative flex items-center {{unless this.copyApiUrl.isRunning "hide-child-instant"}}">
|
||||
<span data-test-text="api-url">
|
||||
{{this.apiUrl}}
|
||||
</span>
|
||||
<div class="app-api-buttons child">
|
||||
<button type="button" {{action (perform this.copyApiUrl)}} class="app-button-copy">
|
||||
{{#if this.copyApiUrl.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small">Webhooks</h4>
|
||||
<div class="gh-main-section-block p0">
|
||||
<table class="gh-list small">
|
||||
<thead>
|
||||
<tr class="gh-list-row header">
|
||||
<th class="gh-list-header">Name</th>
|
||||
<th class="gh-list-header">Event</th>
|
||||
<th class="gh-list-header">URL</th>
|
||||
<th class="gh-list-header">Last triggered</th>
|
||||
<th class="gh-list-header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.filteredWebhooks as |webhook|}}
|
||||
<tr class="gh-list-row hide-child" data-test-webhook-row="{{webhook.id}}">
|
||||
<td class="gh-list-data" data-test-text="name">{{webhook.name}}</td>
|
||||
<td class="gh-list-data" data-test-text="event">{{event-name webhook.event}}</td>
|
||||
<td class="gh-list-data" data-test-text="targetUrl">{{webhook.targetUrl}}</td>
|
||||
<td class="gh-list-data" data-test-text="last-triggered">{{or webhook.lastTriggeredAtUTC "Not triggered"}}</td>
|
||||
<td class="w1 gh-list-data nowrap">
|
||||
<div class="child flex items-center">
|
||||
<LinkTo @route="settings.integration.webhooks.edit" @models={{array this.integration webhook}} data-test-link="edit-webhook">
|
||||
{{svg-jar "pen" class="w6 h6 fill-midgrey pa1 mr1"}}
|
||||
</LinkTo>
|
||||
<button {{action "confirmWebhookDeletion" webhook}} data-test-button="delete-webhook">
|
||||
{{svg-jar "trash" class="w6 fill-red pa1"}}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr class="bt b--whitegrey" data-test-webhooks-blank-slate>
|
||||
<td colspan="5" class="pa5 pt15 pb15 tc midgrey f7">
|
||||
<div class="flex flex-column items-center">
|
||||
<p class="ma0 pa0 tc midgrey lh-title mt2">
|
||||
No webhooks configured
|
||||
</p>
|
||||
<LinkTo @route="settings.integration.webhooks.new" @model={{this.integration}} @classNames="flex items-center" data-test-link="add-webhook">
|
||||
<div class="flex items-center pa2 pt1">
|
||||
{{svg-jar "add" class="w3 h3 fill-green-d1"}}
|
||||
<span class="ml1 green">Add webhook</span>
|
||||
</div>
|
||||
</LinkTo>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
{{#if this.filteredWebhooks}}
|
||||
<tfoot>
|
||||
<tr class="gh-list-row new-webhook-cell">
|
||||
<td colspan="5" class="gh-list-data">
|
||||
<LinkTo @route="settings.integration.webhooks.new" @model={{this.integration}} @classNames="flex items-center" data-test-link="add-webhook">
|
||||
<div class="pt1 pb1 f7">
|
||||
{{svg-jar "add" class="w3 h3 fill-green-d1"}}
|
||||
<span class="ml1 green">Add webhook</span>
|
||||
</div>
|
||||
</LinkTo>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{{/if}}
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<div class="gh-main-section-block">
|
||||
<button class="gh-btn gh-btn-red gh-btn-icon" {{action "confirmIntegrationDeletion"}}>
|
||||
<span> Delete integration </span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if this.showUnsavedChangesModal}}
|
||||
<GhFullscreenModal @modal="leave-settings"
|
||||
@confirm={{action "leaveScreen"}}
|
||||
@close={{action "toggleUnsavedChangesModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showRegenerateKeyModal}}
|
||||
<GhFullscreenModal @modal="regenerate-key"
|
||||
@model={{hash
|
||||
apiKey=this.selectedApiKey
|
||||
integration=this.integration
|
||||
}}
|
||||
@confirm={{action "regenerateKey"}}
|
||||
@close={{action "cancelRegenerateKeyModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showDeleteIntegrationModal}}
|
||||
<GhFullscreenModal @modal="delete-integration"
|
||||
@confirm={{action "deleteIntegration"}}
|
||||
@close={{action "cancelIntegrationDeletion"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.webhookToDelete}}
|
||||
<GhFullscreenModal @modal="delete-webhook"
|
||||
@confirm={{action "deleteWebhook"}}
|
||||
@close={{action "cancelWebhookDeletion"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
@ -0,0 +1,5 @@
|
||||
<GhFullscreenModal @modal="webhook-form"
|
||||
@model={{this.webhook}}
|
||||
@confirm={{action "save"}}
|
||||
@close={{action "cancel"}}
|
||||
@modifier="action wide" />
|
@ -0,0 +1,5 @@
|
||||
<GhFullscreenModal @modal="webhook-form"
|
||||
@model={{this.webhook}}
|
||||
@confirm={{action "save"}}
|
||||
@close={{action "cancel"}}
|
||||
@modifier="action wide" />
|
249
ghost/admin/app/templates/settings/integrations.hbs
Normal file
249
ghost/admin/app/templates/settings/integrations.hbs
Normal file
@ -0,0 +1,249 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Integrations
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<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">
|
||||
<img class="w-100 h-100" style="object-fit:contain;" src="assets/img/disqus.svg" alt="Disqus Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Disqus</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/google/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-analytics">
|
||||
<img class="w-100 h-100" style="object-fit:contain;padding:1px 0;" src="assets/img/google-analytics.png" alt="Google Analytics Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Analytics</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/ulysses/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-ulysses">
|
||||
<img class="w-100 h-100" style="object-fit:contain;" src="assets/img/ulysses.png" alt="Ulysses Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Ulysses</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/typeform/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-typeform">
|
||||
<img class="w-100 h-100" style="object-fit:contain;" src="assets/img/typeform.svg" alt="Typeform Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Typeform</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/buffer/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-buffer">
|
||||
<img class="w-100 h-100" style="object-fit:contain;padding:1px 0;" src="assets/img/buffer.png" alt="Buffer Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Buffer</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/plausible/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-plausible">
|
||||
<img class="w-100 h-100" style="object-fit:contain;padding:1px 0;" src="assets/img/plausible.png" alt="Plausible Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">Plausible</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/github/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-github">
|
||||
<img class="w-100 h-100" style="object-fit:contain;" src="assets/img/github.svg" alt="GitHub Icon" />
|
||||
</div>
|
||||
<div class="f8 mt1">GitHub</div>
|
||||
</a>
|
||||
<a class="id-item" href="https://ghost.org/integrations/" target="_blank" rel="noopener noreferrer">
|
||||
<div class="id-item-logo id-more">
|
||||
{{!-- <img class="w-100 h-100" style="object-fit:contain;padding:0 8px;" src="assets/img/more.png" alt="Three dots" /> --}}
|
||||
{{svg-jar "circle-ellipsis" class="w5"}}
|
||||
</div>
|
||||
<div class="f8 mt1">More</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small">Built-in integrations</h4>
|
||||
<div class="apps-grid">
|
||||
{{#unless this.zapierDisabled}}
|
||||
<div class="apps-grid-cell" data-test-app="zapier">
|
||||
<LinkTo @route="settings.integrations.zapier" data-test-link="zapier">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/zapier.svg);background-size:36px;"></figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Zapier</h3>
|
||||
<p class="apps-card-app-desc">Automation for your favorite apps</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
<span data-test-app-status>Configure</span>
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="apps-grid-cell" data-test-app="slack">
|
||||
<LinkTo @route="settings.integrations.slack" data-test-link="slack">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/slackicon.png); background-size: 36px;"></figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Slack</h3>
|
||||
<p class="apps-card-app-desc">A messaging app for teams</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
{{#if this.settings.slack.isActive}}
|
||||
<span class="gh-badge" data-test-app-status>Active</span>
|
||||
{{else}}
|
||||
<span data-test-app-status>Configure</span>
|
||||
{{/if}}
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
<div class="apps-grid-cell" data-test-app="amp">
|
||||
<LinkTo @route="settings.integrations.amp" data-test-link="amp">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/amp.svg); background-size: 36px;"></figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">AMP</h3>
|
||||
<p class="apps-card-app-desc">Google Accelerated Mobile Pages</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
{{#if this.settings.amp}}
|
||||
<span class="gh-badge" data-test-app-status>Active</span>
|
||||
{{else}}
|
||||
<span data-test-app-status>Configure</span>
|
||||
{{/if}}
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
<div class="apps-grid-cell" data-test-app="unsplash">
|
||||
<LinkTo @route="settings.integrations.unsplash" data-test-link="unsplash">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon id-unsplash" style="background-image:url(assets/icons/unsplash.svg); background-size:30px;"></figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Unsplash</h3>
|
||||
<p class="apps-card-app-desc">Beautiful, free photos</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
{{#if this.settings.unsplash}}
|
||||
<span class="gh-badge" data-test-app-status>Active</span>
|
||||
{{else}}
|
||||
<span data-test-app-status>Configure</span>
|
||||
{{/if}}
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
<div class="apps-grid-cell" data-test-app="firstpromoter">
|
||||
<LinkTo @route="settings.integrations.firstpromoter" data-test-link="firstpromoter">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon id-unsplash" style="background-image:url(assets/icons/firstpromoter.png); background-size:30px;"></figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">FirstPromoter</h3>
|
||||
<p class="apps-card-app-desc">Launch your member referral program</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
{{#if this.settings.firstpromoter}}
|
||||
<span class="gh-badge" data-test-app-status>Active</span>
|
||||
{{else}}
|
||||
<span data-test-app-status>Configure</span>
|
||||
{{/if}}
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small">Custom integrations</h4>
|
||||
<div class="apps-grid">
|
||||
{{#each this.integrations as |integration|}}
|
||||
<div class="apps-grid-cell" data-test-custom-integration>
|
||||
<LinkTo @route="settings.integration" @model={{integration}} data-test-integration={{integration.id}}>
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<figure class="apps-card-app-icon flex items-center" style={{integration-icon-style integration}}>
|
||||
{{#unless integration.iconImage}}
|
||||
{{svg-jar "integration" class="nudge-left--6 w9 stroke-darkgrey"}}
|
||||
{{/unless}}
|
||||
</figure>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title" data-test-text="name">
|
||||
{{integration.name}}
|
||||
</h3>
|
||||
<p class="apps-card-app-desc" data-test-text="description">
|
||||
{{integration.description}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<div class="apps-configured">
|
||||
<span>Configure</span>
|
||||
{{svg-jar "arrow-right"}}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex flex-column justify-center items-center mih30 miw-100 bb b--whitegrey" data-test-blank="custom-integrations">
|
||||
{{#if this.fetchIntegrations.isRunning}}
|
||||
<div class="gh-loading-spinner"></div>
|
||||
{{else}}
|
||||
<div class="flex flex-column items-center pa5 pt15 pb15">
|
||||
<p class="ma0 pa0 tc midgrey lh-title mt2 mb4">
|
||||
Create your own custom Ghost integrations with dedicated API keys & webhooks
|
||||
</p>
|
||||
<LinkTo @route="settings.integrations.new" class="gh-btn gh-btn-green gh-btn-icon" data-test-button="new-integration">
|
||||
<span>{{svg-jar "add" class="w3 h3"}} Add custom integration</span>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if this.integrations}}
|
||||
{{!-- <div class="apps-grid-cell new-integration-cell"> --}}
|
||||
<div class="mt5">
|
||||
<LinkTo @route="settings.integrations.new" class="gh-btn gh-btn-green gh-btn-icon" data-test-button="new-integration">
|
||||
<span>{{svg-jar "add" class="w3 h3"}} Add custom integration</span>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{!-- </div> --}}
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
{{outlet}}
|
@ -0,0 +1,16 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
AMP
|
||||
</h2>
|
||||
<section class="view-actions"></section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-content">
|
||||
<GhLoadingSpinner />
|
||||
</div>
|
||||
</section>
|
87
ghost/admin/app/templates/settings/integrations/amp.hbs
Normal file
87
ghost/admin/app/templates/settings/integrations/amp.hbs
Normal file
@ -0,0 +1,87 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
AMP
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @task={{this.save}} @class="gh-btn gh-btn-primary gh-btn-icon" data-test-save-button={{true}} />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
{{#if this.showLeaveSettingsModal}}
|
||||
<GhFullscreenModal @modal="leave-settings"
|
||||
@confirm={{action "leaveSettings"}}
|
||||
@close={{action "toggleLeaveSettingsModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
<section class="view-container">
|
||||
<section class="gh-main-section bt app-grid">
|
||||
<div class="gh-main-section-block app-detail-heading app-grid">
|
||||
<div class="app-cell">
|
||||
<img class="app-icon" src="assets/img/amp.svg" />
|
||||
</div>
|
||||
<div class="app-cell">
|
||||
<h3>AMP</h3>
|
||||
<p>Accelerated Mobile Pages</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small bn">AMP configuration</h4>
|
||||
<section class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey">
|
||||
<div>
|
||||
<div class="gh-setting-first {{unless this.settings.amp "gh-setting-last"}}">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Enable AMP</div>
|
||||
<div class="gh-setting-desc mb0">Enable <a href="https://ampproject.org" target="_blank">Google Accelerated Mobile Pages</a> for your posts</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-checkbox">
|
||||
<label for="amp" class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{this.settings.amp}}
|
||||
id="amp"
|
||||
name="amp"
|
||||
onclick={{action "update" value="target.checked"}}
|
||||
data-test-amp-checkbox
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#liquid-if this.settings.amp class=""}}
|
||||
<div class="gh-setting-last gh-setting-amp-liquid">
|
||||
<div class="gh-setting-content gh-setting-content--no-action">
|
||||
<div class="gh-setting-title">Google Analytics Tracking ID</div>
|
||||
<div class="gh-setting-desc">Tracks AMP traffic in Google Analytics, find your ID <a href="https://ghost.org/help/how-to-find-your-google-analytics-tracking-id/">here</a></div>
|
||||
<div class="gh-setting-content-extended">
|
||||
<GhFormGroup @class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="ampGtagId">
|
||||
<GhTextInput
|
||||
@placeholder="UA-XXXXXXX-X"
|
||||
@name="amp_gtag_id"
|
||||
@value={{this.settings.ampGtagId}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action "save")
|
||||
}}
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.settings.errors}} @property="ampGtagId"/>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
@ -0,0 +1,87 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
FirstPromoter
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @task={{this.save}} @class="gh-btn gh-btn-primary gh-btn-icon" data-test-save-button={{true}} />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
{{#if this.showLeaveSettingsModal}}
|
||||
<GhFullscreenModal @modal="leave-settings"
|
||||
@confirm={{action "leaveSettings"}}
|
||||
@close={{action "toggleLeaveSettingsModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
<section class="view-container">
|
||||
<section class="gh-main-section bt app-grid">
|
||||
<div class="gh-main-section-block app-detail-heading app-grid">
|
||||
<div class="app-cell">
|
||||
<img class="app-icon" src="assets/img/firstpromoter.png" />
|
||||
</div>
|
||||
<div class="app-cell">
|
||||
<h3>FirstPromoter</h3>
|
||||
<p>Launch your own member referral program</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small bn">FirstPromoter configuration</h4>
|
||||
<section class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey">
|
||||
<div>
|
||||
<div class="gh-setting-first {{unless this.settings.firstpromoter "gh-setting-last"}}">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Enable FirstPromoter</div>
|
||||
<div class="gh-setting-desc mb0">Enable <a href="https://firstpromoter.com/?fpr=ghost&fp_sid=admin" target="_blank">FirstPromoter</a> for tracking referrals</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-checkbox">
|
||||
<label for="firstpromoter" class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{this.settings.firstpromoter}}
|
||||
id="firstpromoter"
|
||||
name="firstpromoter"
|
||||
onclick={{action "update" value="target.checked"}}
|
||||
data-test-firstpromoter-checkbox
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#liquid-if this.settings.firstpromoter class=""}}
|
||||
<div class="gh-setting-last gh-setting-firstpromoter-liquid">
|
||||
<div class="gh-setting-content gh-setting-content--no-action">
|
||||
<div class="gh-setting-title">FirstPromoter Account ID</div>
|
||||
<div class="gh-setting-desc"> Affiliate and referral tracking, find your ID <a href="https://ghost.org/help/firstpromoter-id/">here</a></div>
|
||||
<div class="gh-setting-content-extended">
|
||||
<GhFormGroup @class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="firstpromoterId">
|
||||
<GhTextInput
|
||||
@placeholder="XXXXXXXX"
|
||||
@name="firstpromoter_id"
|
||||
@value={{this.settings.firstpromoterId}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action "save")
|
||||
}}
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.settings.errors}} @property="firstpromoterId"/>
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
14
ghost/admin/app/templates/settings/integrations/new.hbs
Normal file
14
ghost/admin/app/templates/settings/integrations/new.hbs
Normal file
@ -0,0 +1,14 @@
|
||||
{{#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}}
|
@ -0,0 +1,16 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Slack
|
||||
</h2>
|
||||
<section class="view-actions"></section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-content">
|
||||
<GhLoadingSpinner />
|
||||
</div>
|
||||
</section>
|
99
ghost/admin/app/templates/settings/integrations/slack.hbs
Normal file
99
ghost/admin/app/templates/settings/integrations/slack.hbs
Normal file
@ -0,0 +1,99 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Slack
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @task={{this.save}} @class="gh-btn gh-btn-primary gh-btn-icon" data-test-save-button={{true}} />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
{{#if this.showLeaveSettingsModal}}
|
||||
<GhFullscreenModal @modal="leave-settings"
|
||||
@confirm={{action "leaveSettings"}}
|
||||
@close={{action "toggleLeaveSettingsModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
<section class="view-container">
|
||||
<section class="gh-main-section bt app-grid">
|
||||
<div class="gh-main-section-block app-detail-heading app-grid">
|
||||
<div class="app-cell">
|
||||
<img class="app-icon" src="assets/img/slackicon.png" />
|
||||
</div>
|
||||
<div class="app-cell">
|
||||
<h3>Slack</h3>
|
||||
<p>A messaging app for teams</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small bn">Slack configuration</h4>
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey">
|
||||
<form class="app-config-form" id="slack-settings" novalidate="novalidate" {{action "save" on="submit"}}>
|
||||
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-setting-first">
|
||||
<div class="gh-setting-content gh-setting-content--no-action">
|
||||
<div class="gh-setting-title">Webhook URL</div>
|
||||
<div class="gh-setting-desc">Automatically send newly published posts to a channel in Slack or any Slack-compatible service like Discord or Mattermost.</div>
|
||||
<div class="gh-setting-content-extended">
|
||||
<GhFormGroup @errors={{this.slackSettings.errors}} @hasValidated={{this.slackSettings.hasValidated}} @property="url">
|
||||
<GhTextInput
|
||||
@placeholder="https://hooks.slack.com/services/..."
|
||||
@name="slack[url]"
|
||||
@value={{readonly this.slackSettings.url}}
|
||||
@input={{action "updateURL" value="target.value"}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action "save")
|
||||
}}
|
||||
@focus-out={{action "triggerDirtyState"}}
|
||||
data-test-slack-url-input={{true}}
|
||||
/>
|
||||
{{#unless this.slackSettings.errors.url}}
|
||||
<p>Set up a new incoming webhook <a href="https://my.slack.com/apps/new/A0F7XDUAZ-incoming-webhooks" target="_blank">here</a>, and grab the URL.</p>
|
||||
{{else}}
|
||||
<GhErrorMessage @errors={{this.slackSettings.errors}} @property="url" data-test-error="slack-url" />
|
||||
{{/unless}}
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-setting-last">
|
||||
<div class="gh-setting-content gh-setting-content--no-action">
|
||||
<div class="gh-setting-title">Username</div>
|
||||
<div class="gh-setting-desc">The username to display messages from</div>
|
||||
<div class="gh-setting-content-extended">
|
||||
<GhFormGroup @errors={{this.slackSettings.errors}} @hasValidated={{this.slackSettings.hasValidated}} @property="username">
|
||||
<GhTextInput
|
||||
@placeholder="Ghost"
|
||||
@name="slack[username]"
|
||||
@value={{readonly this.slackSettings.username}}
|
||||
@input={{action "updateUsername" value="target.value"}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action "save")
|
||||
}}
|
||||
@focus-out={{action "triggerDirtyState"}}
|
||||
data-test-slack-username-input={{true}}
|
||||
/>
|
||||
{{#if this.slackSettings.errors.username}}
|
||||
<GhErrorMessage @errors={{this.slackSettings.errors}} @property="username" />
|
||||
{{/if}}
|
||||
</GhFormGroup>
|
||||
</div>
|
||||
<GhTaskButton @buttonText="Send test notification" @task={{this.sendTestNotification}} @successText="Sent" @class="gh-btn gh-btn-icon" @disabled={{this.testNotificationDisabled}} data-test-send-notification-button="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
@ -0,0 +1,16 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Unsplash
|
||||
</h2>
|
||||
<section class="view-actions"></section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="gh-content">
|
||||
<GhLoadingSpinner />
|
||||
</div>
|
||||
</section>
|
66
ghost/admin/app/templates/settings/integrations/unsplash.hbs
Normal file
66
ghost/admin/app/templates/settings/integrations/unsplash.hbs
Normal file
@ -0,0 +1,66 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Unsplash
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
<GhTaskButton @task={{this.save}} @class="gh-btn gh-btn-primary gh-btn-icon" data-test-save-button={{true}} />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
{{#if this.showLeaveSettingsModal}}
|
||||
<GhFullscreenModal @modal="leave-settings"
|
||||
@confirm={{action "leaveSettings"}}
|
||||
@close={{action "toggleLeaveSettingsModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
<section class="view-container">
|
||||
<section class="gh-main-section bt app-grid">
|
||||
<div class="gh-main-section-block app-detail-heading app-grid">
|
||||
<div class="app-cell">
|
||||
<img class="app-icon pa2 id-unsplash" src="assets/icons/unsplash.svg" />
|
||||
</div>
|
||||
<div class="app-cell">
|
||||
<h3>Unsplash</h3>
|
||||
<p>Beautiful, free photos</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-main-section">
|
||||
<h4 class="gh-main-section-header small bn">Unsplash configuration</h4>
|
||||
<div class="gh-main-section-block">
|
||||
<div class="gh-main-section-content grey">
|
||||
<div class="gh-setting-first gh-setting-last" id="unsplash-toggle">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Enable Unsplash</div>
|
||||
<div class="gh-setting-desc mb0">Enable <a href="https://unsplash.com" target="_blank">Unsplash</a> image integration for your posts</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="form-group right gh-setting-unsplash-checkbox">
|
||||
<div class="for-checkbox">
|
||||
<label for="unsplash" class="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{this.settings.unsplash}}
|
||||
id="unsplash"
|
||||
name="unsplash"
|
||||
onclick={{action "update" value="target.checked"}}
|
||||
data-test-unsplash-checkbox
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
256
ghost/admin/app/templates/settings/integrations/zapier.hbs
Normal file
256
ghost/admin/app/templates/settings/integrations/zapier.hbs
Normal file
@ -0,0 +1,256 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
<LinkTo @route="settings">Settings</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
<LinkTo @route="settings.integrations" data-test-link="integrations-back">Integrations</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
Zapier
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
|
||||
<section class="gh-main-section no-heading">
|
||||
<div class="gh-main-section-block overflow-hidden">
|
||||
<div class="gh-main-section-content app-detail-heading app-grid">
|
||||
<div class="app-cell">
|
||||
<img class="app-icon" src="assets/img/zapier.svg" />
|
||||
</div>
|
||||
<div class="app-cell overflow-hidden">
|
||||
<h3>Zapier</h3>
|
||||
<p>Automation for your favorite apps</p>
|
||||
|
||||
<div class="gh-zapier-data-container">
|
||||
<div class="gh-zapier-data">
|
||||
<div class="data-label">Admin API key</div>
|
||||
<div class="data highlight-hover">
|
||||
<div class="relative flex items-center {{unless this.copyAdminKey.isRunning "hide-child-instant"}}">
|
||||
<span class="admin-key" data-test-text="admin-key">
|
||||
{{this.integration.adminKey.secret}}
|
||||
</span>
|
||||
<div class="app-api-buttons child">
|
||||
<button type="button" {{action "confirmRegenerateKeyModal" this.integration.adminKey}} class="app-button-regenerate">
|
||||
{{svg-jar "reload" class="w4 h4 stroke-midgrey"}}
|
||||
</button>
|
||||
<button type="button" {{action (perform this.copyAdminKey)}} class="app-button-copy">
|
||||
{{#if this.copyAdminKey.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (eq this.regeneratedKeyType this.integration.adminKey.type)}}
|
||||
<div class="green"> Admin API Key was successfully regenerated </div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-zapier-data">
|
||||
<div class="data-label">API URL</div>
|
||||
<div class="data highlight-hover">
|
||||
<div class="relative flex items-center {{unless this.copyApiUrl.isRunning "hide-child-instant"}}">
|
||||
<span class="api-url" data-test-text="api-url">
|
||||
{{this.apiUrl}}
|
||||
</span>
|
||||
<div class="app-api-buttons child">
|
||||
<button type="button" {{action (perform this.copyApiUrl)}} class="app-button-copy">
|
||||
{{#if this.copyApiUrl.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
{{/if}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="zapier-toggle" class="gh-main-section">
|
||||
<h4 class="gh-main-section-header large bn">Zapier templates</h4>
|
||||
<p class="gh-main-section-description">Explore pre-built templates for common automation tasks</p>
|
||||
<div class="gh-setting-content gh-setting-content--no-action">
|
||||
<div class="gh-setting-content-extended">
|
||||
<div class="apps-grid gh-settings-zapier-templates">
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb" style="background-image:url(assets/img/logos/orb-black-1.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/twitter.svg);background-size:32px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Share new posts to Twitter</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/50909" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb" style="background-image:url(assets/img/logos/orb-black-2.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/slackicon.png);background-size:30px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Share scheduled posts with your team in Slack</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/359499" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb" style="background-image:url(assets/img/logos/orb-black-3.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/patreon.svg);background-size:28px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Connect Patreon to your Ghost membership site</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/75801" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb" style="background-image:url(assets/img/logos/orb-black-4.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/zero-bounce.png);background-size:30px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Protect email delivery with email verification</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/359415" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb" style="background-image:url(assets/img/logos/orb-black-5.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/paypal.svg);background-size:26px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Add members for successful sales in PayPal</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/184423" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb rot-1" style="background-image:url(assets/img/logos/orb-black-3.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/paypal.svg);background-size:26px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Unsubscribe members who cancel a subscription in PayPal</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/359348" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb rot-2" style="background-image:url(assets/img/logos/orb-black-1.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/google-docs.svg);background-size:22px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Send new post drafts from Google Docs to Ghost</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/50924" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb rot-3" style="background-image:url(assets/img/logos/orb-black-4.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon id-typeform" style="background-image:url(assets/img/typeform.svg);background-size:32px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Survey new members using Typeform</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/359407" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="apps-grid-cell">
|
||||
<article class="apps-card-app">
|
||||
<div class="apps-card-left">
|
||||
<div class="flex items-center">
|
||||
<figure class="apps-card-app-orb rot-3" style="background-image:url(assets/img/logos/orb-black-1.png);background-size:32px;"></figure>
|
||||
{{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}}
|
||||
<figure class="apps-card-app-icon" style="background-image:url(assets/img/mailchimp.svg);background-size:32px;"></figure>
|
||||
</div>
|
||||
<div class="apps-card-meta">
|
||||
<h3 class="apps-card-app-title">Sync email subscribers in Ghost + Mailchimp</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-card-right">
|
||||
<a href="https://zapier.com/app/editor/template/359342" target="_blank" rel="noopener" class="gh-btn gh-btn-outline zapier-template-link"><span>Use this Zap</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zapier-footer">
|
||||
<a href="https://zapier.com/apps/ghost/integrations?utm_medium=partner_api&utm_source=widget&utm_campaign=Widget" target="_blank" rel="noopener"><span>See more Ghost integrations powered by</span> <figure style="background-image:url(assets/img/zapier-logo.svg); background-size: 42px;"></figure></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if this.showRegenerateKeyModal}}
|
||||
<GhFullscreenModal @modal="regenerate-key"
|
||||
@model={{hash
|
||||
apiKey=this.selectedApiKey
|
||||
integration=this.integration
|
||||
internalIntegration="zapier"
|
||||
}}
|
||||
@confirm={{action "regenerateKey"}}
|
||||
@close={{action "cancelRegenerateKeyModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
Loading…
Reference in New Issue
Block a user