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 @@
+
+{{#if this.isPaidAvailable}}
+
+{{/if}}
+{{#if this.specificOptions}}
+
+ {{#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"