mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Unified tag and member screen code
no issue The tag and member screens share the same underlying UI/UX patterns but were using different code patterns. This brings both in line so that we have consistent code patterns that can be re-used for other screens. - fixed cleanup of new tags by adding the `deactivate` hook to the `tag` route - updated `member` and `member.new` route/controller setup to match tag route/controller setup - added `save` action to member controller so that Ctrl/Cmd+S works on member screen - updated tag route/controller to utilise the same instant display w/background refresh when accessing the tag details screen - completed transition of non-component tag/members templates over to angle bracket component syntax
This commit is contained in:
parent
51ded01ed3
commit
5585a781b9
@ -8,13 +8,12 @@ import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
members: controller(),
|
||||
notifications: service(),
|
||||
router: service(),
|
||||
store: service(),
|
||||
|
||||
router: service(),
|
||||
|
||||
notifications: service(),
|
||||
|
||||
member: alias('model'),
|
||||
|
||||
subscribedAt: computed('member.createdAtUTC', function () {
|
||||
let memberSince = moment(this.member.createdAtUTC).from(moment());
|
||||
let createdDate = moment(this.member.createdAtUTC).format('MMM DD, YYYY');
|
||||
@ -25,9 +24,15 @@ export default Controller.extend({
|
||||
setProperty(propKey, value) {
|
||||
this._saveMemberProperty(propKey, value);
|
||||
},
|
||||
|
||||
toggleDeleteMemberModal() {
|
||||
this.toggleProperty('showDeleteMemberModal');
|
||||
},
|
||||
|
||||
save() {
|
||||
return this.save.perform();
|
||||
},
|
||||
|
||||
finaliseDeletion() {
|
||||
// decrement the total member count manually so there's no flash
|
||||
// when transitioning back to the members list
|
||||
@ -62,21 +67,8 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
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.member.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
},
|
||||
|
||||
save() {
|
||||
return this.save.perform();
|
||||
return this.leaveScreenTransition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
@ -98,11 +90,13 @@ export default Controller.extend({
|
||||
|
||||
fetchMember: task(function* (memberId) {
|
||||
this.set('isLoading', true);
|
||||
|
||||
yield this.store.findRecord('member', memberId, {
|
||||
reload: true
|
||||
}).then((data) => {
|
||||
this.set('member', data);
|
||||
}).then((member) => {
|
||||
this.set('member', member);
|
||||
this.set('isLoading', false);
|
||||
return member;
|
||||
});
|
||||
})
|
||||
|
||||
|
@ -1,104 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import moment from 'moment';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as controller} from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
members: controller(),
|
||||
store: service(),
|
||||
|
||||
router: service(),
|
||||
|
||||
notifications: service(),
|
||||
|
||||
member: alias('model'),
|
||||
|
||||
displayName: computed('member.{name,email}', function () {
|
||||
return this.member.name || this.member.email || 'New member';
|
||||
}),
|
||||
subscribedAt: computed('member.createdAtUTC', function () {
|
||||
let memberSince = moment(this.member.createdAtUTC).from(moment());
|
||||
let createdDate = moment(this.member.createdAtUTC).format('MMM DD, YYYY');
|
||||
return `${createdDate} (${memberSince})`;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setProperty(propKey, value) {
|
||||
this._saveMemberProperty(propKey, value);
|
||||
},
|
||||
|
||||
toggleDeleteMemberModal() {
|
||||
this.toggleProperty('showDeleteMemberModal');
|
||||
},
|
||||
|
||||
finaliseDeletion() {
|
||||
// decrement the total member count manually so there's no flash
|
||||
// when transitioning back to the members list
|
||||
if (this.members.memberCount) {
|
||||
this.members.decrementProperty('memberCount');
|
||||
}
|
||||
this.router.transitionTo('members');
|
||||
},
|
||||
|
||||
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.member.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
},
|
||||
|
||||
save() {
|
||||
return this.save.perform();
|
||||
}
|
||||
},
|
||||
|
||||
save: task(function* () {
|
||||
let member = this.member;
|
||||
try {
|
||||
return yield member.save();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
this.notifications.showAPIError(error, {key: 'member.save'});
|
||||
}
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
_saveMemberProperty(propKey, newValue) {
|
||||
let member = this.member;
|
||||
member.set(propKey, newValue);
|
||||
}
|
||||
});
|
@ -23,7 +23,11 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
return this._deleteTag();
|
||||
return this.tag.destroyRecord().then(() => {
|
||||
return this.transitionToRoute('tags');
|
||||
}, (error) => {
|
||||
return this.notifications.showAPIError(error, {key: 'tag.delete'});
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
@ -55,17 +59,8 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
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.tag.rollbackAttributes();
|
||||
|
||||
return transition.retry();
|
||||
return this.leaveScreenTransition.retry();
|
||||
}
|
||||
},
|
||||
|
||||
@ -93,6 +88,7 @@ export default Controller.extend({
|
||||
}
|
||||
tag.set('slug', slugValue);
|
||||
}
|
||||
|
||||
// TODO: This is required until .validate/.save mark fields as validated
|
||||
tag.get('hasValidated').addObject(propKey);
|
||||
},
|
||||
@ -125,11 +121,13 @@ export default Controller.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
_deleteTag() {
|
||||
return this.tag.destroyRecord().then(() => {
|
||||
return this.transitionToRoute('tags');
|
||||
}, (error) => {
|
||||
return this.notifications.showAPIError(error, {key: 'tag.delete'});
|
||||
fetchTag: task(function* (slug) {
|
||||
this.set('isLoading', true);
|
||||
|
||||
yield this.store.queryRecord('tag', {slug}).then((tag) => {
|
||||
this.set('tag', tag);
|
||||
this.set('isLoading', false);
|
||||
return tag;
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -3,9 +3,10 @@ import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
|
||||
router: service(),
|
||||
|
||||
_requiresBackgroundRefresh: true,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.router.on('routeWillChange', (transition) => {
|
||||
@ -20,27 +21,29 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
},
|
||||
|
||||
model(params) {
|
||||
this._isMemberUpdated = true;
|
||||
return this.store.findRecord('member', params.member_id, {
|
||||
reload: true
|
||||
});
|
||||
this._requiresBackgroundRefresh = false;
|
||||
|
||||
if (params.member_id) {
|
||||
return this.store.findRecord('member', params.member_id, {reload: true});
|
||||
} else {
|
||||
return this.store.createRecord('member');
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
setupController(controller, member) {
|
||||
this._super(...arguments);
|
||||
if (!this._isMemberUpdated) {
|
||||
controller.fetchMember.perform(model.get('id'));
|
||||
if (this._requiresBackgroundRefresh) {
|
||||
controller.fetchMember.perform(member.get('id'));
|
||||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
|
||||
// clear the properties
|
||||
let {controller} = this;
|
||||
controller.model.rollbackAttributes();
|
||||
this.set('controller.model', null);
|
||||
this._isMemberUpdated = false;
|
||||
// clean up newly created records and revert unsaved changes to existing
|
||||
this.controller.member.rollbackAttributes();
|
||||
|
||||
this._requiresBackgroundRefresh = true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -54,10 +57,13 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
},
|
||||
|
||||
showUnsavedChangesModal(transition) {
|
||||
if (transition.from && transition.from.name.match(/^member$/) && transition.targetName) {
|
||||
if (transition.from && transition.from.name === this.routeName && transition.targetName) {
|
||||
let {controller} = this;
|
||||
|
||||
if (!controller.member.isDeleted && controller.member.hasDirtyAttributes) {
|
||||
// member.changedAttributes is always true for new members but number of changed attrs is reliable
|
||||
let isChanged = Object.keys(controller.member.changedAttributes()).length > 0;
|
||||
|
||||
if (!controller.member.isDeleted && isChanged) {
|
||||
transition.abort();
|
||||
controller.send('toggleUnsavedChangesModal', transition);
|
||||
return;
|
||||
|
@ -1,45 +1,6 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
|
||||
router: service(),
|
||||
|
||||
controllerName: 'member.new',
|
||||
templateName: 'member/new',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.router.on('routeWillChange', (transition) => {
|
||||
this.showUnsavedChangesModal(transition);
|
||||
});
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('member');
|
||||
},
|
||||
|
||||
// reset the model so that mobile screens react to an empty selectedMember
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
|
||||
let {controller} = this;
|
||||
controller.model.rollbackAttributes();
|
||||
controller.set('model', null);
|
||||
},
|
||||
|
||||
showUnsavedChangesModal(transition) {
|
||||
if (transition.from && transition.from.name.match(/^members\.new/) && transition.targetName) {
|
||||
let {controller} = this;
|
||||
let isUnchanged = isEmpty(Object.keys(controller.member.changedAttributes()));
|
||||
if (!controller.member.isDeleted && !isUnchanged) {
|
||||
transition.abort();
|
||||
controller.send('toggleUnsavedChangesModal', transition);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
import MemberRoute from '../member';
|
||||
|
||||
export default MemberRoute.extend({
|
||||
controllerName: 'member',
|
||||
templateName: 'member'
|
||||
});
|
||||
|
@ -6,6 +6,8 @@ import {inject as service} from '@ember/service';
|
||||
export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
router: service(),
|
||||
|
||||
_requiresBackgroundRefresh: true,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.router.on('routeWillChange', (transition) => {
|
||||
@ -20,6 +22,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
},
|
||||
|
||||
model(params) {
|
||||
this._requiresBackgroundRefresh = false;
|
||||
|
||||
if (params.tag_slug) {
|
||||
return this.store.queryRecord('tag', {slug: params.tag_slug});
|
||||
} else {
|
||||
@ -27,8 +31,24 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
}
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
return {tag_slug: model.get('slug')};
|
||||
serialize(tag) {
|
||||
return {tag_slug: tag.get('slug')};
|
||||
},
|
||||
|
||||
setupController(controller, tag) {
|
||||
this._super(...arguments);
|
||||
if (this._requiresBackgroundRefresh) {
|
||||
controller.fetchTag.perform(tag.get('slug'));
|
||||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
|
||||
// clean up newly created records and revert unsaved changes to existing
|
||||
this.controller.tag.rollbackAttributes();
|
||||
|
||||
this._requiresBackgroundRefresh = true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -42,7 +62,9 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, {
|
||||
let {controller} = this;
|
||||
|
||||
// tag.changedAttributes is always true for new tags but number of changed attrs is reliable
|
||||
if (!controller.tag.isDeleted && Object.keys(controller.tag.changedAttributes()).length > 0) {
|
||||
let isChanged = Object.keys(controller.tag.changedAttributes()).length > 0;
|
||||
|
||||
if (!controller.tag.isDeleted && isChanged) {
|
||||
transition.abort();
|
||||
controller.send('toggleUnsavedChangesModal', transition);
|
||||
return;
|
||||
|
@ -2,58 +2,78 @@
|
||||
<form class="mb10 member-basic-info-form">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
{{#link-to "members" data-test-link="members-back"}}Members{{/link-to}}
|
||||
<LinkTo @route="members" data-test-link="members-back">Members</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{#if member.name}}
|
||||
{{member.name}}
|
||||
{{#if this.member.isNew}}
|
||||
New member
|
||||
{{else}}
|
||||
{{member.email}}
|
||||
{{or this.member.name this.member.email}}
|
||||
{{/if}}
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
{{gh-task-button type="button" task=save class="gh-btn gh-btn-blue gh-btn-icon" data-test-button="save"}}
|
||||
<GhTaskButton @class="gh-btn gh-btn-blue gh-btn-icon" @type="button" @task={{save}} @data-test-button="save" />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
|
||||
<GhMemberAvatar @member={{member}} @sizeClass={{if member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}} @containerClass="w18 h18 mr4 gh-member-detail-avatar" />
|
||||
{{#if (or member.name member.email)}}
|
||||
<GhMemberAvatar
|
||||
@member={{this.member}}
|
||||
@sizeClass={{if this.member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}}
|
||||
@containerClass="w18 h18 mr4 gh-member-detail-avatar"
|
||||
/>
|
||||
{{else}}
|
||||
<div class="flex items-center justify-center br-100 w18 h18 mr4 gh-new-member-avatar">
|
||||
<span class="gh-member-avatar-label f-subheadline fw4 lh-zero tracked-1">N</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div>
|
||||
<h3 class="f2 fw5 ma0 pa0">
|
||||
{{or member.name member.email}}
|
||||
{{or this.member.name this.member.email}}
|
||||
</h3>
|
||||
<p class="f6 pa0 ma0 midlightgrey-d1">
|
||||
{{#if (and member.name member.email)}}
|
||||
<span class="darkgrey fw5">{{member.email}}</span> –
|
||||
{{#if (and this.member.name this.member.email)}}
|
||||
<span class="darkgrey fw5">{{this.member.email}}</span>
|
||||
{{/if}}
|
||||
Created on {{this.subscribedAt}}
|
||||
{{#unless this.member.isNew}}
|
||||
{{if (and member.name member.email) "–"}}
|
||||
Created on {{this.subscribedAt}}
|
||||
{{/unless}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{gh-member-settings-form member=member
|
||||
setProperty=(action "setProperty")
|
||||
isLoading=this.isLoading
|
||||
showDeleteTagModal=(action "toggleDeleteMemberModal")}}
|
||||
|
||||
<GhMemberSettingsForm
|
||||
@member={{this.member}}
|
||||
@setProperty={{action "setProperty"}}
|
||||
@isLoading={{this.isLoading}}
|
||||
@showDeleteTagModal={{action "toggleDeleteMemberModal"}} />
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-red gh-btn-icon mt3"
|
||||
{{action (toggle "showDeleteMemberModal" this)}}
|
||||
data-test-button="delete-member"
|
||||
>
|
||||
<span>Delete member</span>
|
||||
</button>
|
||||
{{#unless this.member.isNew}}
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-red gh-btn-icon mt3"
|
||||
{{on "click" (action "toggleDeleteMemberModal")}}
|
||||
data-test-button="delete-member"
|
||||
>
|
||||
<span>Delete member</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
</section>
|
||||
|
||||
{{#if showUnsavedChangesModal}}
|
||||
{{gh-fullscreen-modal "leave-settings"
|
||||
confirm=(action "leaveScreen")
|
||||
close=(action "toggleUnsavedChangesModal")
|
||||
modifier="action wide"}}
|
||||
{{#if this.showUnsavedChangesModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="leave-settings"
|
||||
@confirm={{action "leaveScreen"}}
|
||||
@close={{action "toggleUnsavedChangesModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if showDeleteMemberModal}}
|
||||
{{gh-fullscreen-modal "delete-member"
|
||||
model=(hash member=member onSuccess=(action "finaliseDeletion"))
|
||||
close=(action (toggle "showDeleteMemberModal" this))
|
||||
modifier="action wide"}}
|
||||
{{#if this.showDeleteMemberModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="delete-member"
|
||||
@model={{hash member=this.member onSuccess=(action "finaliseDeletion")}}
|
||||
@close={{action "toggleDeleteMemberModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
@ -1,67 +0,0 @@
|
||||
<section class="gh-canvas">
|
||||
<form class="mb10 member-basic-info-form">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
{{#link-to "members" data-test-link="members-back"}}Members{{/link-to}}
|
||||
<span>{{svg-jar "arrow-right"}}</span>{{displayName}}
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
{{gh-task-button type="button" task=save class="gh-btn gh-btn-blue gh-btn-icon" data-test-button="save"}}
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
|
||||
{{#if (or member.name member.email)}}
|
||||
<GhMemberAvatar @member={{member}} @sizeClass={{if member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}} @containerClass="w18 h18 mr4 gh-member-detail-avatar" />
|
||||
{{else}}
|
||||
<div class="flex items-center justify-center br-100 w18 h18 mr4 gh-new-member-avatar">
|
||||
<span class="gh-member-avatar-label f-subheadline fw4 lh-zero tracked-1">N</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div>
|
||||
<h3 class="f2 fw5 ma0 pa0">
|
||||
{{#if (or member.name member.email)}}
|
||||
{{or member.name member.email}}
|
||||
{{else}}
|
||||
<span class="midlightgrey-d1">New member</span>
|
||||
{{/if}}
|
||||
</h3>
|
||||
<p class="f6 pa0 ma0 midlightgrey-d1">
|
||||
{{#if (and member.name member.email)}}
|
||||
<span class="darkgrey fw5">{{member.email}}</span>
|
||||
{{/if}}
|
||||
{{#unless member.isNew}}
|
||||
{{if (and member.name member.email) "–"}}
|
||||
Created on {{this.subscribedAt}}
|
||||
{{/unless}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{gh-member-settings-form member=member
|
||||
setProperty=(action "setProperty")
|
||||
isLoading=this.isLoading
|
||||
showDeleteTagModal=(action "toggleDeleteMemberModal")}}
|
||||
</form>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-red gh-btn-icon mt3"
|
||||
{{action (toggle "showDeleteMemberModal" this)}}
|
||||
data-test-button="delete-member"
|
||||
>
|
||||
<span>Delete member</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{{#if showUnsavedChangesModal}}
|
||||
{{gh-fullscreen-modal "leave-settings"
|
||||
confirm=(action "leaveScreen")
|
||||
close=(action "toggleUnsavedChangesModal")
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showDeleteMemberModal}}
|
||||
{{gh-fullscreen-modal "delete-member"
|
||||
model=(hash member=member onSuccess=(action "finaliseDeletion"))
|
||||
close=(action (toggle "showDeleteMemberModal" this))
|
||||
modifier="action wide"}}
|
||||
{{/if}}
|
@ -3,43 +3,57 @@
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
|
||||
<section class="view-actions">
|
||||
<span class="dropdown">
|
||||
{{#gh-dropdown-button dropdownName="members-actions-menu" classNames="gh-btn gh-btn-white gh-btn-icon only-has-icon gh-actions-cog" title="Members Actions" data-test-user-actions=true}}
|
||||
<span>
|
||||
{{svg-jar "settings"}}
|
||||
<span class="hidden">Members Actions</span>
|
||||
</span>
|
||||
{{/gh-dropdown-button}}
|
||||
{{#gh-dropdown name="members-actions-menu" tagName="ul" classNames="gh-member-actions-menu dropdown-menu dropdown-triangle-top-right"}}
|
||||
<li>
|
||||
{{#link-to "members.import" class="mr2" data-test-link="import-csv"}}
|
||||
<span>Import CSV </span>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" {{action 'exportData'}} class="mr2">
|
||||
<span>Export CSV </span>
|
||||
</a>
|
||||
</li>
|
||||
{{/gh-dropdown}}
|
||||
<GhDropdownButton
|
||||
@dropdownName="members-actions-menu"
|
||||
@classNames="gh-btn gh-btn-white gh-btn-icon only-has-icon gh-actions-cog"
|
||||
@title="Members Actions"
|
||||
@data-test-user-actions="true"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "settings"}}
|
||||
<span class="hidden">Members Actions</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown
|
||||
@name="members-actions-menu"
|
||||
@tagName="ul"
|
||||
@classNames="gh-member-actions-menu dropdown-menu dropdown-triangle-top-right"
|
||||
>
|
||||
<li>
|
||||
<LinkTo @route="members.import" class="mr2" data-test-link="import-csv">
|
||||
<span>Import CSV</span>
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" {{action 'exportData'}} class="mr2">
|
||||
<span>Export CSV</span>
|
||||
</a>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
<div class="relative gh-members-header-search">
|
||||
{{svg-jar "search" class="gh-input-search-icon"}}
|
||||
<GhTextInput placeholder="Search members..." @value={{this.searchText}} @input={{action (mut this.searchText) value="target.value"}} class="gh-members-list-searchfield {{if this.searchText "active"}}" />
|
||||
<GhTextInput
|
||||
placeholder="Search members..."
|
||||
@value={{this.searchText}}
|
||||
@input={{action (mut this.searchText) value="target.value"}}
|
||||
class="gh-members-list-searchfield {{if this.searchText "active"}}" />
|
||||
</div>
|
||||
{{#link-to "member.new" class="gh-btn gh-btn-green" data-test-new-member-button="true"}}<span>New member</span>{{/link-to}}
|
||||
<LinkTo @route="member.new" class="gh-btn gh-btn-green" data-test-new-member-button="true"><span>New member</span></LinkTo>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
<section class="content-list">
|
||||
<ol class="members-list gh-list {{unless filteredMembers "no-posts"}}">
|
||||
{{#if filteredMembers}}
|
||||
<ol class="members-list gh-list {{unless this.filteredMembers "no-posts"}}">
|
||||
{{#if this.filteredMembers}}
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header">
|
||||
{{#if this.searchText}}
|
||||
Search result
|
||||
{{else}}
|
||||
{{#if this.fetchMembers.lastSuccessful}}
|
||||
{{pluralize memberCount "member"}}
|
||||
{{pluralize this.memberCount "member"}}
|
||||
{{else}}
|
||||
Loading...
|
||||
{{/if}}
|
||||
@ -48,19 +62,10 @@
|
||||
<div class="gh-list-header gh-list-cellwidth-20 nowrap">Created</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-chevron"></div>
|
||||
</li>
|
||||
{{#vertical-collection
|
||||
items=filteredMembers
|
||||
key="id"
|
||||
containerSelector=".gh-main"
|
||||
estimateHeight=60
|
||||
bufferSize=20
|
||||
as |member|
|
||||
}}
|
||||
{{gh-members-list-item
|
||||
member=member
|
||||
data-test-member-id=member.id
|
||||
}}
|
||||
{{/vertical-collection}}
|
||||
<VerticalCollection @items={{this.filteredMembers}} @key="id" @containerSelector=".gh-main" @estimateHeight=60 @bufferSize=20 as |member|>
|
||||
<GhMembersListItem @member={{member}} @data-test-member-id={{member.id}} />
|
||||
</VerticalCollection>
|
||||
|
||||
{{else}}
|
||||
{{#if this.fetchMembers.isRunning}}
|
||||
<div class="gh-content">
|
||||
@ -84,4 +89,5 @@
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{outlet}}
|
@ -2,37 +2,41 @@
|
||||
<form class="mb15">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
{{#link-to "tags" data-test-link="tags-back"}}Tags{{/link-to}}
|
||||
<LinkTo @route="tags" data-test-link="tags-back">Tags</LinkTo>
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{if tag.isNew "New tag" tag.name}}
|
||||
{{if this.tag.isNew "New tag" tag.name}}
|
||||
</h2>
|
||||
<section class="view-actions">
|
||||
{{gh-task-button task=save type="button" class="gh-btn gh-btn-blue gh-btn-icon" data-test-button="save"}}
|
||||
<GhTaskButton @task={{this.save}} @type="button" @class="gh-btn gh-btn-blue gh-btn-icon" @data-test-button="save" />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
{{gh-tag-settings-form tag=tag
|
||||
setProperty=(action "setProperty")
|
||||
showDeleteTagModal=(action "toggleDeleteTagModal")}}
|
||||
|
||||
<GhTagSettingsForm
|
||||
@tag={{this.tag}}
|
||||
@setProperty={{action "setProperty"}}
|
||||
@showDeleteTagModal={{action "toggleDeleteTagModal"}} />
|
||||
</form>
|
||||
|
||||
{{#unless this.tag.isNew}}
|
||||
<button class="gh-btn gh-btn-red gh-btn-icon mb15" {{action "toggleDeleteTagModal"}}>
|
||||
<button type="button" class="gh-btn gh-btn-red gh-btn-icon mb15" {{on "click" (action "toggleDeleteTagModal")}}>
|
||||
<span>Delete tag</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
</section>
|
||||
|
||||
{{#if showUnsavedChangesModal}}
|
||||
{{gh-fullscreen-modal "leave-settings"
|
||||
confirm=(action "leaveScreen")
|
||||
close=(action "toggleUnsavedChangesModal")
|
||||
modifier="action wide"}}
|
||||
<GhFullscreenModal
|
||||
@modal="leave-settings"
|
||||
@confirm={{action "leaveScreen"}}
|
||||
@close={{action "toggleUnsavedChangesModal"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{#if showDeleteTagModal}}
|
||||
{{gh-fullscreen-modal "delete-tag"
|
||||
model=tag
|
||||
confirm=(action "deleteTag")
|
||||
close=(action "toggleDeleteTagModal")
|
||||
modifier="action wide"}}
|
||||
<GhFullscreenModal
|
||||
@modal="delete-tag"
|
||||
@model={{this.tag}}
|
||||
@confirm={{action "deleteTag"}}
|
||||
@close={{action "toggleDeleteTagModal"}}
|
||||
@modifier="action wide"}} />
|
||||
{{/if}}
|
@ -3,42 +3,33 @@
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Tags</h2>
|
||||
<section class="view-actions">
|
||||
<div class="gh-contentfilter gh-btn-group">
|
||||
<button class="gh-btn {{if (eq type "public") "gh-btn-group-selected"}}" {{action "changeType" "public"}}><span>Public tags</span></button>
|
||||
<button class="gh-btn {{if (eq type "internal") "gh-btn-group-selected"}}" {{action "changeType" "internal"}}><span>Internal tags</span></button>
|
||||
<button class="gh-btn {{if (eq this.type "public") "gh-btn-group-selected"}}" {{action "changeType" "public"}}><span>Public tags</span></button>
|
||||
<button class="gh-btn {{if (eq this.type "internal") "gh-btn-group-selected"}}" {{action "changeType" "internal"}}><span>Internal tags</span></button>
|
||||
</div>
|
||||
{{#link-to "tag.new" class="gh-btn gh-btn-green"}}<span>New tag</span>{{/link-to}}
|
||||
<LinkTo @route="tag.new" class="gh-btn gh-btn-green"><span>New tag</span></LinkTo>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="content-list">
|
||||
<ol class="tags-list gh-list {{unless sortedTags "no-posts"}}">
|
||||
{{#if sortedTags}}
|
||||
<ol class="tags-list gh-list {{unless this.sortedTags "no-posts"}}">
|
||||
{{#if this.sortedTags}}
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header">Tag</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-10">Slug</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-10">No. of posts</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-min"></div>
|
||||
</li>
|
||||
{{#vertical-collection
|
||||
items=sortedTags
|
||||
key="id"
|
||||
containerSelector=".gh-main"
|
||||
estimateHeight=60
|
||||
bufferSize=20
|
||||
as |tag|
|
||||
}}
|
||||
{{gh-tags-list-item
|
||||
tag=tag
|
||||
data-test-tag-id=tag.id
|
||||
}}
|
||||
{{/vertical-collection}}
|
||||
<VerticalCollection @items={{this.sortedTags}} @key="id" @containerSelector=".gh-main" @estimateHeight={{60}} @bufferSize={{20}} as |tag|>
|
||||
<GhTagsListItem @tag={{tag}} @data-test-tag-id={{tag.id}} />
|
||||
</VerticalCollection>
|
||||
{{else}}
|
||||
<li class="no-posts-box">
|
||||
<div class="no-posts">
|
||||
{{svg-jar "tags-placeholder" class="gh-tags-placeholder"}}
|
||||
<h3>You haven't created any {{type}} tags yet!</h3>
|
||||
{{#link-to "tag.new" class="gh-btn gh-btn-green gh-btn-lg"}}
|
||||
<h3>You haven't created any {{this.type}} tags yet!</h3>
|
||||
<LinkTo @route="tag.new" class="gh-btn gh-btn-green gh-btn-lg">
|
||||
<span>Create a new tag</span>
|
||||
{{/link-to}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
|
Loading…
Reference in New Issue
Block a user