mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Removed usage of ember-light-table in subscribers screen (#1191)
no issue `ember-light-table` is falling behind Ember.js and other addon development and is increasingly causing issues with Ember deprecations and addon incompatibility. - swaps `ember-light-table` usage for a straightforward table using `vertical-collection` for occlusion - uses the same loading mechanism as the members screen with a slight optimisation where the initial load will fetch subscribers in batches of 200 until they are all loaded - removes now-unused pagination mixin - fixes duplicate subscriber validation handling
This commit is contained in:
parent
3afcbf0c39
commit
a7f4610381
@ -1,17 +0,0 @@
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['subscribers-table'],
|
||||
|
||||
table: null,
|
||||
|
||||
actions: {
|
||||
onScrolledToBottom() {
|
||||
let loadNextPage = this.loadNextPage;
|
||||
|
||||
if (!this.isLoading) {
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -29,10 +29,10 @@ export default ModalComponent.extend({
|
||||
// properly so that errors are added to model.errors automatically
|
||||
if (error && isInvalidError(error)) {
|
||||
let [firstError] = error.payload.errors;
|
||||
let {message} = firstError;
|
||||
let {context} = firstError;
|
||||
|
||||
if (message && message.match(/email/i)) {
|
||||
this.get('subscriber.errors').add('email', message);
|
||||
if (context && context.match(/email/i)) {
|
||||
this.get('subscriber.errors').add('email', context);
|
||||
this.get('subscriber.hasValidated').pushObject('email');
|
||||
return;
|
||||
}
|
||||
|
@ -1,122 +1,64 @@
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
import $ from 'jquery';
|
||||
import Controller from '@ember/controller';
|
||||
import PaginationMixin from 'ghost-admin/mixins/pagination';
|
||||
import Table from 'ember-light-table';
|
||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import {assign} from '@ember/polyfills';
|
||||
import moment from 'moment';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend(PaginationMixin, {
|
||||
const orderMap = {
|
||||
email: 'email',
|
||||
created_at: 'createdAtUTC',
|
||||
status: 'status'
|
||||
};
|
||||
|
||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
||||
export default Controller.extend({
|
||||
session: service(),
|
||||
|
||||
queryParams: ['order', 'direction'],
|
||||
order: 'created_at',
|
||||
direction: 'desc',
|
||||
|
||||
paginationModel: 'subscriber',
|
||||
|
||||
total: 0,
|
||||
table: null,
|
||||
subscribers: null,
|
||||
subscriberToDelete: null,
|
||||
|
||||
// paginationSettings is replaced by the pagination mixin so we need a
|
||||
// getter/setter CP here so that we don't lose the dynamic order param
|
||||
paginationSettings: computed('order', 'direction', {
|
||||
get() {
|
||||
let order = this.order;
|
||||
let direction = this.direction;
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('subscribers', this.store.peekAll('subscriber'));
|
||||
},
|
||||
|
||||
let currentSettings = this._paginationSettings || {
|
||||
limit: 30
|
||||
};
|
||||
|
||||
return assign({}, currentSettings, {
|
||||
order: `${order} ${direction}`
|
||||
});
|
||||
},
|
||||
set(key, value) {
|
||||
this._paginationSettings = value;
|
||||
return value;
|
||||
}
|
||||
filteredSubscribers: computed('subscribers.@each.{email,createdAtUTC}', function () {
|
||||
return this.subscribers.toArray().filter((subscriber) => {
|
||||
return !subscriber.isNew && !subscriber.isDeleted;
|
||||
});
|
||||
}),
|
||||
|
||||
columns: computed('order', 'direction', function () {
|
||||
let order = this.order;
|
||||
let direction = this.direction;
|
||||
sortedSubscribers: computed('order', 'direction', 'subscribers.@each.{email,createdAtUTC,status}', function () {
|
||||
let {filteredSubscribers, order, direction} = this;
|
||||
|
||||
return [{
|
||||
label: 'Email Address',
|
||||
valuePath: 'email',
|
||||
sorted: order === 'email',
|
||||
ascending: direction === 'asc',
|
||||
classNames: ['gh-subscribers-table-email-cell'],
|
||||
cellClassNames: ['gh-subscribers-table-email-cell']
|
||||
}, {
|
||||
label: 'Subscription Date',
|
||||
valuePath: 'createdAtUTC',
|
||||
format(value) {
|
||||
return value.format('MMMM DD, YYYY');
|
||||
},
|
||||
sorted: order === 'created_at',
|
||||
ascending: direction === 'asc',
|
||||
classNames: ['gh-subscribers-table-date-cell'],
|
||||
cellClassNames: ['gh-subscribers-table-date-cell']
|
||||
}, {
|
||||
label: 'Status',
|
||||
valuePath: 'status',
|
||||
sorted: order === 'status',
|
||||
ascending: direction === 'asc',
|
||||
classNames: ['gh-subscribers-table-status-cell'],
|
||||
cellClassNames: ['gh-subscribers-table-status-cell']
|
||||
}, {
|
||||
label: '',
|
||||
sortable: false,
|
||||
cellComponent: 'gh-subscribers-table-delete-cell',
|
||||
align: 'right',
|
||||
classNames: ['gh-subscribers-table-delete-cell'],
|
||||
cellClassNames: ['gh-subscribers-table-delete-cell']
|
||||
}];
|
||||
let sorted = filteredSubscribers.sort((a, b) => {
|
||||
let values = [a.get(orderMap[order]), b.get(orderMap[order])];
|
||||
|
||||
if (direction === 'desc') {
|
||||
values = values.reverse();
|
||||
}
|
||||
|
||||
if (typeof values[0] === 'string') {
|
||||
return values[0].localeCompare(values[1], undefined, {ignorePunctuation: true});
|
||||
}
|
||||
|
||||
if (typeof values[0] === 'object' && values[0]._isAMomentObject) {
|
||||
return values[0].valueOf() - values[1].valueOf();
|
||||
}
|
||||
|
||||
return values[0] - values[1];
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
loadFirstPage() {
|
||||
let table = this.table;
|
||||
|
||||
return this._super(...arguments).then((results) => {
|
||||
table.addRows(results);
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
loadNextPage() {
|
||||
let table = this.table;
|
||||
|
||||
return this._super(...arguments).then((results) => {
|
||||
table.addRows(results);
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
sortByColumn(column) {
|
||||
let table = this.table;
|
||||
|
||||
if (column.sorted) {
|
||||
this.setProperties({
|
||||
order: column.get('valuePath').trim().replace(/UTC$/, '').underscore(),
|
||||
direction: column.ascending ? 'asc' : 'desc'
|
||||
});
|
||||
table.setRows([]);
|
||||
this.send('loadFirstPage');
|
||||
}
|
||||
},
|
||||
|
||||
addSubscriber(subscriber) {
|
||||
this.table.insertRowAt(0, subscriber);
|
||||
this.incrementProperty('total');
|
||||
},
|
||||
|
||||
deleteSubscriber(subscriber) {
|
||||
this.set('subscriberToDelete', subscriber);
|
||||
},
|
||||
@ -126,8 +68,6 @@ export default Controller.extend(PaginationMixin, {
|
||||
|
||||
return subscriber.destroyRecord().then(() => {
|
||||
this.set('subscriberToDelete', null);
|
||||
this.table.removeRow(subscriber);
|
||||
this.decrementProperty('total');
|
||||
});
|
||||
},
|
||||
|
||||
@ -135,11 +75,6 @@ export default Controller.extend(PaginationMixin, {
|
||||
this.set('subscriberToDelete', null);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.table.setRows([]);
|
||||
this.send('loadFirstPage');
|
||||
},
|
||||
|
||||
exportData() {
|
||||
let exportUrl = ghostPaths().url.api('subscribers/csv');
|
||||
let accessToken = this.get('session.data.authenticated.access_token');
|
||||
@ -154,14 +89,28 @@ export default Controller.extend(PaginationMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
initializeTable() {
|
||||
this.set('table', new Table(this.columns, this.subscribers));
|
||||
},
|
||||
fetchSubscribers: task(function* () {
|
||||
let newFetchDate = new Date();
|
||||
let results;
|
||||
|
||||
// capture the total from the server any time we fetch a new page
|
||||
didReceivePaginationMeta(meta) {
|
||||
if (meta && meta.pagination) {
|
||||
this.set('total', meta.pagination.total);
|
||||
if (this._hasFetchedAll) {
|
||||
// fetch any records modified since last fetch
|
||||
results = yield this.store.query('subscriber', {
|
||||
limit: 'all',
|
||||
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`
|
||||
});
|
||||
} else {
|
||||
// fetch all records in batches of 200
|
||||
while (!results || results.meta.pagination.page < results.meta.pagination.pages) {
|
||||
results = yield this.store.query('subscriber', {
|
||||
limit: 200,
|
||||
order: `${this.order} ${this.direction}`,
|
||||
page: results ? results.meta.pagination.page + 1 : 1
|
||||
});
|
||||
}
|
||||
this._hasFetchedAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._lastFetchDate = newFetchDate;
|
||||
})
|
||||
});
|
||||
|
19
ghost/admin/app/controllers/subscribers/import.js
Normal file
19
ghost/admin/app/controllers/subscribers/import.js
Normal 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({
|
||||
subscribers: controller(),
|
||||
router: service(),
|
||||
|
||||
actions: {
|
||||
fetchNewSubscribers() {
|
||||
this.subscribers.fetchSubscribers.perform();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.router.transitionTo('subscribers');
|
||||
}
|
||||
}
|
||||
});
|
21
ghost/admin/app/helpers/subscribers-query-params.js
Normal file
21
ghost/admin/app/helpers/subscribers-query-params.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {helper} from '@ember/component/helper';
|
||||
|
||||
export function subscribersQueryParams([order, currentOrder, direction]) {
|
||||
// if order hasn't changed we toggle the direction
|
||||
if (order === currentOrder) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
return [
|
||||
'subscribers',
|
||||
{
|
||||
isQueryParams: true,
|
||||
values: {
|
||||
order,
|
||||
direction
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export default helper(subscribersQueryParams);
|
14
ghost/admin/app/helpers/subscribers-sort-icon.js
Normal file
14
ghost/admin/app/helpers/subscribers-sort-icon.js
Normal file
@ -0,0 +1,14 @@
|
||||
import {helper} from '@ember/component/helper';
|
||||
import {svgJar} from './svg-jar';
|
||||
|
||||
export function subscribersSortIcon([order, currentOrder, direction]) {
|
||||
if (order === currentOrder) {
|
||||
if (direction === 'asc') {
|
||||
return svgJar('arrow-up', {class: 'ih2 mr2'});
|
||||
} else {
|
||||
return svgJar('arrow-down', {class: 'ih2 mr2'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default helper(subscribersSortIcon);
|
@ -1,118 +0,0 @@
|
||||
import Mixin from '@ember/object/mixin';
|
||||
import RSVP from 'rsvp';
|
||||
// import { assign } from '@ember/polyfills';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
// let defaultPaginationSettings = {
|
||||
// page: 1,
|
||||
// limit: 15
|
||||
// };
|
||||
|
||||
// NOTE: this is DEPRECATED and now _only_ used by the subscribers route.
|
||||
// DO NOT USE - it will disappear soon
|
||||
|
||||
export default Mixin.create({
|
||||
notifications: service(),
|
||||
|
||||
paginationModel: null,
|
||||
paginationSettings: null,
|
||||
|
||||
// add a hook so that routes/controllers can do something with the meta data
|
||||
paginationMeta: computed({
|
||||
get() {
|
||||
return this._paginationMeta;
|
||||
},
|
||||
set(key, value) {
|
||||
if (this.didReceivePaginationMeta) {
|
||||
this.didReceivePaginationMeta(value);
|
||||
}
|
||||
this._paginationMeta = value;
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
|
||||
init() {
|
||||
// NOTE: errors in Ember 3.0 because this.paginationSettings.isDescriptor
|
||||
// no longer exists as CPs will be available directly with no getter.
|
||||
// Commented out for now as this whole mixin will soon disappear
|
||||
//
|
||||
// don't merge defaults if paginationSettings is a CP
|
||||
// if (!this.paginationSettings.isDescriptor) {
|
||||
// let paginationSettings = this.get('paginationSettings');
|
||||
// let settings = assign({}, defaultPaginationSettings, paginationSettings);
|
||||
//
|
||||
// this.set('paginationSettings', settings);
|
||||
// }
|
||||
|
||||
this.set('paginationMeta', {});
|
||||
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
reportLoadError(error) {
|
||||
this.notifications.showAPIError(error, {key: 'pagination.load.failed'});
|
||||
},
|
||||
|
||||
loadFirstPage(transition) {
|
||||
let paginationSettings = this.paginationSettings;
|
||||
let modelName = this.paginationModel;
|
||||
|
||||
this.set('paginationSettings.page', 1);
|
||||
|
||||
this.set('isLoading', true);
|
||||
|
||||
return this.store.query(modelName, paginationSettings).then((results) => {
|
||||
this.set('paginationMeta', results.meta);
|
||||
return results;
|
||||
}).catch((error) => {
|
||||
// if we have a transition we're executing in a route hook so we
|
||||
// want to throw in order to trigger the global error handler
|
||||
if (transition) {
|
||||
throw error;
|
||||
} else {
|
||||
this.reportLoadError(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadFirstPage() {
|
||||
return this.loadFirstPage();
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the next paginated page of posts into the ember-data store. Will cause the posts list UI to update.
|
||||
* @return
|
||||
*/
|
||||
loadNextPage() {
|
||||
let store = this.store;
|
||||
let modelName = this.paginationModel;
|
||||
let metadata = this.paginationMeta;
|
||||
let nextPage = metadata.pagination && metadata.pagination.next;
|
||||
let paginationSettings = this.paginationSettings;
|
||||
|
||||
if (nextPage && !this.isLoading) {
|
||||
this.set('isLoading', true);
|
||||
this.set('paginationSettings.page', nextPage);
|
||||
|
||||
return store.query(modelName, paginationSettings).then((results) => {
|
||||
this.set('paginationMeta', results.meta);
|
||||
return results;
|
||||
}).catch((error) => {
|
||||
this.reportLoadError(error);
|
||||
}).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
} else {
|
||||
return RSVP.resolve([]);
|
||||
}
|
||||
},
|
||||
|
||||
resetPagination() {
|
||||
this.set('paginationSettings.page', 1);
|
||||
}
|
||||
}
|
||||
});
|
@ -26,25 +26,6 @@ export default AuthenticatedRoute.extend({
|
||||
|
||||
setupController(controller) {
|
||||
this._super(...arguments);
|
||||
controller.initializeTable();
|
||||
controller.send('loadFirstPage');
|
||||
},
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
this._super(...arguments);
|
||||
if (isExiting) {
|
||||
controller.set('order', 'created_at');
|
||||
controller.set('direction', 'desc');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addSubscriber(subscriber) {
|
||||
this.controller.send('addSubscriber', subscriber);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.controller.send('reset');
|
||||
}
|
||||
controller.fetchSubscribers.perform();
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
actions: {
|
||||
cancel() {
|
||||
this.transitionTo('subscribers');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -22,10 +22,7 @@ export default Route.extend({
|
||||
actions: {
|
||||
save() {
|
||||
let subscriber = this.controller.get('subscriber');
|
||||
return subscriber.save().then((saved) => {
|
||||
this.send('addSubscriber', saved);
|
||||
return saved;
|
||||
});
|
||||
return subscriber.save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
|
@ -1 +0,0 @@
|
||||
<button class="gh-btn gh-btn-link gh-btn-sm" {{action tableActions.delete row.content}}><span>{{svg-jar "trash"}}</span></button>
|
@ -1,28 +0,0 @@
|
||||
{{#light-table table scrollBuffer=100 as |t|}}
|
||||
{{t.head
|
||||
onColumnClick=(action sortByColumn)
|
||||
iconAscending="gh-icon-ascending"
|
||||
iconDescending="gh-icon-descending"}}
|
||||
|
||||
{{#t.body
|
||||
canSelect=false
|
||||
tableActions=(hash delete=delete)
|
||||
scrollBuffer=100
|
||||
onScrolledToBottom=(action 'onScrolledToBottom')
|
||||
as |body|
|
||||
}}
|
||||
{{#if isLoading}}
|
||||
{{#body.loader}}
|
||||
<div class="gh-loading-content" style="margin-top: 2em;">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{/body.loader}}
|
||||
{{else}}
|
||||
{{#if table.isEmpty}}
|
||||
{{#body.no-data}}
|
||||
No subscribers found.
|
||||
{{/body.no-data}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/t.body}}
|
||||
{{/light-table}}
|
@ -1,6 +1,15 @@
|
||||
<section class="gh-canvas">
|
||||
<header class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Subscribers <span style="font-weight:200;margin-left:10px;display:inline-block;" data-test-total-subscribers> ({{total}})</span></h2>
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
Subscribers
|
||||
<span style="font-weight:200;margin-left:10px;display:inline-block;" data-test-total-subscribers>
|
||||
{{#if this.fetchSubscribers.lastSuccessful}}
|
||||
({{this.filteredSubscribers.length}})
|
||||
{{else}}
|
||||
(Loading...)
|
||||
{{/if}}
|
||||
</span>
|
||||
</h2>
|
||||
<div class="view-actions">
|
||||
{{#link-to "subscribers.import" class="gh-btn gh-btn-hover-green mr2" data-test-link="import-csv"}}<span>Import CSV</span>{{/link-to}}
|
||||
<a href="#" {{action 'exportData'}} class="gh-btn gh-btn-hover-blue mr2"><span>Export CSV</span></a>
|
||||
@ -9,21 +18,83 @@
|
||||
</header>
|
||||
|
||||
<section class="view-container">
|
||||
{{gh-subscribers-table
|
||||
table=table
|
||||
isLoading=isLoading
|
||||
loadNextPage=(action 'loadNextPage')
|
||||
sortByColumn=(action 'sortByColumn')
|
||||
delete=(action 'deleteSubscriber')}}
|
||||
<div class="subscribers-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="gh-subscribers-table-email-cell">
|
||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "email" this.order this.direction)}}
|
||||
{{subscribers-sort-icon "email" this.order this.direction}}
|
||||
Email Address
|
||||
{{/link-to}}
|
||||
</th>
|
||||
<th class="gh-subscribers-table-date-cell">
|
||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "created_at" this.order this.direction)}}
|
||||
{{subscribers-sort-icon "created_at" this.order this.direction}}
|
||||
Subscription Date
|
||||
{{/link-to}}
|
||||
</th>
|
||||
<th class="gh-subscribers-table-status-cell">
|
||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "status" this.order this.direction)}}
|
||||
{{subscribers-sort-icon "status" this.order this.direction}}
|
||||
Status
|
||||
{{/link-to}}
|
||||
</th>
|
||||
<th class="gh-subscribers-table-delete-cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<table>
|
||||
<tbody>
|
||||
{{#if this.sortedSubscribers}}
|
||||
<VerticalCollection
|
||||
@items={{this.sortedSubscribers}}
|
||||
@key="id"
|
||||
@containerSelector=".gh-main"
|
||||
@estimateHeight=34
|
||||
@bufferSize=30
|
||||
as |subscriber|
|
||||
>
|
||||
<tr>
|
||||
<td class="gh-subscribers-table-email-cell">
|
||||
{{subscriber.email}}
|
||||
</td>
|
||||
<td class="gh-subscribers-table-date-cell">
|
||||
{{moment-format subscriber.createdAtUTC 'MMMM DD, YYYY'}}
|
||||
</td>
|
||||
<td class="gh-subscribers-table-status-cell">
|
||||
{{subscriber.status}}
|
||||
</td>
|
||||
<td class="gh-subscribers-table-delete-cell">
|
||||
<button class="gh-btn gh-btn-link gh-btn-sm" {{action "deleteSubscriber" subscriber}}><span>{{svg-jar "trash"}}</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
</VerticalCollection>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
{{#if this.fetchSubscribers.isRunning}}
|
||||
<div class="relative h50"><GhLoadingSpinner /></div>
|
||||
{{else}}
|
||||
{{!-- match height to delete button height for consistent spacing --}}
|
||||
<span class="dib" style="line-height: 33px">No subscribers found.</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if subscriberToDelete}}
|
||||
{{gh-fullscreen-modal "delete-subscriber"
|
||||
model=subscriberToDelete
|
||||
confirm=(action "confirmDeleteSubscriber")
|
||||
close=(action "cancelDeleteSubscriber")
|
||||
modifier="action wide"}}
|
||||
{{#if this.subscriberToDelete}}
|
||||
<GhFullscreenModal
|
||||
@modal="delete-subscriber"
|
||||
@model={{this.subscriberToDelete}}
|
||||
@confirm={{action "confirmDeleteSubscriber"}}
|
||||
@close={{action "cancelDeleteSubscriber"}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
||||
|
||||
{{outlet}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{gh-fullscreen-modal "import-subscribers"
|
||||
confirm=(route-action "reset")
|
||||
close=(route-action "cancel")
|
||||
close=(action "close")
|
||||
confirm=(action "fetchNewSubscribers")
|
||||
modifier="action wide"}}
|
||||
|
@ -13,8 +13,9 @@ export default function mockSubscribers(server) {
|
||||
return new Response(422, {}, {
|
||||
errors: [{
|
||||
type: 'ValidationError',
|
||||
message: 'Email already exists.',
|
||||
property: 'email'
|
||||
message: 'Validation error, cannot save subscriber.',
|
||||
context: 'Email address is already subscribed.',
|
||||
property: null
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
|
@ -76,7 +76,6 @@
|
||||
"ember-fetch": "6.5.1",
|
||||
"ember-in-viewport": "3.3.0",
|
||||
"ember-infinity": "1.4.4",
|
||||
"ember-light-table": "https://github.com/kevinansfield/ember-light-table#bump-ember-in-viewport",
|
||||
"ember-load": "0.0.17",
|
||||
"ember-load-initializers": "2.0.0",
|
||||
"ember-mocha": "0.14.0",
|
||||
|
@ -4,7 +4,7 @@ import {beforeEach, describe, it} from 'mocha';
|
||||
import {click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
|
||||
import {expect} from 'chai';
|
||||
import {fileUpload} from '../helpers/file-upload';
|
||||
import {findAllWithText, findWithText} from '../helpers/find';
|
||||
import {findAllWithText} from '../helpers/find';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {visit} from '../helpers/visit';
|
||||
|
||||
@ -76,11 +76,10 @@ describe('Acceptance: Subscribers', function () {
|
||||
expect(document.title, 'page title')
|
||||
.to.equal('Subscribers - Test Blog');
|
||||
|
||||
// it loads the first page
|
||||
// TODO: latest ember-in-viewport causes infinite scroll issues with
|
||||
// FF here where it loads two pages straight away so we need to check
|
||||
// if rows are greater than or equal to a single page
|
||||
expect(findAll('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows')
|
||||
// it loads subscribers
|
||||
// NOTE: we use vertical-collection for occlusion so the max number of rows can be less than the
|
||||
// number of subscribers that are loaded
|
||||
expect(findAll('.subscribers-table tbody tr').length, 'number of subscriber rows')
|
||||
.to.be.at.least(30);
|
||||
|
||||
// it shows the total number of subscribers
|
||||
@ -91,37 +90,6 @@ describe('Acceptance: Subscribers', function () {
|
||||
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.queryParams.order).to.equal('created_at desc');
|
||||
|
||||
let createdAtHeader = findWithText('.subscribers-table th', 'Subscription Date');
|
||||
expect(createdAtHeader, 'createdAt column is sorted')
|
||||
.to.have.class('is-sorted');
|
||||
expect(createdAtHeader.querySelectorAll('.gh-icon-descending'), 'createdAt column has descending icon')
|
||||
.to.exist;
|
||||
|
||||
// click the column to re-order
|
||||
await click(findWithText('th', 'Subscription Date'));
|
||||
|
||||
// it flips the directions and re-fetches
|
||||
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.queryParams.order).to.equal('created_at asc');
|
||||
|
||||
createdAtHeader = findWithText('.subscribers-table th', 'Subscription Date');
|
||||
expect(createdAtHeader.querySelector('.gh-icon-ascending'), 'createdAt column has ascending icon')
|
||||
.to.exist;
|
||||
|
||||
// TODO: scroll test disabled as ember-light-table doesn't calculate
|
||||
// the scroll trigger element's positioning against the scroll
|
||||
// container - https://github.com/offirgolan/ember-light-table/issues/201
|
||||
//
|
||||
// // scroll to the bottom of the table to simulate infinite scroll
|
||||
// await find('.subscribers-table').scrollTop(find('.subscribers-table .ember-light-table').height() - 50);
|
||||
//
|
||||
// // trigger infinite scroll
|
||||
// await triggerEvent('.subscribers-table tbody', 'scroll');
|
||||
//
|
||||
// // it loads the next page
|
||||
// expect(find('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows after infinite-scroll')
|
||||
// .to.equal(40);
|
||||
|
||||
// click the add subscriber button
|
||||
await click('[data-test-link="add-subscriber"]');
|
||||
|
||||
@ -146,7 +114,7 @@ describe('Acceptance: Subscribers', function () {
|
||||
.to.not.exist;
|
||||
|
||||
// the subscriber is added to the table
|
||||
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type'), 'first email in list after addition')
|
||||
expect(find('.subscribers-table tbody tr td'), 'first email in list after addition')
|
||||
.to.contain.text('test@example.com');
|
||||
|
||||
// the table is scrolled to the top
|
||||
@ -165,10 +133,10 @@ describe('Acceptance: Subscribers', function () {
|
||||
|
||||
// the validation error is displayed
|
||||
expect(find('[data-test-error="new-subscriber-email"]'), 'duplicate email validation')
|
||||
.to.have.trimmed.text('Email already exists.');
|
||||
.to.have.trimmed.text('Email address is already subscribed.');
|
||||
|
||||
// the subscriber is not added to the table
|
||||
expect(findAllWithText('.lt-cell', 'test@example.com').length, 'number of "test@example.com rows"')
|
||||
expect(findAllWithText('td.gh-subscribers-table-email-cell', 'test@example.com').length, 'number of "test@example.com rows"')
|
||||
.to.equal(1);
|
||||
|
||||
// the subscriber total is unchanged
|
||||
@ -198,7 +166,7 @@ describe('Acceptance: Subscribers', function () {
|
||||
.to.not.exist;
|
||||
|
||||
// the subscriber is removed from the table
|
||||
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type'), 'first email in list after addition')
|
||||
expect(find('.subscribers-table tbody td.gh-subscribers-table-email-cell'), 'first email in list after addition')
|
||||
.to.not.have.trimmed.text('test@example.com');
|
||||
|
||||
// the subscriber total is updated
|
||||
@ -235,19 +203,6 @@ describe('Acceptance: Subscribers', function () {
|
||||
// subscriber total is updated
|
||||
expect(find('[data-test-total-subscribers]'), 'subscribers total after import')
|
||||
.to.have.trimmed.text('(90)');
|
||||
|
||||
// TODO: re-enable once bug in ember-light-table that triggers second page load is fixed
|
||||
// table is reset
|
||||
// [lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
// expect(lastRequest.url, 'endpoint requested after import')
|
||||
// .to.match(/\/subscribers\/\?/);
|
||||
// expect(lastRequest.queryParams.page, 'page requested after import')
|
||||
// .to.equal('1');
|
||||
|
||||
// expect(find('.subscribers-table .lt-body .lt-row').length, 'number of rows in table after import')
|
||||
// .to.equal(30);
|
||||
|
||||
// close modal
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -768,7 +768,7 @@
|
||||
dependencies:
|
||||
"@glimmer/di" "^0.2.0"
|
||||
|
||||
"@html-next/vertical-collection@1.0.0-beta.13", "@html-next/vertical-collection@^1.0.0-beta.12":
|
||||
"@html-next/vertical-collection@1.0.0-beta.13":
|
||||
version "1.0.0-beta.13"
|
||||
resolved "https://registry.yarnpkg.com/@html-next/vertical-collection/-/vertical-collection-1.0.0-beta.13.tgz#0cd7fbe813fef8c7daea3c46a3d4167cbd8db682"
|
||||
integrity sha512-YFs+toYLFSCiZSv1kkb3IPvrcVJHVX6q7vkzP0qb2Rvd0s61YqgpPDqtZ8sVViip0Lu3kw+Hs9fysjpAFqJDzQ==
|
||||
@ -2401,7 +2401,7 @@ broccoli-funnel@2.0.2, "broccoli-funnel@^1.2.0 || ^2.0.0", broccoli-funnel@^2.0.
|
||||
symlink-or-copy "^1.0.0"
|
||||
walk-sync "^0.3.1"
|
||||
|
||||
broccoli-funnel@^1.0.1, broccoli-funnel@^1.0.2, broccoli-funnel@^1.0.9, broccoli-funnel@^1.1.0:
|
||||
broccoli-funnel@^1.0.1, broccoli-funnel@^1.0.9, broccoli-funnel@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.2.0.tgz#cddc3afc5ff1685a8023488fff74ce6fb5a51296"
|
||||
integrity sha1-zdw6/F/xaFqAI0iP/3TOb7WlEpY=
|
||||
@ -4173,7 +4173,7 @@ element-closest@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/element-closest/-/element-closest-2.0.2.tgz#72a740a107453382e28df9ce5dbb5a8df0f966ec"
|
||||
integrity sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw=
|
||||
|
||||
element-resize-detector@1.1.15, element-resize-detector@^1.1.4:
|
||||
element-resize-detector@1.1.15:
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.15.tgz#48eba1a2eaa26969a4c998d972171128c971d8d2"
|
||||
integrity sha512-16/5avDegXlUxytGgaumhjyQoM6hpp5j3+L79sYq5hlXfTNRy5WMMuTVWkZU3egp/CokCmTmvf18P3KeB57Iog==
|
||||
@ -4303,7 +4303,7 @@ ember-cli-babel@7.7.3, ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.0, ember-cli
|
||||
ensure-posix-path "^1.0.2"
|
||||
semver "^5.5.0"
|
||||
|
||||
ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.10.0, ember-cli-babel@^6.11.0, ember-cli-babel@^6.12.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.18.0, ember-cli-babel@^6.3.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.7.2, ember-cli-babel@^6.8.0, ember-cli-babel@^6.8.1, ember-cli-babel@^6.8.2:
|
||||
ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.12.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.18.0, ember-cli-babel@^6.3.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.7.2, ember-cli-babel@^6.8.1, ember-cli-babel@^6.8.2:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.18.0.tgz#3f6435fd275172edeff2b634ee7b29ce74318957"
|
||||
integrity sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA==
|
||||
@ -4601,14 +4601,6 @@ ember-cli-string-helpers@2.0.0:
|
||||
broccoli-funnel "^1.0.1"
|
||||
ember-cli-babel "^6.16.0"
|
||||
|
||||
ember-cli-string-helpers@^1.8.1:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-string-helpers/-/ember-cli-string-helpers-1.10.0.tgz#6ee6c18d15759acb0905aa0153fe9e031a382fa4"
|
||||
integrity sha512-z2eNT7BsTNSxp3qNrv7KAxjPwdLC1kIYCck9CERg0RM5vBGy2vK6ozZE3U6nWrtth1xO4PrYkgISwhSgN8NMeg==
|
||||
dependencies:
|
||||
broccoli-funnel "^1.0.1"
|
||||
ember-cli-babel "^6.6.0"
|
||||
|
||||
ember-cli-string-utils@^1.0.0, ember-cli-string-utils@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-string-utils/-/ember-cli-string-utils-1.1.0.tgz#39b677fc2805f55173735376fcef278eaa4452a1"
|
||||
@ -4774,14 +4766,7 @@ ember-compatibility-helpers@^1.0.0, ember-compatibility-helpers@^1.1.1, ember-co
|
||||
ember-cli-version-checker "^2.1.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
ember-component-inbound-actions@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-component-inbound-actions/-/ember-component-inbound-actions-1.3.1.tgz#875938d6ec14a4d62e59fe4d87fb2f6a410217eb"
|
||||
integrity sha512-UZVmhZrbkvLge/zIZWORWB+o5sNt+cyv+YVhOSU41KK71yxA0C2DOcmDZPpQgT3PdGcPtqIekaWZoOiOpKQOjA==
|
||||
dependencies:
|
||||
ember-cli-babel "^6.16.0"
|
||||
|
||||
ember-composable-helpers@2.3.1, ember-composable-helpers@^2.1.0:
|
||||
ember-composable-helpers@2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-2.3.1.tgz#db98ad8b55d053e2ac216b9da091c9e7a3b9f453"
|
||||
integrity sha512-Eltj5yt2CtHhBMrdsjKQTP1zFyfEXQ5/v85ObV2zh0eIJZa1t/gImHN+GIHHuJ+9xOrCUAy60/2TJZjadpoPBQ==
|
||||
@ -4871,17 +4856,6 @@ ember-drag-drop@0.4.8:
|
||||
dependencies:
|
||||
ember-cli-babel "^6.6.0"
|
||||
|
||||
ember-element-resize-detector@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-element-resize-detector/-/ember-element-resize-detector-0.4.0.tgz#e21affa62fc9df5c13f587a940611c05f725c1ef"
|
||||
integrity sha512-lRwxPQZjhvdBkGxvhZSvZrOOqdtwRjm7lF/e3CFWfKjcZzJv2afNFfQCEWFT1z753b+rqOk+V2f6JD1kfp7WJw==
|
||||
dependencies:
|
||||
broccoli-funnel "^1.0.2"
|
||||
broccoli-merge-trees "^1.1.1"
|
||||
element-resize-detector "^1.1.4"
|
||||
ember-cli-babel "^6.6.0"
|
||||
ember-cli-htmlbars "^2.0.1"
|
||||
|
||||
ember-exam@2.1.5:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ember-exam/-/ember-exam-2.1.5.tgz#de20cd2f4ec3376d0e0a2696139bb8e37aba5c4f"
|
||||
@ -4927,7 +4901,7 @@ ember-fetch@6.5.1, "ember-fetch@^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
|
||||
node-fetch "^2.3.0"
|
||||
whatwg-fetch "^3.0.0"
|
||||
|
||||
ember-get-config@^0.2.2, ember-get-config@^0.2.4:
|
||||
ember-get-config@^0.2.2:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.4.tgz#118492a2a03d73e46004ed777928942021fe1ecd"
|
||||
integrity sha1-EYSSoqA9c+RgBO13eSiUICH+Hs0=
|
||||
@ -4951,7 +4925,7 @@ ember-hash-helper-polyfill@^0.2.0:
|
||||
ember-cli-babel "^6.8.2"
|
||||
ember-cli-version-checker "^2.1.0"
|
||||
|
||||
ember-in-viewport@3.3.0, ember-in-viewport@^3.2.2, ember-in-viewport@~3.0.0, ember-in-viewport@~3.5.2:
|
||||
ember-in-viewport@3.3.0, ember-in-viewport@~3.0.0, ember-in-viewport@~3.5.2:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-in-viewport/-/ember-in-viewport-3.3.0.tgz#718a9df85c1ad7a16c804dc61ce540414cfad710"
|
||||
integrity sha512-m9ejkLrc1XiXwbzSiPOOe8P6lz0MV7NBWf/vc+/7b1iNGucwDCA0Hsv9xZS3S1s3m9KWTCIobT8htZx/dCeTrw==
|
||||
@ -4983,29 +4957,6 @@ ember-invoke-action@^1.5.0:
|
||||
dependencies:
|
||||
ember-cli-babel "^6.6.0"
|
||||
|
||||
ember-lifeline@^3.0.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-lifeline/-/ember-lifeline-3.1.1.tgz#a14936cfe840de4769a1800f8b0b1ab00770cc33"
|
||||
integrity sha512-UsteL1i2LFS4BNSMKOIAJzIJQNypNqLCoHz23zZ6hpDTWuHB1R3mnK1mane40VVqwU8aRPoohJvowRBKXwdh6Q==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.1.3"
|
||||
|
||||
"ember-light-table@https://github.com/kevinansfield/ember-light-table#bump-ember-in-viewport":
|
||||
version "1.13.2"
|
||||
uid a757399504755ad8dde5cb4196576201881b93d4
|
||||
resolved "https://github.com/kevinansfield/ember-light-table#a757399504755ad8dde5cb4196576201881b93d4"
|
||||
dependencies:
|
||||
"@html-next/vertical-collection" "^1.0.0-beta.12"
|
||||
ember-cli-babel "^6.11.0"
|
||||
ember-cli-htmlbars "^2.0.3"
|
||||
ember-cli-string-helpers "^1.8.1"
|
||||
ember-composable-helpers "^2.1.0"
|
||||
ember-get-config "^0.2.4"
|
||||
ember-in-viewport "^3.2.2"
|
||||
ember-scrollable "^0.5.0"
|
||||
ember-truth-helpers "^2.0.0"
|
||||
ember-wormhole "^0.5.4"
|
||||
|
||||
ember-load-initializers@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.0.0.tgz#d4b3108dd14edb0f9dc3735553cc96dadd8a80cb"
|
||||
@ -5171,17 +5122,6 @@ ember-router-generator@^1.2.3:
|
||||
dependencies:
|
||||
recast "^0.11.3"
|
||||
|
||||
ember-scrollable@^0.5.0:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ember-scrollable/-/ember-scrollable-0.5.2.tgz#829e0e437075e47c079b21c1e56545a9bcc30494"
|
||||
integrity sha512-hykRSdjwq00DcGwYip5AXhvQ9HCj8ZPbVRMjyuTR+HUkRYwBHPMGSU5IPLRI4j/eTJgxac8x2cHCKfYCgaEu/Q==
|
||||
dependencies:
|
||||
ember-cli-babel "^6.8.0"
|
||||
ember-cli-htmlbars "^2.0.1"
|
||||
ember-component-inbound-actions "^1.3.0"
|
||||
ember-element-resize-detector "~0.4.0"
|
||||
ember-lifeline "^3.0.1"
|
||||
|
||||
ember-simple-auth@1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/ember-simple-auth/-/ember-simple-auth-1.8.2.tgz#caf823117097c7baafcaa3eac92c8217092c5299"
|
||||
@ -5277,7 +5217,7 @@ ember-text-measurer@^0.5.0:
|
||||
dependencies:
|
||||
ember-cli-babel "^7.1.0"
|
||||
|
||||
ember-truth-helpers@2.1.0, ember-truth-helpers@^2.0.0, ember-truth-helpers@^2.1.0:
|
||||
ember-truth-helpers@2.1.0, ember-truth-helpers@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz#d4dab4eee7945aa2388126485977baeb33ca0798"
|
||||
integrity sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==
|
||||
@ -5302,14 +5242,6 @@ ember-weakmap@^3.0.0:
|
||||
debug "^3.1.0"
|
||||
ember-cli-babel "^6.6.0"
|
||||
|
||||
ember-wormhole@^0.5.4:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/ember-wormhole/-/ember-wormhole-0.5.5.tgz#db417ff748cb21e574cd5f233889897bc27096cb"
|
||||
integrity sha512-z8l3gpoKmRA2BnTwvnYRk4jKVcETKHpddsD6kpS+EJ4EfyugadFS3zUqBmRDuJhFbNP8BVBLXlbbATj+Rk1Kgg==
|
||||
dependencies:
|
||||
ember-cli-babel "^6.10.0"
|
||||
ember-cli-htmlbars "^2.0.1"
|
||||
|
||||
emberx-file-input@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/emberx-file-input/-/emberx-file-input-1.2.1.tgz#a9214a9b3278c270a4d60e58836d2c1b98bce0ab"
|
||||
@ -6440,7 +6372,6 @@ gonzales-pe@4.2.4:
|
||||
|
||||
"google-caja-bower@https://github.com/acburdine/google-caja-bower#ghost":
|
||||
version "6011.0.0"
|
||||
uid "275cb75249f038492094a499756a73719ae071fd"
|
||||
resolved "https://github.com/acburdine/google-caja-bower#275cb75249f038492094a499756a73719ae071fd"
|
||||
|
||||
got@^8.0.1:
|
||||
@ -7533,7 +7464,6 @@ just-extend@^4.0.2:
|
||||
|
||||
"keymaster@https://github.com/madrobby/keymaster.git":
|
||||
version "1.6.3"
|
||||
uid f8f43ddafad663b505dc0908e72853bcf8daea49
|
||||
resolved "https://github.com/madrobby/keymaster.git#f8f43ddafad663b505dc0908e72853bcf8daea49"
|
||||
|
||||
keyv@3.0.0:
|
||||
@ -10678,7 +10608,6 @@ simple-swizzle@^0.2.2:
|
||||
|
||||
"simplemde@https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost":
|
||||
version "1.11.2"
|
||||
uid "7afb50dcdd63d3a1f6ccad682b70acbd7bc52eb9"
|
||||
resolved "https://github.com/kevinansfield/simplemde-markdown-editor.git#7afb50dcdd63d3a1f6ccad682b70acbd7bc52eb9"
|
||||
|
||||
sinon@^7.1.1:
|
||||
|
Loading…
Reference in New Issue
Block a user