Merge branch 'master' into v3

This commit is contained in:
Kevin Ansfield 2019-10-08 14:04:38 +01:00
commit 159e810d5c
48 changed files with 1813 additions and 757 deletions

View File

@ -16,10 +16,7 @@ install:
- yarn
script:
- yarn run lint:js && yarn run test
notifications:
slack:
rooms:
- secure: qEi0rR3dNcOthdz5VItSrYv73hUFE19cRqNl6qkaoN5uyOxrV75W2HeR7zQ7qbIi6MhHvV+Y9kT96RKPXtBilTm5/+pZ2bogglnrR8kGy8YkWdDgEqCUPqTLX1zgm92tjFsNia5CYFwxnasUhogZ0G3uSYVjJjoNzQ8HZdk/DTfl52jmJhJ4cXT2Hd2lvREm+iNwgwy2ena5trJOYqWS/5aov3ls1vK4H2qQOOVdeQ0Jc/hpipSm/WIR0wQal58tnQUbnQ2LhyCP6EhtrlINhQBboRcD8J7fTXlfAZdy4HrEnbEhnrYQefBsqYnQL1s+q8kvjsMi7hujo/O0kywaE1tLDLJL98I+JLA3k1JZeFfXxU9rwXsTDF8McQbaeqYZb/4NtU6aO3By45S8mz2Y9L6nLREmmeVsiFkelkiKHaWkeOMmgji0lHZ/56NyN5jidbRKW+PevGFQacsNhs3pBglvqLwZUVCcXfSGAUVM88HHbVqZOq/nktatqC41qhv+V5eLksJ1bHAz2iO3/6QPVfCkw4iqJd+iWkMIZY9Pmvh8JwxajflNvemiGUOO+2vz9gt4ua7IKbM+bX1UH19nPmeADnv+0r65YcnHSntSD3rYFQuHQInthnadyUeTKx/mWTOTIC1+bmnI3Ltkd2qxG1ui4riHDBXbEXDFFjBfRhI=
on_success: change
on_failure: always
on_pull_requests: false
after_failure: |
if [ "${TRAVIS_EVENT_TYPE}" == "cron" ]; then
curl -X POST --data-urlencode "payload={\"attachments\": [{\"color\": \"danger\", \"fallback\": \"Build Failure: $TRAVIS_JOB_WEB_URL\", \"title\": \"Build Failure\", \"text\": \"$TRAVIS_JOB_WEB_URL\"}]}" $SLACK_URL
fi

View File

@ -16,11 +16,12 @@ export default Component.extend({
tagName: '',
member: null,
initialsClass: 'f6 fw4',
backgroundStyle: computed('member.name', function () {
let name = this.member.name;
initialsClass: computed('sizeClass', function () {
return this.sizeClass || 'f5 fw4 lh-zero';
}),
backgroundStyle: computed('member.{name,email}', function () {
let name = this.member.name || this.member.email;
if (name) {
let color = stringToHslColor(name, 55, 55);
return htmlSafe(`background-color: ${color}`);
@ -29,11 +30,11 @@ export default Component.extend({
return htmlSafe('');
}),
initials: computed('member.name', function () {
let name = this.member.name;
initials: computed('member.{name,email}', function () {
let name = this.member.name || this.member.email;
if (name) {
let names = name.split(' ');
let intials = [names[0][0], names[names.length - 1][0]];
let intials = names.length > 1 ? [names[0][0], names[names.length - 1][0]] : [names[0][0]];
return intials.join('').toUpperCase();
}
return '';

View File

@ -0,0 +1,56 @@
import Component from '@ember/component';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import moment from 'moment';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Component.extend({
feature: service(),
config: service(),
mediaQueries: service(),
isViewingSubview: false,
scratchDescription: '',
// Allowed actions
setProperty: () => {},
showDeleteTagModal: () => {},
scratchName: boundOneWay('member.name'),
scratchEmail: boundOneWay('member.email'),
subscriptions: computed('member.stripe', function () {
let subscriptions = this.member.get('stripe');
if (subscriptions && subscriptions.length > 0) {
return subscriptions.map((subscription) => {
return {
customer: subscription.customer,
name: '',
email: subscription.email || '',
status: subscription.status,
startDate: '',
planName: subscription.name,
validUntil: moment(subscription.validUntil * 1000).format('MMM DD YYYY')
};
}).reverse();
}
return null;
}),
hasMultipleSubscriptions: computed('member.stripe', function () {
let subscriptions = this.member.get('stripe');
if (subscriptions && subscriptions.length > 1) {
return true;
}
return false;
}),
actions: {
setProperty(property, value) {
this.setProperty(property, value);
},
deleteTag() {
this.showDeleteTagModal();
}
}
});

View File

@ -0,0 +1,25 @@
import Component from '@ember/component';
import moment from 'moment';
import {alias} from '@ember/object/computed';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default Component.extend({
ghostPaths: service(),
notifications: service(),
router: service(),
tagName: 'li',
classNames: ['gh-flex-list-row'],
active: false,
id: alias('member.id'),
email: alias('member.email'),
name: alias('member.name'),
subscribedAt: computed('member.createdAt', function () {
let memberSince = moment(this.member.createdAt).from(moment());
let createdDate = moment(this.member.createdAt).format('MMM DD, YYYY');
return `${createdDate} (${memberSince})`;
})
});

View File

@ -12,6 +12,7 @@ import {task, timeout} from 'ember-concurrency';
const PSM_ANIMATION_LENGTH = 400;
export default Component.extend(SettingsMenuMixin, {
feature: service(),
store: service(),
config: service(),
ghostPaths: service(),

View File

@ -0,0 +1,32 @@
import Component from '@ember/component';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
const VISIBILITIES = [
{label: 'Public', name: 'public'},
{label: 'Members only', name: 'members'},
{label: 'Paid-members only', name: 'paid'}
];
export default Component.extend({
settings: service(),
// public attrs
post: null,
selectedVisibility: computed('post.visibility', function () {
return this.get('post.visibility') || this.settings.get('defaultContentVisibility');
}),
init() {
this._super(...arguments);
this.availableVisibilities = VISIBILITIES;
},
actions: {
updateVisibility(newVisibility) {
this.post.set('visibility', newVisibility);
}
}
});

View File

@ -0,0 +1,43 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {computed} from '@ember/object';
export default ModalComponent.extend({
labelText: 'Select or drag-and-drop a CSV File',
response: null,
closeDisabled: false,
// Allowed actions
confirm: () => {},
uploadUrl: computed(function () {
return `${ghostPaths().apiRoot}/members/csv/`;
}),
actions: {
uploadStarted() {
this.set('closeDisabled', true);
},
uploadFinished() {
this.set('closeDisabled', false);
},
uploadSuccess(response) {
this.set('response', response.meta.stats);
// invoke the passed in confirm action
this.confirm();
},
confirm() {
// noop - we don't want the enter key doing anything
},
closeModal() {
if (!this.closeDisabled) {
this._super(...arguments);
}
}
}
});

View File

@ -1,4 +1,5 @@
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';
@ -13,24 +14,19 @@ export default Controller.extend({
member: alias('model'),
subscription: computed('member.subscriptions', function () {
let subscriptions = this.member.get('subscriptions');
if (!subscriptions) {
return {
amount: '...',
status: '...',
plan: '...'
};
}
let subscription = subscriptions[0] || {};
return {
amount: subscription.amount ? (subscription.amount / 100) : 0,
status: subscription.status || '-',
plan: subscription.plan || 'Free'
};
subscribedAt: computed('member.createdAt', function () {
let memberSince = moment(this.member.createdAt).from(moment());
let createdDate = moment(this.member.createdAt).format('MMM DD, YYYY');
return `${createdDate} (${memberSince})`;
}),
actions: {
setProperty() {
return;
},
toggleDeleteTagModal() {
this.toggleProperty('showDeleteMemberModal');
},
finaliseDeletion() {
// decrememnt the total member count manually so there's no flash
// when transitioning back to the members list
@ -42,10 +38,12 @@ 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);
this.set('isLoading', false);
});
})

View File

@ -1,4 +1,5 @@
import Controller from '@ember/controller';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
@ -11,7 +12,6 @@ export default Controller.extend({
meta: null,
members: null,
searchText: '',
init() {
this._super(...arguments);
this.set('members', this.store.peekAll('member'));
@ -27,13 +27,31 @@ export default Controller.extend({
}
let {name, email} = member;
return name.toLowerCase().indexOf(searchText) >= 0
|| email.toLowerCase().indexOf(searchText) >= 0;
return (name && name.toLowerCase().indexOf(searchText) >= 0)
|| (email && email.toLowerCase().indexOf(searchText) >= 0);
}).sort((a, b) => {
return b.get('createdAt').valueOf() - a.get('createdAt').valueOf();
});
return filtered;
}),
actions: {
exportData() {
let exportUrl = ghostPaths().url.api('members/csv');
let downloadURL = `${exportUrl}`;
let iframe = document.getElementById('iframeDownload');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'iframeDownload';
iframe.style.display = 'none';
document.body.append(iframe);
}
iframe.setAttribute('src', downloadURL);
}
},
fetchMembers: task(function* () {
let newFetchDate = new Date();
let results;
@ -42,17 +60,19 @@ export default Controller.extend({
// fetch any records modified since last fetch
results = yield this.store.query('member', {
limit: 'all',
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`,
order: 'created_at desc'
});
} else {
// fetch all records
results = yield this.store.query('member', {
limit: 'all'
limit: 'all',
order: 'created_at desc'
});
this._hasFetchedAll = true;
this.set('meta', results.meta);
}
this.set('meta', results.meta);
this._lastFetchDate = newFetchDate;
})
});

View File

@ -0,0 +1,19 @@
import Controller from '@ember/controller';
import {inject as controller} from '@ember/controller';
import {inject as service} from '@ember/service';
/* eslint-disable ghost/ember/alias-model-in-controller */
export default Controller.extend({
members: controller(),
router: service(),
actions: {
fetchNewMembers() {
this.members.fetchMembers.perform();
},
close() {
this.router.transitionTo('members');
}
}
});

View File

@ -46,7 +46,6 @@ export default Controller.extend({
importErrors: null,
importSuccessful: false,
showDeleteAllModal: false,
showMemberConfig: false,
submitting: false,
uploadButtonText: 'Import',
@ -71,13 +70,14 @@ export default Controller.extend({
});
let monthlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'month');
let yearlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'year');
monthlyPlan.dollarAmount = (monthlyPlan.amount / 100);
yearlyPlan.dollarAmount = (yearlyPlan.amount / 100);
monthlyPlan.dollarAmount = parseInt(monthlyPlan.amount) ? (monthlyPlan.amount / 100) : 0;
yearlyPlan.dollarAmount = parseInt(yearlyPlan.amount) ? (yearlyPlan.amount / 100) : 0;
stripeProcessor.config.plans = {
monthly: monthlyPlan,
yearly: yearlyPlan
};
subscriptionSettings.stripeConfig = stripeProcessor.config;
subscriptionSettings.requirePaymentForSetup = !!subscriptionSettings.requirePaymentForSetup;
return subscriptionSettings;
}),
@ -161,10 +161,6 @@ export default Controller.extend({
this.toggleProperty('showDeleteAllModal');
},
toggleMemberConfig() {
this.toggleProperty('showMemberConfig');
},
/**
* Opens a file selection dialog - Triggered by "Upload x" buttons,
* searches for the hidden file input within the .gh-setting element
@ -180,6 +176,10 @@ export default Controller.extend({
.click();
},
setDefaultContentVisibility(value) {
this.set('settings.defaultContentVisibility', value);
},
setSubscriptionSettings(key, event) {
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
@ -189,6 +189,7 @@ export default Controller.extend({
stripeConfig.product = {
name: this.settings.get('title')
};
// TODO: this flag has to be removed as it doesn't serve any purpose
if (key === 'isPaid') {
subscriptionSettings.isPaid = event;
}
@ -198,11 +199,14 @@ export default Controller.extend({
if (key === 'month' || key === 'year') {
stripeConfig.plans = stripeConfig.plans.map((plan) => {
if (key === plan.interval) {
plan.amount = event.target.value * 100;
plan.amount = parseInt(event.target.value) ? (event.target.value * 100) : 0;
}
return plan;
});
}
if (key === 'requirePaymentForSignup') {
subscriptionSettings.requirePaymentForSignup = !subscriptionSettings.requirePaymentForSignup;
}
this.set('settings.membersSubscriptionSettings', JSON.stringify(subscriptionSettings));
}
},

View File

@ -89,6 +89,9 @@ export default Controller.extend({
// Generate slug based on name for new tag when empty
if (propKey === 'name' && !tag.get('slug') && isNewTag) {
let slugValue = slugify(newValue);
if (/^#/.test(newValue)) {
slugValue = 'hash-' + slugValue;
}
tag.set('slug', slugValue);
}
// TODO: This is required until .validate/.save mark fields as validated

View File

@ -1,9 +1,14 @@
import EmberObject from '@ember/object';
export default EmberObject.extend({
adapter: '',
amount: 0,
plan: '',
status: '',
validUntil: 0
customer: 'customer_id',
subscription: 'subscription_id',
plan: 'plan_id',
status: 'subscription status',
name: 'plan nickname e.g. "Monthly"',
interval: 'how often plan charges e.g "month", "year"',
amount: 'amount in smallest denomination e.g. cents, so value for 5 dollars would be 500',
currency: 'e.g. usd',
last4: 'last four digits of card OR null',
validUntil: 'epoch timestamp of when current interval ends IN SECONDS'
});

View File

@ -5,5 +5,5 @@ export default DS.Model.extend({
name: attr('string'),
email: attr('string'),
createdAt: attr('moment-utc'),
subscriptions: attr('member-subscription')
stripe: attr('member-subscription')
});

View File

@ -93,6 +93,7 @@ export default Model.extend(Comparable, ValidationEngine, {
twitterDescription: attr('string'),
html: attr('string'),
locale: attr('string'),
visibility: attr('string'),
metaDescription: attr('string'),
metaTitle: attr('string'),
mobiledoc: attr('json-string'),

View File

@ -29,6 +29,7 @@ export default Model.extend(ValidationEngine, {
return {isActive: true};
}
}),
defaultContentVisibility: attr('string'),
membersSubscriptionSettings: attr('string'),
metaTitle: attr('string'),
metaDescription: attr('string'),

View File

@ -58,7 +58,9 @@ Router.map(function () {
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
this.route('members');
this.route('members', function () {
this.route('import');
});
this.route('member', {path: '/members/:member_id'});
this.route('error404', {path: '/*path'});

View File

@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default Route.extend({
});

View File

@ -41,6 +41,10 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
// Deprecated property (replaced with data.authors)
delete json.author;
if (json.visibility === null) {
delete json.visibility;
}
return json;
}
});

View File

@ -53,6 +53,7 @@
@import "layouts/user.css";
@import "layouts/about.css";
@import "layouts/tags.css";
@import "layouts/members.css";
@import "layouts/error.css";
@import "layouts/apps.css";
@import "layouts/packages.css";

View File

@ -53,6 +53,7 @@
@import "layouts/user.css";
@import "layouts/about.css";
@import "layouts/tags.css";
@import "layouts/members.css";
@import "layouts/error.css";
@import "layouts/apps.css";
@import "layouts/packages.css";

View File

@ -23,23 +23,23 @@
background: var(--whitegrey-l2);
}
.gh-list-row:first-child .gh-list-cell:first-child,
.gh-list-row:first-child .gh-list-data:first-child {
.gh-list-row:first-of-type .gh-list-cell:first-child,
.gh-list-row:first-of-type .gh-list-data:first-child {
border-top-left-radius: 5px;
}
.gh-list-row:first-child .gh-list-cell:last-child,
.gh-list-row:first-child .gh-list-data:last-child {
.gh-list-row:first-of-type .gh-list-cell:last-child,
.gh-list-row:first-of-type .gh-list-data:last-child {
border-top-right-radius: 5px;
}
.gh-list-row:last-child .gh-list-cell:first-child,
.gh-list-row:last-child .gh-list-data:first-child {
.gh-list-row:last-of-type .gh-list-cell:first-child,
.gh-list-row:last-of-type .gh-list-data:first-child {
border-bottom-left-radius: 5px;
}
.gh-list-row:last-child .gh-list-cell:last-child,
.gh-list-row:last-child .gh-list-data:last-child {
.gh-list-row:last-of-type .gh-list-cell:last-child,
.gh-list-row:last-of-type .gh-list-data:last-child {
border-bottom-right-radius: 5px;
}
@ -79,7 +79,7 @@
}
.gh-list-row:nth-of-type(2) .gh-list-data {
border: none;
border-top: none;
}
.gh-list-data.show-on-hover > *,
@ -98,6 +98,10 @@
text-align: right;
}
.gh-list-cellwidth-max {
width: 100%;
}
.gh-list-cellwidth-2-3 {
width: 67%;
}
@ -152,6 +156,55 @@
}
/* Flex lists - header and responsive sizes must be custom
/* -------------------------------------------------- */
.gh-flex-list-row {
display: flex;
align-items: stretch;
justify-content: space-between;
margin: 0;
padding: 0;
border-top: var(--whitegrey) 1px solid;
}
.gh-flex-list-row:first-of-type {
border-top: none;
}
.gh-flex-list-row:not(.header):not(.loading):hover {
background: var(--whitegrey-l2);
}
.gh-flex-list-row:first-of-type {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.gh-flex-list-row:last-of-type {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.gh-flex-list-data {
display: flex;
align-items: center;
padding: 16px 20px;
transition: none !important;
}
.gh-flex-list-auto {
flex: 1 1 auto;
}
.gh-flex-list-grow {
flex-grow: 1;
}
.gh-flex-list-shrink {
flex-grow: 1;
}
/* Fake loading list
/* -------------------------------------------------- */

View File

@ -147,7 +147,7 @@
.modal-body .gh-image-uploader {
margin: 0;
background: color-mod(var(--lightgrey) l(+4%));
background: color-mod(var(--whitegrey) h(+7) s(-4%) l(+5%));
}

View File

@ -235,6 +235,16 @@
max-width: 400px;
}
.gh-flow-content .login span {
height: 37px !important;
line-height: 37px !important;
}
.gh-flow-content .gh-input:focus {
box-shadow: none;
border-color: color-mod(var(--midlightgrey) l(+10%));
}
.gh-flow-content .gh-flow-skip {
display: inline-block;
margin-top: 5px;
@ -343,7 +353,7 @@
}
.gh-flow-content .form-group {
margin-bottom: 20px;
margin-bottom: 20px !important;
}
.gh-flow-content .form-group label {
@ -358,7 +368,7 @@
.gh-flow-content input {
padding: 10px 10px 10px 30px;
font-size: 1.5rem;
font-size: 1.4rem;
line-height: 1.4em;
font-weight: 400;
}

View File

@ -348,6 +348,18 @@
fill: var(--active-menu-color);
}
.gh-nav-list a svg g {
stroke: var(--midgrey);
}
.gh-nav-list a:not(.active):hover svg g {
stroke: var(--darkgrey);
}
.gh-nav-list .active svg g {
stroke: var(--active-menu-color);
}
.gh-nav-list .gh-secondary-action {
position: absolute;
z-index: 999;
@ -423,6 +435,11 @@
border-radius: 5px;
}
.gh-nav-pro .gh-btn-green {
margin: 7px 10px 7px 20px !important;
width: calc(100% - 40px) !important;
}
/* Mobile Nav Menu (Slides out)
/* ---------------------------------------------------------- */
@ -752,6 +769,46 @@
user-select: text;
}
.view-actions input[type="text"] {
padding: 8px 8px 9px;
height: 35px;
font-size: 1.3rem;
}
.view-actions .gh-input-search-icon {
position: absolute;
top: 9px;
left: 9px;
width: 16px;
height: 16px;
fill: color-mod(var(--midlightgrey) l(+5%));
}
.gh-actions-cog {
margin-right: 10px
}
.gh-actions-cog svg {
height: 13px;
width: 13px;
fill: var(--midgrey);
margin-right: 0;
}
.gh-actions-cog svg path {
stroke: initial;
}
.gh-actions-menu {
top: calc(100% + 6px);
right: 10px;
left: auto;
}
.gh-actions-menu.fade-out {
animation-duration: 0.01s;
}
@media (max-width: 400px) {
.view-header {
padding: 0 7px;

View File

@ -0,0 +1,72 @@
/* Members list
/* ----------------------------------------- */
.view-actions input.gh-members-list-searchfield {
min-width: 220px;
padding-left: 30px;
}
p.gh-members-list-email {
margin: -2px 0 -1px;
}
.gh-members-list-subscribed-at {
margin-right: -8px;
padding-right: 0;
}
.gh-members-placeholder {
width: 118px;
margin: -30px 0 15px;
}
/* Member details
/* ----------------------------------------- */
label[for="member-description"] + p {
margin: 0 0 4px;
}
textarea.gh-member-details-textarea {
max-width: 100%;
height: 123px;
}
.gh-member-info-icon {
width: 18px;
height: 18px;
}
.gh-member-stripe-info {
border-bottom: 1px solid var(--whitegrey);
}
.gh-member-stripe-info:last-of-type {
border-bottom: none;
}
.gh-member-stripe-table {
width: 100%;
margin: 10px 0 12px;
}
.gh-member-stripe-table td {
vertical-align: top;
font-size: 1.4rem;
padding: 6px 12px 6px 0;
}
.gh-member-stripe-label {
color: var(--midgrey-d1);
white-space: nowrap;
width: 200px;
}
/* Import modal
/* ---------------------------------------------------------- */
.gh-members-import-results {
margin: 0;
width: auto;
}

View File

@ -21,8 +21,8 @@
}
.user-actions-menu {
top: calc(100% + 17px);
right: 0;
top: calc(100% + 6px);
right: 10px;
left: auto;
}

View File

@ -103,12 +103,12 @@ input {
z-index: 2;
width: 14px;
height: 14px;
fill: color-mod(var(--midgrey) l(+15%));
fill: color-mod(var(--lightgrey) l(-15%) s(-10%));
transform: translateY(-7px);
}
.gh-input-icon.gh-icon-link svg path {
stroke: color-mod(var(--midgrey) l(+15%));
stroke: color-mod(var(--lightgrey) l(-15%) s(-10%));
}
.gh-input-icon input {
@ -133,7 +133,6 @@ select {
background: var(--input-bg-color);
color: var(--darkgrey);
font-size: 1.5rem;
line-height: 1em;
font-weight: 400;
user-select: text;
border-radius: var(--border-radius);
@ -174,13 +173,15 @@ textarea {
min-width: 250px;
min-height: 10rem;
max-width: 500px;
line-height: 1.5;
line-height: 1.5em;
user-select: text;
resize: vertical;
}
textarea.gh-input {
line-height: 1.5em;
.gh-input[disabled] {
background: var(--whitegrey-l2);
color: var(--midlightgrey);
border-color: var(--whitegrey);
}
@ -290,6 +291,64 @@ textarea.gh-input {
opacity: 1;
}
.gh-radio {
display: flex;
margin: 0 0 20px;
}
.gh-radio-button {
flex-shrink: 0;
position: relative;
width: 15px;
height: 15px;
border: color-mod(var(--lightgrey) l(-10%)) 1px solid;
border-radius: 100%;
background: #fff;
}
.gh-radio-content {
display: flex;
flex-direction: column;
margin: 0 0 0 15px;
width: 100%;
}
.gh-radio-label {
font-size: 1.4rem;
line-height: 1.2em;
font-weight: 400;
}
.gh-radio-desc {
font-size: 1.3rem;
line-height: 1.4em;
font-weight: 200;
color: color-mod(var(--midgrey) l(+10%));
}
.gh-radio-label:hover,
.gh-radio-button:hover {
cursor: pointer;
}
.gh-radio.active .gh-radio-button {
border-color: color-mod(var(--blue) l(-12%));
background: var(--blue);
}
.gh-radio.active .gh-radio-button:before {
display: block;
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 7px;
height: 7px;
background: #fff;
border-radius: 100%;
box-shadow: rgba(0,0,0,0.25) 0 1px 3px;
}
/* Switch
/* ---------------------------------------------------------- */

View File

@ -21,6 +21,7 @@
.lh-copy { line-height: var(--lh-1-6); }
.lh-list { line-height: 3.2rem; }
.lh-code { line-height: var(--lh-1-3); }
.lh-zero { line-height: 0; }
@media (--breakpoint-not-small) {
.lh-solid-ns { line-height: var(--lh-1-1); }
@ -29,6 +30,7 @@
.lh-copy-ns { line-height: var(--lh-1-6); }
.lh-list-ns { line-height: var(--lh-2-0); }
.lh-code-ns { line-height: var(--lh-1-3); }
.lh-zero-ns { line-height: 0; }
}
@media (--breakpoint-medium) {
@ -38,6 +40,7 @@
.lh-copy-m { line-height: var(--lh-1-6); }
.lh-list-m { line-height: var(--lh-2-0); }
.lh-code-m { line-height: var(--lh-1-3); }
.lh-zero-m { line-height: 0; }
}
@media (--breakpoint-large) {
@ -47,4 +50,5 @@
.lh-copy-l { line-height: var(--lh-1-6); }
.lh-list-l { line-height: var(--lh-2-0); }
.lh-code-l { line-height: var(--lh-1-3); }
.lh-zero-l { line-height: 0; }
}

View File

@ -0,0 +1,134 @@
<h4 class="midlightgrey f-small fw5 ttu">Basic info</h4>
<div class="pa5 pb0 pt4 br4 shadow-1 bg-grouped-table mt2 flex flex-column flex-row-ns items-start justify-between gh-tag-basic-settings-form">
<div class="flex flex-column items-start mr8 w-100 w-50-ns">
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="name"}}
<label for="member-name">Name</label>
{{gh-text-input
disabled=true
id="member-name"
name="name"
value=(readonly scratchName)
tabindex="1"
input=(action (mut scratchName) value="target.value")
focus-out=(action 'setProperty' 'name' scratchName)}}
{{gh-error-message errors=member.errors property="name"}}
{{/gh-form-group}}
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="email"}}
<label for="member-email">Email</label>
{{gh-text-input
disabled=true
value=(readonly scratchEmail)
id="member-email"
name="email"
tabindex="2"
focus-out=(action 'setProperty' 'email' scratchEmail)
input=(action (mut scratchEmail) value="target.value")}}
{{/gh-form-group}}
</div>
<div class="mb6 mb0-ns w-100 w-50-ns">
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="note"}}
<label for="member-description">Note</label>
{{gh-textarea
disabled=true
id="member-description"
name="description"
class="gh-member-details-textarea"
tabindex="3"
value=(readonly scratchDescription)
input=(action (mut scratchDescription) value="target.value")
focus-out=(action 'setProperty' 'description' scratchDescription)
}}
{{gh-error-message errors=member.errors property="description"}}
<p>Maximum: <b>500</b> characters. Youve used {{gh-count-down-characters scratchDescription 500}}</p>
{{/gh-form-group}}
</div>
</div>
<h4 class="midlightgrey f-small fw5 ttu mt12">Stripe info</h4>
{{#if isLoading}}
<div class="pa20 br4 shadow-1 bg-grouped-table mt2">
<div class="flex justify-center flex-auto">
<div class="gh-loading-spinner"> </div>
</div>
</div>
{{else}}
{{#if subscriptions}}
<div class="br4 shadow-1 bg-grouped-table mt2">
{{#if hasMultipleSubscriptions}}
<div class="pa2 flex flex-column flex-row-ns items-center justify-center f7 fw5 bg-whitegrey-l2 bb b--whitegrey br4 br--top">
{{svg-jar "info" class="gh-member-info-icon mr2 fill-darkgrey"}} Member has multiple Stripe subscriptions
</div>
{{/if}}
{{#each subscriptions as |subscription|}}
<section class="gh-member-stripe-info pa5 pb0 pt4 flex flex-column flex-row-ns items-start justify-between">
<div class="flex items-start w-100">
<div class="w-50 flex-auto mr8">
<h4 class="f8 fw6">Customer data</h4>
<table class="gh-member-stripe-table">
<tr>
<td class="gh-member-stripe-label">Stripe customer ID</td>
<td class="gh-member-stripe-data"><a href="https://dashboard.stripe.com/test/customers/{{subscription.customer}}" target="_blank" rel="noopener" data-tooltip="View on Stripe">{{subscription.customer}}</a></td>
</tr>
<tr>
<td class="gh-member-stripe-label">Name</td>
<td class="gh-member-stripe-data">
{{#if subscription.name}}
{{subscription.name}}
{{else}}
<span class="midlightgrey-l2">No name</span>
{{/if}}
</td>
</tr>
<tr>
<td class="gh-member-stripe-label">Email</td>
<td class="gh-member-stripe-data">
{{#if subscription.email}}
{{subscription.email}}
{{else}}
<span class="midlightgrey-l2">No email</span>
{{/if}}
</td>
</tr>
<tr>
<td class="gh-member-stripe-label">Customer since</td>
<td class="gh-member-stripe-data">
{{#if subscription.startDate}}
{{subscription.startDate}}
{{else}}
<span class="midlightgrey-l2">No data</span>
{{/if}}
</td>
</tr>
</table>
</div>
<div class="w-50 flex-auto">
<h4 class="f8 fw6">Subscription data</h4>
<table class="gh-member-stripe-table">
<tr>
<td class="gh-member-stripe-label">Plan</td>
<td class="gh-member-stripe-data">{{subscription.planName}}</td>
</tr>
<tr>
<td class="gh-member-stripe-label">Current status</td>
<td class="gh-member-stripe-data">{{subscription.status}}</td>
</tr>
<tr>
<td class="gh-member-stripe-label">Next renewal date</td>
<td class="gh-member-stripe-data">{{subscription.validUntil}}</td>
</tr>
</table>
</div>
</div>
</section>
{{/each}}
</div>
{{else}}
<div class="pa20 br4 shadow-1 bg-grouped-table mt2">
<p class="ma0 pa0 tc midgrey">Member doesn't have Stripe subscription</p>
</div>
{{/if}}
{{/if}}

View File

@ -0,0 +1,25 @@
{{#link-to "member" member class="gh-flex-list-data gh-flex-list-auto" title="Member details"}}
<div class="flex items-center">
<GhMemberAvatar @member={{member}} class="w9 h9 mr3" />
<div>
<h3 class="ma0 pa0 gh-members-list-name">
{{#if this.name}}
{{this.name}}
{{else}}
{{this.email}}
{{/if}}
</h3>
{{#if this.name}}
<p class="ma0 pa0 middarkgrey f8 gh-members-list-email">{{this.email}}</p>
{{/if}}
</div>
</div>
{{/link-to}}
{{#link-to "member" member class="gh-flex-list-data gh-members-list-subscribed-at middarkgrey f8 nowrap" title="Member details" }}
Member since {{this.subscribedAt}}
{{/link-to}}
{{#link-to "member" member class="gh-flex-list-data" title="Member details"}}
{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1 nr2"}}
{{/link-to}}

View File

@ -75,6 +75,17 @@
</div>
{{/unless}}
{{#if feature.members}}
{{#unless session.user.isContributor}}
<div class="form-group">
<label for="visibility-input">Post access</label>
{{gh-psm-visibility-input post=post triggerId="visibility-input"}}
</div>
{{/unless}}
{{/if}}
{{#gh-form-group errors=post.errors hasValidated=post.hasValidated property="customExcerpt"}}
<label for="custom-excerpt">Excerpt</label>
{{gh-textarea

View File

@ -0,0 +1,10 @@
<span class="gh-select">
{{one-way-select selectedVisibility
options=availableVisibilities
optionValuePath="name"
optionLabelPath="label"
optionTargetPath="name"
update=(action "updateVisibility")
}}
{{svg-jar "arrow-down-small"}}
</span>

View File

@ -0,0 +1,47 @@
<header class="modal-header" data-test-modal="import-members">
<h1>
{{#if response}}
Import result
{{else}}
Import members
{{/if}}
</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
<div class="modal-body bg-whitegrey-l2 ba b--whitegrey br3">
{{#if response}}
<table class="gh-members-import-results">
<tr>
<td>Imported:</td>
<td align="left" data-test-text="import-members-imported">{{response.imported}}</td>
</tr>
{{#if response.duplicates}}
<tr>
<td>Duplicates:</td>
<td align="left" data-test-text="import-members-duplicates">{{response.duplicates}}</td>
</tr>
{{/if}}
{{#if response.invalid}}
<tr>
<td class="red">Invalid:</td>
<td align="left" data-test-text="import-members-invalid" class="red">{{response.invalid}}</td>
</tr>
{{/if}}
</table>
{{else}}
{{gh-file-uploader
url=uploadUrl
paramName="membersfile"
labelText="Select or drag-and-drop a CSV file."
uploadStarted=(action "uploadStarted")
uploadFinished=(action "uploadFinished")
uploadSuccess=(action "uploadSuccess")}}
{{/if}}
</div>
<div class="modal-footer">
<button {{action "closeModal"}} disabled={{closeDisabled}} class="gh-btn" data-test-button="close-import-members">
<span>{{#if response}}Close{{else}}Cancel{{/if}}</span>
</button>
</div>

View File

@ -1,42 +1,35 @@
<section class="gh-canvas">
<GhCanvasHeader 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>
</GhCanvasHeader>
<section class="view-container">
<div class="br3 bg-white mt2 shadow-1">
<div class="flex flex-row pa5 bb b--whitegrey">
<GhMemberAvatar class="w20 h20 mr4" @initialsClass="f-subheadline" @member={{member}} />
<div class="flex flex-column justify-center">
<h3 class="ma0 pa0 fw5">{{member.name}}</h3>
<span class="db">
<a class="midlightgrey" href="mailto:{{member.email}}">
{{member.email}}
</a>
</span>
</div>
</div>
<div class="flex flex-row">
<div class="flex flex-column flex-grow-1 pa5 br b--whitegrey w-50">
<span class="db ttu f8 midlightgrey">Member since</span>
<span class="db f5">{{moment-format member.createdAt "MMMM Do"}}</span>
<span class="db f8 midlightgrey">({{moment-to-now member.createdAt hideAffix=true}})</span>
</div>
<div class="flex flex-column flex-grow-1 pa5 w-50">
<span class="db ttu f8 midlightgrey">Current plan</span>
<span class="db f5 {{if (eq subscription.plan '...') "midlightgrey"}}">{{subscription.plan}}</span>
{{#unless (eq subscription.amount '...')}}
<span class="db f8 midlightgrey">{{subscription.amount}} USD/month</span>
{{/unless}}
</div>
<form class="mb10 member-basic-info-form" {{action (perform "save") on="submit"}}>
<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>
{{#if member.name}}
{{member.name}}
{{else}}
{{member.email}}
{{/if}}
</h2>
</GhCanvasHeader>
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
<GhMemberAvatar @member={{member}} @sizeClass={{'f-headline fw4 lh-zero'}} class="w18 h18 mr4" />
<div>
<h3 class="f2 fw5 ma0 pa0">
{{if member.name member.name member.email}}
</h3>
<p class="f6 pa0 ma0 midgrey">
{{#if member.name}}
<span class="darkgrey">{{member.email}}</span>
{{/if}}
Member since {{this.subscribedAt}}
</p>
</div>
</div>
<h2 class="pb1 bb b--whitegrey-d1 f-small fw5 midlightgrey ttu mt10 pb2">Danger zone</h2>
{{gh-member-settings-form member=member
setProperty=(action "setProperty")
isLoading=this.isLoading
showDeleteTagModal=(action "toggleDeleteTagModal")}}
</form>
<button
type="button"
class="gh-btn gh-btn-red gh-btn-icon mt3"
@ -45,7 +38,6 @@
>
<span>Delete member</span>
</button>
</section>
</section>
{{#if showDeleteMemberModal}}
@ -53,4 +45,4 @@
model=(hash member=member onSuccess=(action "finaliseDeletion"))
close=(action (toggle "showDeleteMemberModal" this))
modifier="action wide"}}
{{/if}}
{{/if}}

View File

@ -1,15 +1,38 @@
<section class="gh-canvas">
<GhCanvasHeader class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
<div>
<GhTextInput placeholder="Search members..." @value={{this.searchText}}
@input={{action (mut this.searchText) value="target.value"}} />
</div>
<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="user-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}}
</span>
<div class="relative">
{{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" />
</div>
</section>
</GhCanvasHeader>
<section class="view-container">
<section class="view-container h-100">
{{#if (or filteredMembers this.fetchMembers.isRunning this.searchText)}}
<div class="flex justify-between items-center">
<h2 class="f-small fw5 midlightgrey ttu">
<h2 class="f-small fw5 midlightgrey ttu mb2">
{{#if this.searchText}}
Search result
{{else}}
@ -22,41 +45,44 @@
{{/if}}
</h2>
</div>
{{#if this.filteredMembers}}
{{!-- members list, styles taken from .apps-grid --}}
<div class="flex flex-row flex-wrap items-start br3 mt2 mb10 bg-grouped-table shadow-1">
<VerticalCollection
@items={{this.filteredMembers}}
@key="id"
@containerSelector=".gh-main"
@estimateHeight=76
as |member index|
>
<div class="flex-grow-1 flex-shrink-1" style="flex-basis: 100%">
{{#link-to "member" member classNames="link whitegrey"}}
<article class="flex items-center justify-between pa4 pt3 pb3 {{if index "bt"}} highlight-whitegrey">
<div class="flex items-center">
<GhMemberAvatar @member={{member}} class="w9 h9 mr4" />
<div class="flex flex-column">
<h3 class="darkgrey ma0 f6 fw6">{{member.name}}</h3>
<p class="midgrey ma0 pa0 f7">{{member.email}}</p>
</div>
</div>
<div class="flex items-center">{{svg-jar "arrow-right" class="w4 h4 fill-midlightgrey"}}</div>
</article>
{{/link-to}}
</div>
</VerticalCollection>
</div>
{{else}}
{{#if this.fetchMembers.isRunning}}
<div class="gh-content">
<GhLoadingSpinner />
</div>
{{else}}
<p class="mt2 pt10 bt b--whitegrey tc midlightgrey">No members found.</p>
{{/if}}
{{/if}}
<section class="content-list">
<ol class="members-list gh-list {{unless filteredMembers "no-posts"}}">
{{#if filteredMembers}}
{{#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}}
{{else}}
{{#if this.fetchMembers.isRunning}}
<div class="gh-content">
<GhLoadingSpinner />
</div>
{{else}}
<li class="no-posts-box">
<div class="no-posts">
{{svg-jar "members-placeholder" class="gh-members-placeholder"}}
{{#if this.searchText}}
<h3>No members found!</h3>
{{else}}
<h3>You don't have any members</h3>
{{/if}}
</div>
</li>
{{/if}}
{{/if}}
</ol>
</section>
</section>
</section>
</section>
{{outlet}}

View File

@ -0,0 +1,4 @@
{{gh-fullscreen-modal "import-members"
close=(action "close")
confirm=(action "fetchNewMembers")
modifier="action wide"}}

View File

@ -87,6 +87,126 @@
</div>
</div>
<div class="gh-setting-header">Members</div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
{{#if config.enableDeveloperExperiments}}
<div class="gh-setting-first gh-setting-last">
<div class="gh-setting-content">
<div class="gh-setting-title">Members</div>
<div class="gh-setting-desc">Enable free or paid member registration.</div>
{{#liquid-if feature.labs.members class="nr25"}}
<div class="flex ba br4 b--whitegrey pa5 pt4 mt5">
<section class="flex flex-column">
<label class="fw6 f8">Stripe publishable API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.public_token)
input=(action "setSubscriptionSettings" "public_token")
class="mt1"
}}
<label class="fw6 f8 mt4">Stripe secret API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.secret_token)
input=(action "setSubscriptionSettings" "secret_token")
class="mt1"
}}
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">Where to find Stripe
API keys</a>
<div class="mt5 flex nb5">
<div class="w-50 mr3">
{{#gh-form-group}}
<label class="fw6 f8">Monthly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-monthly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "month")
}}
</div>
{{/gh-form-group}}
</div>
<div class="w-50 ml2">
{{#gh-form-group class="description-container"}}
<label class="fw6 f8">Yearly price</label>
<div class="mt1 relative gh-labs-price-label gh-labs-yearly-price">
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.plans.yearly.dollarAmount)
type="number"
input=(action "setSubscriptionSettings" "year")
}}
</div>
{{/gh-form-group}}
</div>
</div>
<div class="mt5 flex nb5">
{{#gh-form-group class="require-payment-container"}}
<div class="form-group for-checkbox">
<label class="checkbox" for="members-require-payment" {{action "setSubscriptionSettings" "requirePaymentForSignup" bubbles="false"}}>
<input
type="checkbox"
checked={{subscriptionSettings.requirePaymentForSignup}}
class="gh-input"
onclick={{action "setSubscriptionSettings" "requirePaymentForSignup"}}
data-test-checkbox="members-require-payment"
>
<span class="input-toggle-component"></span>
<p> Require payment before signing up members</p>
</label>
</div>
{{/gh-form-group}}
</div>
</section>
<section class="gh-visibility-menu-content w-50 ml10">
<label class="dib f8 fw6 mb4">Default post access</label>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "public") "active"}}"
{{action "setDefaultContentVisibility" "public" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-unpublished-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Public</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "members") "active"}}"
{{action "setDefaultContentVisibility" "members" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Members only</div>
</div>
</div>
<div class="gh-radio {{if (eq settings.defaultContentVisibility "paid") "active"}}"
{{action "setDefaultContentVisibility" "paid" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-radio-content">
<div class="gh-radio-label">Paid-members only</div>
</div>
</div>
</section>
</div>
<div class="mb2 mt5">
{{gh-task-button "Save members settings"
task=saveSettings
successText="Saved"
runningText="Saving"
class="gh-btn gh-btn-blue gh-btn-icon"
}}
</div>
{{/liquid-if}}
</div>
<div class="gh-setting-action">
<div class="for-switch">{{gh-feature-flag "members"}}</div>
</div>
</div>
{{/if}}
</div>
<div class="gh-setting-header">Beta features</div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
<div class="gh-setting-first">
@ -208,6 +328,17 @@
</div>
</div>
{{/if}}
{{#if (not-eq feature.labs.publicAPI undefined)}}
<div class="gh-setting">
<div class="gh-setting-content">
<div class="gh-setting-title">Public API (deprecated)</div>
<div class="gh-setting-desc">⚠️ Please use the Content API instead, more info in <a href="https://ghost.org/docs/api/content/">the docs</a></div>
</div>
<div class="gh-setting-action">
<div class="for-switch">{{gh-feature-flag "publicAPI"}}</div>
</div>
</div>
{{/if}}
<div class="gh-setting">
{{#gh-uploader
extensions=jsonExtension

View File

@ -6,7 +6,7 @@ export default Transform.extend({
deserialize(serialized) {
let subscriptions, subscriptionArray;
subscriptionArray = serialized || [];
subscriptionArray = serialized.subscriptions || [];
subscriptions = subscriptionArray.map(itemDetails => MemberSubscription.create(itemDetails));
@ -30,6 +30,8 @@ export default Transform.extend({
subscriptionArray = [];
}
return subscriptionArray;
return {
subscriptions: subscriptionArray
};
}
});

View File

@ -80,7 +80,7 @@
{{#if isSelected}}
<button
title="Toggle between editing alt text and caption"
class="absolute right-0 bottom-0 ma2 pl1 pr1 ba br3 f7 sans-serif fw4 lh-title tracked-2 {{if this.isEditingAlt "bg-blue b--blue white" "b--midlightgrey midlightgrey"}}"
class="absolute right-0 bottom-0 ma2 pl1 pr1 ba br3 f8 sans-serif fw4 lh-title tracked-2 bg-white {{if this.isEditingAlt "bg-blue b--blue white" "b--midlightgrey midlightgrey"}}"
{{on "click" this.toggleAltEditing passive=true}}
>
Alt

View File

@ -13,6 +13,7 @@ export default Factory.extend({
featureImage(i) { return `/content/images/2015/10/post-${i}.jpg`; },
html(i) { return `<p>HTML for post ${i}.</p>`; },
locale: null,
visibility: 'public',
metaDescription(i) { return `Meta description for post ${i}.`; },
metaTitle(i) { return `Meta Title for post ${i}`; },
ogDescription: null,

View File

@ -98,7 +98,7 @@ export default [
created_at: '2015-09-11T09:44:30.810Z',
created_by: 1,
key: 'is_private',
type: 'blog',
type: 'private',
updated_at: '2015-09-23T13:32:49.868Z',
updated_by: 1,
value: false
@ -108,7 +108,7 @@ export default [
created_at: '2015-09-11T09:44:30.810Z',
created_by: 1,
key: 'password',
type: 'blog',
type: 'private',
updated_at: '2015-09-23T13:32:49.868Z',
updated_by: 1,
value: ''
@ -121,7 +121,7 @@ export default [
type: 'blog',
updated_at: '2016-05-05T18:33:09.168Z',
updated_by: 1,
value: '[{"url":""}]'
value: '[{"url":"", "username":"Ghost"}]'
},
{
id: 17,
@ -172,5 +172,15 @@ export default [
created_by: 1,
updated_at: '2015-10-27T17:39:58.276Z',
updated_by: 1
},
{
id: 23,
type: 'members',
key: 'members_subscription_settings',
value: '{"isPaid":false,"paymentProcessors":[{"adapter":"stripe","config":{"secret_token":"","public_token":"","product":{"name":"Ghost Subscription"},"plans":[{"name":"Monthly","currency":"usd","interval":"month","amount":""},{"name":"Yearly","currency":"usd","interval":"year","amount":""}]}}]}',
created_at: '2019-10-09T09:49:00.000Z',
created_by: 1,
updated_at: '2019-10-09T09:49:00.000Z',
updated_by: 1
}
];

View File

@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "2.31.0",
"version": "2.33.0",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",
@ -25,17 +25,17 @@
"node": "^8.10.0 || ^10.13.0"
},
"devDependencies": {
"@ember/jquery": "0.6.1",
"@ember/optional-features": "0.7.0",
"@ember/render-modifiers": "1.0.1",
"@ember/jquery": "1.1.0",
"@ember/optional-features": "1.0.0",
"@ember/render-modifiers": "1.0.2",
"@html-next/vertical-collection": "1.0.0",
"@tryghost/helpers": "1.1.9",
"@tryghost/helpers": "1.1.12",
"@tryghost/kg-clean-basic-html": "0.1.3",
"@tryghost/kg-parser-plugins": "0.8.0",
"@tryghost/mobiledoc-kit": "0.11.2-ghost.4",
"@tryghost/string": "0.1.5",
"@tryghost/timezone-data": "0.2.8",
"autoprefixer": "9.6.1",
"@tryghost/timezone-data": "0.2.11",
"autoprefixer": "9.6.4",
"blueimp-md5": "2.12.0",
"broccoli-asset-rev": "3.0.0",
"broccoli-concat": "3.7.4",
@ -52,32 +52,33 @@
"ember-auto-import": "1.5.2",
"ember-cli": "3.12.0",
"ember-cli-app-version": "3.2.0",
"ember-cli-babel": "7.11.0",
"ember-cli-babel": "7.12.0",
"ember-cli-chai": "0.5.0",
"ember-cli-chart": "3.5.0",
"ember-cli-dependency-checker": "3.2.0",
"ember-cli-deprecation-workflow": "1.0.1",
"ember-cli-eslint": "5.1.0",
"ember-cli-htmlbars": "3.1.0",
"ember-cli-htmlbars-inline-precompile": "3.0.0",
"ember-cli-htmlbars": "4.0.5",
"ember-cli-htmlbars-inline-precompile": "3.0.1",
"ember-cli-inject-live-reload": "2.0.1",
"ember-cli-mirage": "1.1.1",
"ember-cli-mirage": "1.1.3",
"ember-cli-moment-shim": "3.7.1",
"ember-cli-node-assets": "0.2.2",
"ember-cli-postcss": "5.0.0",
"ember-cli-pretender": "3.1.1",
"ember-cli-shims": "1.2.0",
"ember-cli-string-helpers": "4.0.4",
"ember-cli-string-helpers": "4.0.5",
"ember-cli-test-loader": "2.2.0",
"ember-cli-uglify": "3.0.0",
"ember-composable-helpers": "2.3.1",
"ember-concurrency": "1.0.0",
"ember-data": "3.12.2",
"ember-concurrency": "1.1.0",
"ember-data": "3.12.4",
"ember-drag-drop": "0.4.8",
"ember-exam": "4.0.1",
"ember-exam": "4.0.4",
"ember-export-application-global": "2.0.0",
"ember-fetch": "6.7.1",
"ember-in-viewport": "3.5.8",
"ember-infinity": "1.4.8",
"ember-infinity": "1.4.9",
"ember-load": "0.0.17",
"ember-load-initializers": "2.1.0",
"ember-mocha": "0.16.0",
@ -86,18 +87,18 @@
"ember-power-calendar-moment": "0.1.7",
"ember-power-datepicker": "0.6.2",
"ember-power-select": "2.3.5",
"ember-resolver": "5.2.1",
"ember-resolver": "5.3.0",
"ember-route-action-helper": "2.0.7",
"ember-simple-auth": "1.9.2",
"ember-sinon": "4.1.1",
"ember-source": "3.12.0",
"ember-sticky-element": "0.2.3",
"ember-svg-jar": "2.2.1",
"ember-svg-jar": "2.2.3",
"ember-test-selectors": "2.1.0",
"ember-truth-helpers": "2.1.0",
"ember-useragent": "0.9.1",
"emberx-file-input": "1.2.1",
"eslint": "6.2.2",
"eslint": "6.5.1",
"eslint-plugin-ghost": "0.5.0",
"faker": "4.1.0",
"fs-extra": "8.1.0",
@ -122,7 +123,7 @@
"postcss-easy-import": "3.0.0",
"reframe.js": "2.2.5",
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost",
"testem": "2.17.0",
"testem": "3.0.0",
"top-gh-contribs": "2.0.4",
"validator": "7.2.0",
"walk-sync": "2.0.2"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>information-circle</title><path d="M12 23.501c-6.341 0-11.5-5.159-11.5-11.5S5.659.501 12 .501s11.5 5.159 11.5 11.5-5.159 11.5-11.5 11.5zm0-22c-5.79 0-10.5 4.71-10.5 10.5s4.71 10.5 10.5 10.5 10.5-4.71 10.5-10.5-4.71-10.5-10.5-10.5z"/><path d="M13 17.505c-.827 0-1.5-.673-1.5-1.5v-6.5H10c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h1.5c.551 0 1 .449 1 1v6.5c0 .276.224.5.5.5h1.5c.276 0 .5.224.5.5s-.224.5-.5.5H13zM11.745 7.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75.75.336.75.75-.336.75-.75.75z"/></svg>

After

Width:  |  Height:  |  Size: 559 B

View File

@ -0,0 +1 @@
<svg width="138" height="138" viewBox="0 0 138 138" xmlns="http://www.w3.org/2000/svg"><title>Combined Shape</title><g fill="none" fill-rule="evenodd"><path d="M69 .5c37.832 0 68.5 30.668 68.5 68.5s-30.668 68.5-68.5 68.5S.5 106.832.5 69 31.168.5 69 .5zM24.82 98.183h50.347C75.025 88.69 70.84 81.579 62.58 76.79c-8.404-4.871-16.772-4.871-25.175 0-8.26 4.788-12.445 11.899-12.586 21.392zm50.347 0h.503l-.5.5c0-.168 0-.335-.003-.5zm-.663-21.045c5.182 5.666 8.348 12.685 9.498 21.044h28.848c-.14-9.492-4.325-16.603-12.585-21.391-8.404-4.871-16.772-4.871-25.176 0-.198.115-.393.23-.585.347zM49.994 37.58c-4.173 0-7.72 1.475-10.67 4.437s-4.42 6.525-4.42 10.714c0 4.19 1.47 7.752 4.42 10.714 2.95 2.962 6.497 4.437 10.67 4.437 4.171 0 7.718-1.475 10.669-4.437 2.95-2.962 4.42-6.524 4.42-10.714s-1.47-7.752-4.42-10.714c-2.95-2.962-6.498-4.437-10.67-4.437zm37.683 0c-4.172 0-7.719 1.475-10.67 4.437-2.95 2.962-4.42 6.525-4.42 10.714 0 4.19 1.47 7.752 4.42 10.714 2.951 2.962 6.498 4.437 10.67 4.437s7.72-1.475 10.67-4.437 4.42-6.524 4.42-10.714-1.47-7.752-4.42-10.714c-2.95-2.962-6.498-4.437-10.67-4.437z" stroke="#9BAEB8" fill-opacity=".1" fill="#9BAEB8" stroke-linecap="square"/><path stroke-opacity=".012" stroke="#000" stroke-width="0" d="M25 25h88v88H25z"/><path d="M62.832 76.358C71.39 81.32 75.67 88.761 75.67 98.683H24.316c0-9.922 4.28-17.364 12.839-22.325 8.559-4.96 17.118-4.96 25.677 0zm37.684 0c8.559 4.961 12.838 12.403 12.838 22.325l-29.79-.001c-1.114-8.672-4.394-15.888-9.84-21.648.363-.23.734-.455 1.115-.676 8.559-4.96 17.118-4.96 25.677 0zM49.993 37.08c4.305 0 7.98 1.528 11.024 4.584 3.044 3.056 4.566 6.745 4.566 11.067s-1.522 8.01-4.566 11.067c-3.044 3.056-6.719 4.584-11.024 4.584-4.305 0-7.98-1.528-11.023-4.584-3.044-3.056-4.566-6.745-4.566-11.067s1.522-8.01 4.566-11.067c3.044-3.056 6.718-4.584 11.023-4.584zm37.684 0c4.305 0 7.98 1.528 11.024 4.584 3.044 3.056 4.566 6.745 4.566 11.067s-1.522 8.01-4.566 11.067c-3.044 3.056-6.719 4.584-11.024 4.584-4.305 0-7.98-1.528-11.023-4.584-3.045-3.056-4.567-6.745-4.567-11.067s1.522-8.01 4.567-11.067c3.044-3.056 6.718-4.584 11.023-4.584z" stroke="#9BAEB8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -63,7 +63,7 @@ describe('Acceptance: Members', function () {
await click('[data-test-nav="members"]');
expect(currentURL()).to.equal('/members');
expect(currentRouteName()).to.equal('members');
expect(currentRouteName()).to.equal('members.index');
expect(find('[data-test-screen-title]')).to.have.text('Members');
});
});

View File

@ -0,0 +1,43 @@
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import {blur, fillIn, find, findAll, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-psm-visibility-input', function () {
setupRenderingTest();
it('renders', async function () {
this.set('post', {
visibility: 'members'
});
await render(hbs`{{gh-psm-visibility-input post=post}}`);
expect(this.element, 'top-level elements').to.exist;
expect(findAll('option'), 'number of options').to.have.length(3);
expect(find('select').value, 'selected option value').to.equal('members');
});
it('updates post visibility on change', async function () {
let setVisibility = sinon.spy();
this.set('post', {
visibility: 'public',
set: setVisibility
});
await render(hbs`{{gh-psm-visibility-input post=post}}`);
expect(this.element, 'top-level elements').to.exist;
expect(findAll('option'), 'number of options').to.have.length(3);
expect(find('select').value, 'selected option value').to.equal('public');
await fillIn('select', 'paid');
await blur('select');
expect(setVisibility.calledOnce).to.be.true;
expect(setVisibility.calledWith('visibility', 'paid')).to.be.true;
});
});

File diff suppressed because it is too large Load Diff