mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
First pass at members list
no issue - don't nest details route as it's not nested UI - implement styled list of members - add `<MemberAvatar>` component that generates random background colour and initials based on member name - fixed generation of fake member details in mirage
This commit is contained in:
parent
76a1a98b48
commit
7ec48b36e0
32
ghost/admin/app/components/member-avatar.js
Normal file
32
ghost/admin/app/components/member-avatar.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Component from '@ember/component';
|
||||
import {computed} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
|
||||
const stringToHslColor = function (str, saturation, lightness) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
var h = hash % 360;
|
||||
return 'hsl(' + h + ', ' + saturation + '%, ' + lightness + '%)';
|
||||
};
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
member: null,
|
||||
|
||||
backgroundStyle: computed('member.name', function () {
|
||||
let name = this.member.name;
|
||||
if (name) {
|
||||
let color = stringToHslColor(name, 30, 80);
|
||||
return htmlSafe(`background-color: ${color}`);
|
||||
}
|
||||
}),
|
||||
|
||||
initials: computed('member.name', function () {
|
||||
let names = this.member.name.split(' ');
|
||||
let intials = [names[0][0], names[names.length - 1][0]];
|
||||
return intials.join('').toUpperCase();
|
||||
})
|
||||
});
|
6
ghost/admin/app/controllers/member.js
Normal file
6
ghost/admin/app/controllers/member.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {alias} from '@ember/object/computed';
|
||||
|
||||
export default Controller.extend({
|
||||
member: alias('model')
|
||||
});
|
31
ghost/admin/app/controllers/members.js
Normal file
31
ghost/admin/app/controllers/members.js
Normal file
@ -0,0 +1,31 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {computed} from '@ember/object';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
export default Controller.extend({
|
||||
queryParams: ['page'],
|
||||
|
||||
meta: null,
|
||||
members: null,
|
||||
|
||||
page: computed('meta.pagination.page', function () {
|
||||
let page = this.get('meta.pagination.page');
|
||||
|
||||
if (!page || page === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return page;
|
||||
}),
|
||||
|
||||
fetchMembers: task(function* () {
|
||||
let results = yield this.store.query('member', {
|
||||
page: this.page || 1,
|
||||
limit: 15
|
||||
});
|
||||
|
||||
this.set('meta', results.meta);
|
||||
this.set('members', results);
|
||||
})
|
||||
});
|
@ -64,9 +64,8 @@ Router.map(function () {
|
||||
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
|
||||
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
|
||||
|
||||
this.route('members', function () {
|
||||
this.route('details', {path: ':member_id'});
|
||||
});
|
||||
this.route('members');
|
||||
this.route('member', {path: '/members/:member_id'});
|
||||
|
||||
this.route('subscribers', function () {
|
||||
this.route('new');
|
||||
|
9
ghost/admin/app/routes/member.js
Normal file
9
ghost/admin/app/routes/member.js
Normal file
@ -0,0 +1,9 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default AuthenticatedRoute.extend({
|
||||
// TODO: add model method to load member if not passed in
|
||||
|
||||
titleToken() {
|
||||
return this.controller.get('member.name');
|
||||
}
|
||||
});
|
@ -22,5 +22,10 @@ export default AuthenticatedRoute.extend({
|
||||
return this.transitionTo('posts');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.fetchMembers.perform();
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +0,0 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default AuthenticatedRoute.extend({});
|
@ -1,7 +0,0 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default AuthenticatedRoute.extend({
|
||||
model() {
|
||||
return this.store.findAll('member');
|
||||
}
|
||||
});
|
@ -51,7 +51,7 @@
|
||||
</li>
|
||||
<li>{{#link-to "team" data-test-nav="team"}}{{svg-jar "account-group"}}Team{{/link-to}}</li>
|
||||
{{#if (and config.enableDeveloperExperiments (gh-user-can-admin session.user))}}
|
||||
<li>{{#link-to "members" data-test-nav="members"}}{{svg-jar "email"}}Members{{/link-to}}</li>
|
||||
<li>{{#link-to "members" current-when="members member" data-test-nav="members"}}{{svg-jar "email"}}Members{{/link-to}}</li>
|
||||
{{/if}}
|
||||
{{#if (and feature.subscribers (gh-user-can-admin session.user))}}
|
||||
<li>{{#link-to "subscribers" data-test-nav="subscribers"}}{{svg-jar "email"}}Subscribers{{/link-to}}</li>
|
||||
|
3
ghost/admin/app/templates/components/member-avatar.hbs
Normal file
3
ghost/admin/app/templates/components/member-avatar.hbs
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="flex items-center justify-center br-100" style={{this.backgroundStyle}} ...attributes>
|
||||
<span class="block white f5 fw7">{{this.initials}}</span>
|
||||
</div>
|
13
ghost/admin/app/templates/member.hbs
Normal file
13
ghost/admin/app/templates/member.hbs
Normal file
@ -0,0 +1,13 @@
|
||||
<section class="gh-canvas">
|
||||
<header class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
{{link-to "Members" "members" data-test-link="members"}}
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{member.name}}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<section class="view-container">
|
||||
|
||||
</section>
|
||||
</section>
|
@ -1 +1,36 @@
|
||||
{{outlet}}
|
||||
<section class="gh-canvas">
|
||||
<header class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
|
||||
<div class="view-actions">
|
||||
<span>{{this.meta.pagination.total}} Members</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="view-container">
|
||||
{{#if this.members}}
|
||||
{{!-- members list, styles taken from .apps-grid --}}
|
||||
<div class="flex flex-row flex-wrap items-start ba br3">
|
||||
{{#each this.members as |member index|}}
|
||||
<div class="flex-grow-1 flex-shrink-1" style="flex-basis: 100%">
|
||||
{{#link-to "member" member}}
|
||||
<article class="flex items-center justify-between pa4 {{if index "bt"}}">
|
||||
<div class="flex items-center">
|
||||
<MemberAvatar @member={{member}} class="w10 h10 mr4" />
|
||||
<div class="flex flex-column">
|
||||
<h3 class="ma0 f5 fw7">{{member.name}}</h3>
|
||||
<p class="ma0 pa0 f7">{{member.email}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">{{svg-jar "arrow-right" class="w4"}}</div>
|
||||
</article>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#unless this.fetchMembers.isRunning}}
|
||||
<p>No members found.</p>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</section>
|
||||
</section>
|
@ -1,10 +0,0 @@
|
||||
<section class="gh-canvas">
|
||||
<header class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
|
||||
<div class="view-actions"></div>
|
||||
</header>
|
||||
|
||||
<section class="view-container">
|
||||
<p>...</p>
|
||||
</section>
|
||||
</section>
|
@ -24,15 +24,13 @@ export default function () {
|
||||
|
||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
this.namespace = '/ghost/api/v2/admin'; // make this `api`, for example, if your API is namespaced
|
||||
this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
this.timing = 1000; // delay for each request, automatically set to 0 during testing
|
||||
this.logging = true;
|
||||
|
||||
// Mock endpoints here to override real API requests during development, eg...
|
||||
// this.put('/posts/:id/', versionMismatchResponse);
|
||||
// mockTags(this);
|
||||
// this.loadFixtures('settings');
|
||||
|
||||
this.createList('member', 200);
|
||||
mockMembers(this);
|
||||
|
||||
// keep this line, it allows all other API requests to hit the real server
|
||||
|
@ -8,4 +8,6 @@ export default function (server) {
|
||||
server.createList('tag', 100);
|
||||
|
||||
server.create('integration', {name: 'Demo'});
|
||||
|
||||
server.createList('member', 125);
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { setupComponentTest } from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
describe('Integration | Component | member-avatar', function() {
|
||||
setupComponentTest('member-avatar', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
it('renders', function() {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||
// Template block usage:
|
||||
// this.render(hbs`
|
||||
// {{#member-avatar}}
|
||||
// template content
|
||||
// {{/member-avatar}}
|
||||
// `);
|
||||
|
||||
this.render(hbs`{{member-avatar}}`);
|
||||
expect(this.$()).to.have.length(1);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user