Skip to content

Commit

Permalink
Added new members filters and refactored filters (#15667)
Browse files Browse the repository at this point in the history
fixes https://github.com/TryGhost/Team/issues/2112

- Removed a bit of duplicate code across templates and components that was used to handle filters
- Updated filter objects to contain information about the filter
- Added resource filters that are able to select a single resource, which can be used in columns
- Filters can now define columns by themselves. Not all columns already make use of this functionality, but we can move those over later (cleanup: https://github.com/TryGhost/Team/issues/2133)
- The filter definitions became quite long. We should move them to separate files in the future: https://github.com/TryGhost/Team/issues/2134
- Filters can now have custom NQL parsing
- Improved support for parsing recursive or grouped NQL queries
- Added support for filtering members by feedback
  • Loading branch information
SimonBackx authored Oct 21, 2022
1 parent 7d6cfc9 commit 7c82455
Show file tree
Hide file tree
Showing 16 changed files with 739 additions and 398 deletions.
25 changes: 15 additions & 10 deletions ghost/admin/app/components/gh-resource-select.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<GhTokenInput
<PowerSelect
@searchEnabled={{true}}
@options={{this.options}}
@selected={{this.selectedOptions}}
@disabled={{or @disabled this.fetchOptionsTask.isRunning}}
@optionsComponent={{component "power-select/options"}}
@allowCreation={{false}}
@renderInPlace={{this.renderInPlace}}
@selected={{this.selectedOption}}
@onChange={{this.onChange}}
@class="select-members"
@triggerClass="gh-resource-select-trigger"
@dropdownClass="gh-resource-select-dropdown"
@placeholder={{this.placeholderText}}
as |resource|
@renderInPlace={{this.renderInPlace}}
@disabled={{or @disabled this.fetchOptionsTask.isRunning}}
@optionsComponent={{component "power-select/options"}}
@search={{this.searchAndSuggest}}
@searchField={{this.searchField}}
@searchMessage={{@searchMessage}}
@searchPlaceholder={{this.searchPlaceholderText}}
as |resource|
>
{{resource.name}}
</GhTokenInput>
{{resource.title}}
</PowerSelect>
71 changes: 61 additions & 10 deletions ghost/admin/app/components/gh-resource-select.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {A} from '@ember/array';
import {action, get} from '@ember/object';
import {
defaultMatcher,
filterOptions
} from 'ember-power-select/utils/group-utils';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
Expand All @@ -13,6 +18,45 @@ export default class GhResourceSelect extends Component {
return this.args.renderInPlace === undefined ? false : this.args.renderInPlace;
}

get searchField() {
return this.args.searchField === undefined ? 'name' : this.args.searchField;
}

@action
searchAndSuggest(term, select) {
return this.searchAndSuggestTask.perform(term, select);
}

@task
*searchAndSuggestTask(term) {
let newOptions = this.flatOptions.toArray();

if (term.length === 0) {
return newOptions;
}

// todo: we can do actual filtering on posts here (allow searching when we have lots and lots of posts)
yield undefined;

newOptions = this._filter(A(newOptions), term);

return newOptions;
}

get matcher() {
return this.args.matcher || defaultMatcher;
}

_filter(options, searchText) {
let matcher;
if (this.searchField) {
matcher = (option, text) => this.matcher(get(option, this.searchField), text);
} else {
matcher = (option, text) => this.matcher(option, text);
}
return filterOptions(options || [], searchText, matcher);
}

constructor() {
super(...arguments);
this.fetchOptionsTask.perform();
Expand All @@ -38,9 +82,12 @@ export default class GhResourceSelect extends Component {
return options;
}

get selectedOptions() {
const resources = this.args.resources || [];
return this.flatOptions.filter(option => resources.find(resource => resource.id === option.id));
get selectedOption() {
if (this.args.resource.title) {
return this.args.resource;
}
const resource = this.args.resource ?? {};
return this.flatOptions.find(option => resource.id === option.id);
}

@action
Expand All @@ -55,16 +102,20 @@ export default class GhResourceSelect extends Component {
return 'Select a page/post';
}

get searchPlaceholderText() {
if (this.args.type === 'email') {
return 'Search emails';
}
return 'Search posts/pages';
}

@task
*fetchOptionsTask() {
const options = yield [];

if (this.args.type === 'email') {
const posts = yield this.store.query('post', {filter: '(status:published,status:sent)+newsletter_id:-null', limit: 'all'});
options.push({
groupName: 'Emails',
options: posts.map(mapResource)
});
options.push(...posts.map(mapResource));
this._options = options;
return;
}
Expand All @@ -74,8 +125,8 @@ export default class GhResourceSelect extends Component {

function mapResource(resource) {
return {
name: resource.title,
id: resource.id
id: resource.id,
title: resource.title
};
}

Expand Down
75 changes: 8 additions & 67 deletions ghost/admin/app/components/members/filter-value.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -47,47 +47,15 @@
<GhResourceSelect
@onChange={{fn this.setResourceFilterValue @filter}}
@type={{this.resourceFilterType}}
@resources={{this.resourceFilterValue}}
@resource={{this.resourceFilterValue}}
/>
</div>

{{else if (eq @filter.type 'subscribed')}}
{{else if (eq @filter.valueType 'options')}}
<span class="gh-select">
<OneWaySelect
@value={{@filter.value}}
@options={{this.availableFilterOptions.subscribed}}
@optionValuePath="name"
@optionLabelPath="label"
@optionTargetPath="name"
@update={{fn @setFilterValue @filter}}
data-test-select="members-filter-value"
/>
{{svg-jar "arrow-down-small"}}
</span>

{{else if (eq @filter.type 'last_seen_at')}}
<GhDatePicker
@value={{@filter.value}}
@maxDate={{now}}
@maxDateError="Must be in the past"
@onChange={{fn @setFilterValue @filter}}
data-test-input="members-filter-value"
/>

{{else if (eq @filter.type 'created_at')}}
<GhDatePicker
@value={{@filter.value}}
@maxDate={{now}}
@maxDateError="Must be in the past"
@onChange={{fn @setFilterValue @filter}}
data-test-input="members-filter-value"
/>

{{else if (eq @filter.type 'status')}}
<span class="gh-select">
<OneWaySelect
@value={{@filter.value}}
@options={{this.availableFilterOptions.status}}
@options={{@filter.options}}
@optionValuePath="name"
@optionLabelPath="label"
@optionTargetPath="name"
Expand Down Expand Up @@ -132,46 +100,19 @@
{{on "input" (fn this.setInputFilterValue @filter)}}
{{on "blur" (fn this.updateInputFilterValue @filter)}}
{{on "keypress" (fn this.updateInputFilterValueOnEnter @filter)}}
data-test-input="members-filter-value"
data-test-input="members-filter-value"
/>
</div>

{{else if (eq @filter.type 'subscriptions.plan_interval')}}
<span class="gh-select">
<OneWaySelect
@value={{@filter.value}}
@options={{this.availableFilterOptions.subscriptionPriceInterval}}
@optionValuePath="name"
@optionLabelPath="label"
@optionTargetPath="name"
@update={{fn @setFilterValue @filter}}
data-test-select="members-filter-value"
/>
{{svg-jar "arrow-down-small"}}
</span>

{{else if (eq @filter.type 'subscriptions.status')}}
<span class="gh-select">
<OneWaySelect
@value={{@filter.value}}
@options={{this.availableFilterOptions.subscriptionStripeStatus}}
@optionValuePath="name"
@optionLabelPath="label"
@optionTargetPath="name"
@update={{fn @setFilterValue @filter}}
data-test-select="members-filter-value"
/>
{{svg-jar "arrow-down-small"}}
</span>

{{else if (eq @filter.type 'subscriptions.start_date')}}
{{else if (or (eq @filter.type 'last_seen_at') (eq @filter.type 'created_at'))}}
<GhDatePicker
@value={{@filter.value}}
@maxDate={{now}}
@maxDateError="Must be in the past"
@onChange={{fn @setFilterValue @filter}}
data-test-input="members-filter-value"
/>

{{else if (eq @filter.type 'subscriptions.current_period_end')}}
{{else if (eq @filter.valueType 'date')}}
<GhDatePicker
@value={{@filter.value}}
@onChange={{fn @setFilterValue @filter}}
Expand Down
51 changes: 10 additions & 41 deletions ghost/admin/app/components/members/filter-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,11 @@ import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';

const FILTER_OPTIONS = {
subscriptionPriceInterval: [
{label: 'Monthly', name: 'month'},
{label: 'Yearly', name: 'year'}
],
status: [
{label: 'Paid', name: 'paid'},
{label: 'Free', name: 'free'},
{label: 'Complimentary', name: 'comped'}
],
subscribed: [
{label: 'Subscribed', name: 'true'},
{label: 'Unsubscribed', name: 'false'}
],
subscriptionStripeStatus: [
{label: 'Active', name: 'active'},
{label: 'Trialing', name: 'trialing'},
{label: 'Canceled', name: 'canceled'},
{label: 'Unpaid', name: 'unpaid'},
{label: 'Past Due', name: 'past_due'},
{label: 'Incomplete', name: 'incomplete'},
{label: 'Incomplete - Expired', name: 'incomplete_expired'}
]
};

export default class MembersFilterValue extends Component {
@tracked filterValue;

constructor(...args) {
super(...args);
this.availableFilterOptions = FILTER_OPTIONS;
this.filterValue = this.args.filter.value;
}

Expand Down Expand Up @@ -80,35 +54,30 @@ export default class MembersFilterValue extends Component {
}

get isResourceFilter() {
return ['signup', 'conversion', 'emails.post_id', 'opened_emails.post_id', 'clicked_links.post_id'].includes(this.args.filter?.type);
return !!this.args.filter?.isResourceFilter;
}

get resourceFilterType() {
if (!this.isResourceFilter) {
return '';
}

if (['emails.post_id', 'opened_emails.post_id', 'clicked_links.post_id'].includes(this.args.filter?.type)) {
return 'email';
}

return '';
return this.args.filter?.properties?.resource ?? '';
}

get resourceFilterValue() {
if (!this.isResourceFilter) {
return [];
return {};
}
const resources = this.args.filter?.value || [];
return resources.map((resource) => {
return {
id: resource
};
});
const resource = this.args.filter?.resource || undefined;
const resourceId = this.args.filter?.value || undefined;
return resource ?? {
id: resourceId
};
}

@action
setResourceFilterValue(filter, resources) {
this.args.setFilterValue(filter, resources.map(resource => resource.id));
setResourceFilterValue(filter, resource) {
this.args.setResourceValue(filter, resource);
}
}
3 changes: 2 additions & 1 deletion ghost/admin/app/components/members/filter.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
@index={{index}}
@filter={{filter}}
@setFilterValue={{this.setFilterValue}}
@setResourceValue={{this.setResourceValue}}
@onLabelEdit={{@onLabelEdit}}
/>
<button
Expand Down Expand Up @@ -84,7 +85,7 @@
</button>
<button
class="gh-btn gh-btn-primary"
data-test-button="members-apply-filter" type="button" {{on "click" this.applyFilter}} {{on "keyup" this.handleSubmitKeyup}}
data-test-button="members-apply-filter" type="button" {{on "click" (fn this.applyFiltersPressed dd)}} {{on "keyup" this.handleSubmitKeyup}}
>
<span>Apply filters</span>
</button>
Expand Down
Loading

0 comments on commit 7c82455

Please sign in to comment.