TryGhost/Ghost

View on GitHub
ghost/admin/app/components/gh-member-label-input.js

Summary

Maintainability
A
2 hrs
Test Coverage
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';

export default class GhMemberLabelInput extends Component {
    @service store;

    get availableLabels() {
        return this._availableLabels.toArray().sort((labelA, labelB) => {
            return (labelA.name || '').localeCompare((labelB.name || ''), undefined, {ignorePunctuation: true});
        });
    }

    get availableLabelNames() {
        return this.availableLabels.map(label => label.name.toLowerCase());
    }

    constructor(...args) {
        super(...args);
        // perform a background query to fetch all users and set `availableLabels`
        // to a live-query that will be immediately populated with what's in the
        // store and be updated when the above query returns
        this.store.query('label', {limit: 'all'});
        this._availableLabels = this.store.peekAll('label');
    }

    get selectedLabels() {
        if (typeof this.args.labels === 'object') {
            if (this.args.labels?.length && typeof this.args.labels[0] === 'string') {
                return this.args.labels.map((d) => {
                    return this.availableLabels.find(label => label.slug === d);
                }) || [];
            }
            return this.args.labels || [];
        }
        return [];
    }

    willDestroy() {
        super.willDestroy?.(...arguments);
        this._availableLabels.forEach((label) => {
            if (label.get('isNew')) {
                this.store.deleteRecord(label);
            }
        });
    }

    @action
    hideCreateOptionOnMatchingLabel(term) {
        return !this.availableLabelNames.includes(term.toLowerCase());
    }

    @action
    updateLabels(newLabels) {
        let currentLabels = this.selectedLabels;

        // destroy new+unsaved labels that are no longer selected
        currentLabels.forEach(function (label) {
            if (!newLabels.includes(label) && label.get('isNew')) {
                label.destroyRecord();
            }
        });

        // update labels
        this.args.onChange(newLabels);
    }

    @action
    editLabel(label, event) {
        event.stopPropagation();
        this.args.onLabelEdit?.(label.slug);
    }

    @action
    createLabel(labelName) {
        let currentLabels = this.selectedLabels;
        let currentLabelNames = currentLabels.map(label => label.get('name').toLowerCase());
        let labelToAdd;

        labelName = labelName.trim();

        // abort if label is already selected
        if (currentLabelNames.includes(labelName.toLowerCase())) {
            return;
        }

        // find existing label if there is one
        labelToAdd = this._findLabelByName(labelName);

        // create new label if no match
        if (!labelToAdd) {
            labelToAdd = this.store.createRecord('label', {
                name: labelName
            });
        }

        // push label onto member relationship
        currentLabels.pushObject(labelToAdd);
        this.args.onChange(currentLabels);
    }

    _findLabelByName(name) {
        let withMatchingName = function (label) {
            return label.name.toLowerCase() === name.toLowerCase();
        };
        return this.availableLabels.find(withMatchingName);
    }
}