0
Fork 0
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:
Kevin Ansfield 2021-05-21 18:22:01 +01:00 committed by GitHub
parent 77bd0ab6f1
commit 495e435daf
12 changed files with 302 additions and 19 deletions

View 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}}

View 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;
})
]);
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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}}

View file

@ -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');

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View 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;
}

View file

@ -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"
},

View file

@ -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"