From 495e435dafd9153d2b9a02319b585f34b26c17de Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 21 May 2021 18:22:01 +0100 Subject: [PATCH] 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 `` 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 `` Co-authored-by: Sanne de Vries --- .../gh-members-recipient-select.hbs | 69 +++++++ .../components/gh-members-recipient-select.js | 168 ++++++++++++++++++ .../app/components/gh-publishmenu-draft.hbs | 9 +- .../components/gh-publishmenu-scheduled.hbs | 6 +- ghost/admin/app/components/gh-publishmenu.hbs | 4 +- ghost/admin/app/services/members-utils.js | 4 + .../app/styles/components/power-select.css | 4 + .../app/styles/components/publishmenu.css | 2 +- ghost/admin/app/styles/patterns/forms.css | 14 ++ .../app/utils/flatten-grouped-options.js | 15 ++ ghost/admin/package.json | 1 + ghost/admin/yarn.lock | 25 ++- 12 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 ghost/admin/app/components/gh-members-recipient-select.hbs create mode 100644 ghost/admin/app/components/gh-members-recipient-select.js create mode 100644 ghost/admin/app/utils/flatten-grouped-options.js diff --git a/ghost/admin/app/components/gh-members-recipient-select.hbs b/ghost/admin/app/components/gh-members-recipient-select.hbs new file mode 100644 index 0000000000..12f9fa1b7a --- /dev/null +++ b/ghost/admin/app/components/gh-members-recipient-select.hbs @@ -0,0 +1,69 @@ +
+

Free members {{this.freeMemberCountLabel}}

+
+ +
+
+{{#if this.isPaidAvailable}} +
+

Paid members {{this.paidMemberCountLabel}}

+
+ +
+
+{{/if}} +{{#if this.specificOptions}} +
+

Specific people

+
+ +
+
+ {{#if this.isSpecificChecked}} + + {{option.name}} + + {{/if}} +{{/if}} \ No newline at end of file diff --git a/ghost/admin/app/components/gh-members-recipient-select.js b/ghost/admin/app/components/gh-members-recipient-select.js new file mode 100644 index 0000000000..638cd2ef55 --- /dev/null +++ b/ghost/admin/app/components/gh-members-recipient-select.js @@ -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; + }) + ]); + } +} diff --git a/ghost/admin/app/components/gh-publishmenu-draft.hbs b/ghost/admin/app/components/gh-publishmenu-draft.hbs index 4923e0b799..8f062fa5db 100644 --- a/ghost/admin/app/components/gh-publishmenu-draft.hbs +++ b/ghost/admin/app/components/gh-publishmenu-draft.hbs @@ -36,15 +36,12 @@

{{html-safe this.sendingEmailLimitError}}

{{else}}
- +
-
diff --git a/ghost/admin/app/components/gh-publishmenu-scheduled.hbs b/ghost/admin/app/components/gh-publishmenu-scheduled.hbs index f9463fa108..db3a951da3 100644 --- a/ghost/admin/app/components/gh-publishmenu-scheduled.hbs +++ b/ghost/admin/app/components/gh-publishmenu-scheduled.hbs @@ -38,11 +38,9 @@
-
diff --git a/ghost/admin/app/components/gh-publishmenu.hbs b/ghost/admin/app/components/gh-publishmenu.hbs index e14b4954e3..575416478e 100644 --- a/ghost/admin/app/components/gh-publishmenu.hbs +++ b/ghost/admin/app/components/gh-publishmenu.hbs @@ -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}} diff --git a/ghost/admin/app/services/members-utils.js b/ghost/admin/app/services/members-utils.js index a8b2734bfb..f86d0270e3 100644 --- a/ghost/admin/app/services/members-utils.js +++ b/ghost/admin/app/services/members-utils.js @@ -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'); diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index 1531d63fc1..185d574128 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -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; diff --git a/ghost/admin/app/styles/components/publishmenu.css b/ghost/admin/app/styles/components/publishmenu.css index bc95680582..3f91e8af84 100644 --- a/ghost/admin/app/styles/components/publishmenu.css +++ b/ghost/admin/app/styles/components/publishmenu.css @@ -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 { diff --git a/ghost/admin/app/styles/patterns/forms.css b/ghost/admin/app/styles/patterns/forms.css index df6fa04ec1..b37e70ceaa 100644 --- a/ghost/admin/app/styles/patterns/forms.css +++ b/ghost/admin/app/styles/patterns/forms.css @@ -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; diff --git a/ghost/admin/app/utils/flatten-grouped-options.js b/ghost/admin/app/utils/flatten-grouped-options.js new file mode 100644 index 0000000000..578951da69 --- /dev/null +++ b/ghost/admin/app/utils/flatten-grouped-options.js @@ -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; +} diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 20fa43ddd0..8bc892b45e 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -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" }, diff --git a/ghost/admin/yarn.lock b/ghost/admin/yarn.lock index f27ee603af..bc830e6a6a 100644 --- a/ghost/admin/yarn.lock +++ b/ghost/admin/yarn.lock @@ -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"