mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Brought checkboxes back to publish menu recipient selection (#1972)
no issue Free and Paid are by far the two most common options for email recipients so it makes more sense to have them as very clear options which we felt was not the case with the single token/segment select. - created a new `<GhMembersRecipientSelect>` component that has individual checkboxes for free/paid/segment and when segment is selected an additional token input for specific labels - updated draft and scheduled publish menu components to use the `<GhMembersRecipientSelect>` Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
parent
77bd0ab6f1
commit
495e435daf
12 changed files with 302 additions and 19 deletions
69
ghost/admin/app/components/gh-members-recipient-select.hbs
Normal file
69
ghost/admin/app/components/gh-members-recipient-select.hbs
Normal file
|
@ -0,0 +1,69 @@
|
|||
<div class="gh-publishmenu-send-to-option">
|
||||
<p>Free members <span class="gh-publishmenu-emailcount">{{this.freeMemberCountLabel}}</span></p>
|
||||
<div class="for-switch x-small" {{on "click" (fn this.toggleFilter "status:free")}}>
|
||||
<label class="switch" for="send-email-to-free">
|
||||
<input
|
||||
id="send-email-to-free"
|
||||
type="checkbox"
|
||||
class="gh-input post-settings-featured"
|
||||
checked={{this.isFreeChecked}}
|
||||
disabled={{@disabled}}
|
||||
data-test-checkbox="free-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.isPaidAvailable}}
|
||||
<div class="gh-publishmenu-send-to-option">
|
||||
<p>Paid members <span class="gh-publishmenu-emailcount">{{this.paidMemberCountLabel}}</span></p>
|
||||
<div class="for-switch x-small" {{on "click" (fn this.toggleFilter "status:-free")}}>
|
||||
<label class="switch" for="send-email-to-paid">
|
||||
<input
|
||||
id="send-email-to-paid"
|
||||
type="checkbox"
|
||||
class="gh-input post-settings-featured"
|
||||
checked={{this.isPaidChecked}}
|
||||
disabled={{@disabled}}
|
||||
data-test-checkbox="paid-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.specificOptions}}
|
||||
<div class="gh-publishmenu-send-to-option">
|
||||
<p>Specific people</p>
|
||||
<div class="for-switch x-small" {{on "click" this.toggleSpecificFilter}}>
|
||||
<label class="switch" for="send-email-to-paid">
|
||||
<input
|
||||
id="send-email-to-paid"
|
||||
type="checkbox"
|
||||
class="gh-input post-settings-featured"
|
||||
checked={{this.isSpecificChecked}}
|
||||
disabled={{@disabled}}
|
||||
{{on "click" this.toggleSpecificFilter}}
|
||||
data-test-checkbox="paid-members"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.isSpecificChecked}}
|
||||
<GhTokenInput
|
||||
@class="select-members"
|
||||
@options={{this.specificOptions}}
|
||||
@selected={{this.selectedSpecificOptions}}
|
||||
@disabled={{@disabled}}
|
||||
@searchMessage="All labels selected"
|
||||
@optionsComponent="power-select/options"
|
||||
@allowCreation={{false}}
|
||||
@renderInPlace={{true}}
|
||||
@onChange={{this.selectSpecificOptions}}
|
||||
as |option|
|
||||
>
|
||||
{{option.name}}
|
||||
</GhTokenInput>
|
||||
{{/if}}
|
||||
{{/if}}
|
168
ghost/admin/app/components/gh-members-recipient-select.js
Normal file
168
ghost/admin/app/components/gh-members-recipient-select.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
import Component from '@glimmer/component';
|
||||
import flattenGroupedOptions from 'ghost-admin/utils/flatten-grouped-options';
|
||||
import {Promise} from 'rsvp';
|
||||
import {TrackedSet} from 'tracked-built-ins';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const BASE_FILTERS = ['status:free', 'status:-free'];
|
||||
|
||||
export default class GhMembersRecipientSelect extends Component {
|
||||
@service membersUtils;
|
||||
@service session;
|
||||
@service store;
|
||||
|
||||
baseFilters = new TrackedSet();
|
||||
specificFilters = new TrackedSet();
|
||||
|
||||
@tracked isSpecificChecked = false;
|
||||
@tracked specificOptions = [];
|
||||
@tracked freeMemberCount;
|
||||
@tracked paidMemberCount;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.fetchSpecificOptionsTask.perform();
|
||||
this.fetchMemberCountsTask.perform();
|
||||
|
||||
this.baseFilters.clear();
|
||||
this.specificFilters.clear();
|
||||
|
||||
(this.args.filter || '').split(',').forEach((filter) => {
|
||||
if (filter?.trim()) {
|
||||
if (BASE_FILTERS.includes(filter)) {
|
||||
this.baseFilters.add(filter);
|
||||
} else {
|
||||
this.isSpecificChecked = true;
|
||||
this.specificFilters.add(filter);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isPaidAvailable() {
|
||||
return this.membersUtils.isStripeEnabled;
|
||||
}
|
||||
|
||||
get isFreeChecked() {
|
||||
return this.baseFilters.has('status:free');
|
||||
}
|
||||
|
||||
get isPaidChecked() {
|
||||
return this.baseFilters.has('status:-free');
|
||||
}
|
||||
|
||||
get selectedSpecificOptions() {
|
||||
return flattenGroupedOptions(this.specificOptions)
|
||||
.filter(o => this.specificFilters.has(o.segment));
|
||||
}
|
||||
|
||||
get freeMemberCountLabel() {
|
||||
if (this.freeMemberCount !== undefined) {
|
||||
return `(${this.freeMemberCount})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get paidMemberCountLabel() {
|
||||
if (this.paidMemberCount !== undefined) {
|
||||
return `(${this.paidMemberCount})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get filterString() {
|
||||
const selectedFilters = !this.isSpecificChecked ?
|
||||
new Set([...this.baseFilters.values()]) :
|
||||
new Set([...this.baseFilters.values(), ...this.specificFilters.values()]);
|
||||
|
||||
if (!this.isPaidAvailable) {
|
||||
selectedFilters.delete('status:-free');
|
||||
}
|
||||
|
||||
return Array.from(selectedFilters).join(',') || null;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleFilter(filter, event) {
|
||||
event?.preventDefault();
|
||||
if (this.args.disabled) {
|
||||
return;
|
||||
}
|
||||
this.baseFilters.has(filter) ? this.baseFilters.delete(filter) : this.baseFilters.add(filter);
|
||||
this.args.onChange?.(this.filterString);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSpecificFilter(event) {
|
||||
event?.preventDefault();
|
||||
if (this.args.disabled) {
|
||||
return;
|
||||
}
|
||||
this.isSpecificChecked = !this.isSpecificChecked;
|
||||
this.args.onChange?.(this.filterString);
|
||||
}
|
||||
|
||||
@action
|
||||
selectSpecificOptions(selectedOptions) {
|
||||
if (this.args.disabled) {
|
||||
return;
|
||||
}
|
||||
this.specificFilters.clear();
|
||||
selectedOptions.forEach(o => this.specificFilters.add(o.segment));
|
||||
|
||||
if (this.isSpecificChecked) {
|
||||
this.args.onChange?.(this.filterString);
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
*fetchSpecificOptionsTask() {
|
||||
const options = [];
|
||||
|
||||
// fetch all labels w̶i̶t̶h̶ c̶o̶u̶n̶t̶s̶
|
||||
// TODO: add `include: 'count.members` to query once API is fixed
|
||||
const labels = yield this.store.query('label', {limit: 'all'});
|
||||
|
||||
if (labels.length > 0) {
|
||||
const labelsGroup = {
|
||||
groupName: 'Labels',
|
||||
options: []
|
||||
};
|
||||
|
||||
labels.forEach((label) => {
|
||||
labelsGroup.options.push({
|
||||
name: label.name,
|
||||
segment: `label:${label.slug}`,
|
||||
count: label.count?.members,
|
||||
class: 'segment-label'
|
||||
});
|
||||
});
|
||||
|
||||
options.push(labelsGroup);
|
||||
}
|
||||
|
||||
this.specificOptions = options;
|
||||
}
|
||||
|
||||
@task
|
||||
*fetchMemberCountsTask() {
|
||||
const user = yield this.session.user;
|
||||
|
||||
if (!user.isOwnerOrAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield Promise.all([
|
||||
this.store.query('member', {filter: 'status:free', limit: 1}).then((res) => {
|
||||
this.freeMemberCount = res.meta.pagination.total;
|
||||
}),
|
||||
this.store.query('member', {filter: 'status:-free', limit: 1}).then((res) => {
|
||||
this.paidMemberCount = res.meta.pagination.total;
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -36,15 +36,12 @@
|
|||
<p class="gh-box gh-box-alert">{{html-safe this.sendingEmailLimitError}}</p>
|
||||
{{else}}
|
||||
<div class="gh-publishmenu-email-label {{if this.disableEmailOption "pe-none"}}">
|
||||
<label class="gh-publishmenu-radio-label mb2 {{if this.disableEmailOption "midgrey"}}">Send by email to</label>
|
||||
<label class="gh-publishmenu-radio-label mb3 {{if this.disableEmailOption "midgrey"}}">Send by email to</label>
|
||||
|
||||
<div class="form-group">
|
||||
<GhMembersSegmentSelect
|
||||
@segment={{this.recipientsSegment}}
|
||||
<GhMembersRecipientSelect
|
||||
@filter={{this.recipientsFilter}}
|
||||
@onChange={{this.setSendEmailWhenPublished}}
|
||||
@onSegmentCountChange={{this.updateMemberCount}}
|
||||
@renderInPlace={{true}}
|
||||
@enforcedCountFilter="subscribed:true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -38,11 +38,9 @@
|
|||
<label class="gh-publishmenu-radio-label mb3 {{if this.disableEmailOption "midgrey"}}">Send by email to</label>
|
||||
|
||||
<div class="form-group">
|
||||
<GhMembersSegmentSelect
|
||||
@segment={{this.recipientsSegment}}
|
||||
<GhMembersRecipientSelect
|
||||
@filter={{this.recipientsFilter}}
|
||||
@disabled={{true}}
|
||||
@renderInPlace={{true}}
|
||||
@enforcedCountFilter="subscribed:true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
@saveType={{this.saveType}}
|
||||
@isClosing={{this.isClosing}}
|
||||
@canSendEmail={{this.canSendEmail}}
|
||||
@recipientsSegment={{this.sendEmailWhenPublished}}
|
||||
@recipientsFilter={{this.sendEmailWhenPublished}}
|
||||
@setSaveType={{action "setSaveType"}}
|
||||
@setTypedDateError={{action (mut this.typedDateError)}}
|
||||
@isSendingEmailLimited={{this.isSendingEmailLimited}}
|
||||
|
@ -29,7 +29,7 @@
|
|||
@setSaveType={{action "setSaveType"}}
|
||||
@setTypedDateError={{action (mut this.typedDateError)}}
|
||||
@canSendEmail={{this.canSendEmail}}
|
||||
@recipientsSegment={{this.sendEmailWhenPublished}}
|
||||
@recipientsFilter={{this.sendEmailWhenPublished}}
|
||||
@updateMemberCount={{action "updateMemberCount"}}
|
||||
@setSendEmailWhenPublished={{action "setSendEmailWhenPublished"}}
|
||||
@isSendingEmailLimited={{this.isSendingEmailLimited}}
|
||||
|
|
|
@ -4,6 +4,10 @@ export default class MembersUtilsService extends Service {
|
|||
@service config;
|
||||
@service settings;
|
||||
|
||||
get isMembersEnabled() {
|
||||
return this.settings.get('membersSignupAccess') !== 'none';
|
||||
}
|
||||
|
||||
get isStripeEnabled() {
|
||||
const stripeDirect = this.config.get('stripeDirect');
|
||||
|
||||
|
|
|
@ -137,6 +137,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ember-power-select-options .ember-power-select-group:first-child .ember-power-select-group-name {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.ember-power-select-group:first-of-type .ember-power-select-group-name {
|
||||
margin: 8px 0;
|
||||
padding-top: 0;
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gh-publishmenu-send-to-option p {
|
||||
|
|
|
@ -557,6 +557,20 @@ textarea {
|
|||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.for-switch.x-small .input-toggle-component {
|
||||
width: 34px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
.for-switch.x-small .input-toggle-component:before {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
}
|
||||
|
||||
.for-switch.x-small input:checked + .input-toggle-component:before {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
.for-switch.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
|
|
15
ghost/admin/app/utils/flatten-grouped-options.js
Normal file
15
ghost/admin/app/utils/flatten-grouped-options.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export default function flattenGroupedOptions(options) {
|
||||
const flatOptions = [];
|
||||
|
||||
function getOptions(option) {
|
||||
if (option.options) {
|
||||
return option.options.forEach(getOptions);
|
||||
}
|
||||
|
||||
flatOptions.push(option);
|
||||
}
|
||||
|
||||
options.forEach(getOptions);
|
||||
|
||||
return flatOptions;
|
||||
}
|
|
@ -134,6 +134,7 @@
|
|||
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost",
|
||||
"testem": "3.4.1",
|
||||
"top-gh-contribs": "2.0.4",
|
||||
"tracked-built-ins": "^1.1.1",
|
||||
"validator": "7.2.0",
|
||||
"walk-sync": "2.2.0"
|
||||
},
|
||||
|
|
|
@ -1533,7 +1533,7 @@
|
|||
handlebars "^4.0.13"
|
||||
simple-html-tokenizer "^0.5.8"
|
||||
|
||||
"@glimmer/tracking@^1.0.2", "@glimmer/tracking@^1.0.4":
|
||||
"@glimmer/tracking@^1.0.0", "@glimmer/tracking@^1.0.2", "@glimmer/tracking@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@glimmer/tracking/-/tracking-1.0.4.tgz#f1bc1412fe5e2236d0f8d502994a8f88af1bbb21"
|
||||
integrity sha512-F+oT8I55ba2puSGIzInmVrv/8QA2PcK1VD+GWgFMhF6WC97D+uZX7BFg+a3s/2N4FVBq5KHE+QxZzgazM151Yw==
|
||||
|
@ -5728,7 +5728,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0, em
|
|||
resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.1.tgz#5016b80cdef37036c4282eef2d863e1d73576879"
|
||||
integrity sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==
|
||||
|
||||
ember-cli-babel@7.26.6, ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.2, ember-cli-babel@^7.26.4, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.3:
|
||||
ember-cli-babel@7.26.6, ember-cli-babel@^7.0.0, ember-cli-babel@^7.1.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.22.1, ember-cli-babel@^7.23.0, ember-cli-babel@^7.23.1, ember-cli-babel@^7.26.2, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.4, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.3:
|
||||
version "7.26.6"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.6.tgz#322fbbd3baad9dd99e3276ff05bc6faef5e54b39"
|
||||
integrity sha512-040svtfj2RC35j/WMwdWJFusZaXmNoytLAMyBDGLMSlRvznudTxZjGlPV6UupmtTBApy58cEF8Fq4a+COWoEmQ==
|
||||
|
@ -6693,7 +6693,6 @@ ember-power-calendar@^0.16.3:
|
|||
|
||||
ember-power-datepicker@cibernox/ember-power-datepicker:
|
||||
version "0.8.1"
|
||||
uid da580474a2c449b715444934ddb626b7c07f46a7
|
||||
resolved "https://codeload.github.com/cibernox/ember-power-datepicker/tar.gz/da580474a2c449b715444934ddb626b7c07f46a7"
|
||||
dependencies:
|
||||
ember-basic-dropdown "^3.0.11"
|
||||
|
@ -8478,7 +8477,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:
|
||||
|
@ -9818,7 +9816,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:
|
||||
|
@ -13156,7 +13153,6 @@ simple-swizzle@^0.2.2:
|
|||
|
||||
"simplemde@https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost":
|
||||
version "1.11.2"
|
||||
uid "4c39702de7d97f9b32d5c101f39237b6dab7c3ee"
|
||||
resolved "https://github.com/kevinansfield/simplemde-markdown-editor.git#4c39702de7d97f9b32d5c101f39237b6dab7c3ee"
|
||||
|
||||
sinon@^9.0.0:
|
||||
|
@ -14119,6 +14115,23 @@ tr46@^2.0.2:
|
|||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tracked-built-ins@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tracked-built-ins/-/tracked-built-ins-1.1.1.tgz#d472142b268f2e03de719e33c0407b4c8b8ce5fa"
|
||||
integrity sha512-ZPGvTu+7d2tkUe4fJPgKkW8Bh512ZBih1S+DhuCSuT4VGj5qLwKbabSMqRiPSYOwWeM5aER0HMRGUvpWARPaHQ==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.26.3"
|
||||
ember-cli-typescript "^4.1.0"
|
||||
tracked-maps-and-sets "^2.0.0"
|
||||
|
||||
tracked-maps-and-sets@^2.0.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tracked-maps-and-sets/-/tracked-maps-and-sets-2.2.1.tgz#323dd40540c561e8b0ffdec8bf129c68ec5025f9"
|
||||
integrity sha512-XYrXh6L/GpGmVmG3KcN/qoDyi4FxHh8eZY/BA/RuoxynskV+GZSfwrX3R+5DR2CIkzkCx4zi4kkDRg1AMDfDhg==
|
||||
dependencies:
|
||||
"@glimmer/tracking" "^1.0.0"
|
||||
ember-cli-babel "^7.17.2"
|
||||
|
||||
tree-sync@^1.2.2:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.4.0.tgz#314598d13abaf752547d9335b8f95d9a137100d6"
|
||||
|
|
Loading…
Add table
Reference in a new issue