diff --git a/ghost/admin/app/components/modal-impersonate-member.js b/ghost/admin/app/components/modal-impersonate-member.js new file mode 100644 index 0000000000..b52823c587 --- /dev/null +++ b/ghost/admin/app/components/modal-impersonate-member.js @@ -0,0 +1,33 @@ +import ModalComponent from 'ghost-admin/components/modal-base'; +import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; +import {alias} from '@ember/object/computed'; +import {inject as service} from '@ember/service'; +import {task, timeout} from 'ember-concurrency'; + +export default ModalComponent.extend({ + config: service(), + store: service(), + + classNames: 'modal-impersonate-member', + + signinUrl: null, + member: alias('model'), + + didInsertElement() { + this._super(...arguments); + + this._signinUrlUpdateTask.perform(); + }, + + copySigninUrl: task(function* () { + copyTextToClipboard(this.get('signinUrl')); + yield timeout(1000); + return true; + }), + + _signinUrlUpdateTask: task(function*() { + const memberSigninURL = yield this.member.fetchSigninUrl.perform(); + + this.set('signinUrl', memberSigninURL.url); + }).drop() +}); diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 066e1ee319..146a101678 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -12,10 +12,14 @@ const SCRATCH_PROPS = ['name', 'email', 'note']; export default Controller.extend({ members: controller(), + session: service(), + dropdown: service(), notifications: service(), router: service(), store: service(), + showImpersonateMemberModal: false, + member: alias('model'), scratchMember: computed('member', function () { @@ -39,6 +43,10 @@ export default Controller.extend({ this.toggleProperty('showDeleteMemberModal'); }, + toggleImpersonateMemberModal() { + this.toggleProperty('showImpersonateMemberModal'); + }, + save() { return this.save.perform(); }, @@ -106,13 +114,12 @@ export default Controller.extend({ fetchMember: task(function* (memberId) { this.set('isLoading', true); - yield this.store.findRecord('member', memberId, { + let member = yield this.store.findRecord('member', memberId, { reload: true - }).then((member) => { - this.set('member', member); - this.set('isLoading', false); - return member; }); + + this.set('member', member); + this.set('isLoading', false); }), _saveMemberProperty(propKey, newValue) { diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js index 8e209f87e4..59da242910 100644 --- a/ghost/admin/app/models/member.js +++ b/ghost/admin/app/models/member.js @@ -1,5 +1,7 @@ import Model, {attr, hasMany} from '@ember-data/model'; import ValidationEngine from 'ghost-admin/mixins/validation-engine'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; export default Model.extend(ValidationEngine, { validationType: 'member', @@ -12,6 +14,10 @@ export default Model.extend(ValidationEngine, { subscribed: attr('boolean', {defaultValue: true}), labels: hasMany('label', {embedded: 'always', async: false}), comped: attr('boolean', {defaultValue: false}), + + ghostPaths: service(), + ajax: service(), + // remove client-generated labels, which have `id: null`. // Ember Data won't recognize/update them automatically // when returned from the server with ids. @@ -22,5 +28,13 @@ export default Model.extend(ValidationEngine, { labels.removeObjects(oldLabels); oldLabels.invoke('deleteRecord'); - } + }, + + fetchSigninUrl: task(function* () { + let url = this.get('ghostPaths.url').api('members', this.get('id'), 'signin_urls'); + + let response = yield this.ajax.request(url); + + return response.member_signin_urls[0]; + }).drop() }); diff --git a/ghost/admin/app/styles/layouts/members.css b/ghost/admin/app/styles/layouts/members.css index 57f44ffcad..26df007e73 100644 --- a/ghost/admin/app/styles/layouts/members.css +++ b/ghost/admin/app/styles/layouts/members.css @@ -96,6 +96,10 @@ p.gh-members-list-email { pointer-events: none; } +.member-link-copied svg { + margin-right: 4px; +} + .members-header .gh-members-header-search { margin-right: 12px; border-right: 1px solid var(--lightgrey-d2); diff --git a/ghost/admin/app/styles/patterns/buttons.css b/ghost/admin/app/styles/patterns/buttons.css index 69e23cf3b9..070bccc216 100644 --- a/ghost/admin/app/styles/patterns/buttons.css +++ b/ghost/admin/app/styles/patterns/buttons.css @@ -279,6 +279,11 @@ fieldset[disabled] .gh-btn { background: var(--white); } +.gh-btn-white.gh-btn-green:hover, +.gh-btn-white.gh-btn-blue:hover { + border: none; +} + .gh-btn-white span { height: 35px; line-height: 35px; diff --git a/ghost/admin/app/styles/patterns/forms.css b/ghost/admin/app/styles/patterns/forms.css index f53b7bcc43..49f7edfc89 100644 --- a/ghost/admin/app/styles/patterns/forms.css +++ b/ghost/admin/app/styles/patterns/forms.css @@ -656,6 +656,18 @@ textarea { background: var(--input-bg-color); } +.gh-input-group .gh-btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.gh-input-group .gh-btn span { + height: 36px; + line-height: 36px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + /* FFF: Fucking Firefox Fixes /* ---------------------------------------------------------- */ diff --git a/ghost/admin/app/styles/spirit/_animations.css b/ghost/admin/app/styles/spirit/_animations.css index 1454c5887c..67e761a4a9 100644 --- a/ghost/admin/app/styles/spirit/_animations.css +++ b/ghost/admin/app/styles/spirit/_animations.css @@ -284,3 +284,34 @@ Pop: Appear from bottom, disappear to bottom width: 6px; background: var(--darkgrey-l2); } + +/* Animated icons */ +.animated-icon path { + stroke-dashoffset: 300; + stroke-dasharray: 300; + animation: icon-dash 3s ease-out forwards; +} + +@keyframes icon-dash { + 0% { + stroke-dashoffset: 300; + } + 100% { + stroke-dashoffset: 0; + } +} + +/* Fade in */ +.fade-in { + opacity: 0; + animation: fade-in 3s ease-out forwards; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1.0; + } +} \ No newline at end of file diff --git a/ghost/admin/app/templates/components/modal-impersonate-member.hbs b/ghost/admin/app/templates/components/modal-impersonate-member.hbs new file mode 100644 index 0000000000..4bc127df6a --- /dev/null +++ b/ghost/admin/app/templates/components/modal-impersonate-member.hbs @@ -0,0 +1,40 @@ + +{{!-- disable mouseDown so it doesn't trigger focus-out validations --}} + + {{svg-jar "close"}} + + + + +
+

This link is only valid for the next 10 minutes

+
diff --git a/ghost/admin/app/templates/member.hbs b/ghost/admin/app/templates/member.hbs index fcbaabb414..3e1fe5036b 100644 --- a/ghost/admin/app/templates/member.hbs +++ b/ghost/admin/app/templates/member.hbs @@ -1,26 +1,37 @@
-
- -

- Members - {{svg-jar "arrow-right"}} - {{#if this.member.isNew}} - New member - {{else}} - {{or this.member.name this.member.email}} - {{/if}} -

-
- -
-
+ +

+ Members + {{svg-jar "arrow-right"}} + {{#if this.member.isNew}} + New member + {{else}} + {{or this.member.name this.member.email}} + {{/if}} +

+
+ {{#if this.session.user.isOwner}} + {{#unless this.member.isNew}} + + {{/unless}} + {{/if}} + + +
+
+ +
{{#if (or this.member.name this.member.email)}} {{else}}
@@ -78,3 +89,11 @@ @close={{action "toggleDeleteMemberModal"}} @modifier="action wide" /> {{/if}} + +{{#if this.showImpersonateMemberModal}} + +{{/if}} diff --git a/ghost/admin/tests/acceptance/members-test.js b/ghost/admin/tests/acceptance/members-test.js index ad5c9317e3..c92d545e01 100644 --- a/ghost/admin/tests/acceptance/members-test.js +++ b/ghost/admin/tests/acceptance/members-test.js @@ -134,7 +134,7 @@ describe('Acceptance: Members', function () { // it navigates to the new member route expect(currentURL(), 'new member URL').to.equal('/members/new'); // it displays the new member form - expect(find('.member-basic-info-form .gh-canvas-header h2').textContent, 'settings pane title') + expect(find('.gh-canvas-header h2').textContent, 'settings pane title') .to.contain('New member'); // // all fields start blank