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"