Source: document-library-web/src/main/resources/META-INF/resources/document_library/js/categorization/EditCategories.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import 'asset-taglib/asset_categories_selector/AssetCategoriesSelector.es';

import 'clay-multi-select';

import 'clay-radio';

import 'frontend-js-web/liferay/compat/modal/Modal.es';
import {fetch} from 'frontend-js-web';
import Component from 'metal-component';
import Soy from 'metal-soy';
import {Config} from 'metal-state';

import templates from './EditCategories.soy';

/**
 * Handles the categories of the selected
 * fileEntries inside a modal.
 */
class EditCategories extends Component {
	/**
	 * @inheritDoc
	 */
	attached() {
		this._assetVocabularyCategories = new Map();

		this._bulkStatusComponent = Liferay.component(
			this.namespace + 'BulkStatus'
		);
	}

	/**
	 * Close the modal.
	 */
	close() {
		this.showModal = false;
	}

	/**
	 * @inheritDoc
	 */
	created() {
		this.append = true;
		this.dataSource = [];
		this.urlCategories = `/bulk/v1.0/sites/${
			this.groupIds[0]
		}/taxonomy-vocabularies/common`;

		this._feedbackErrorClass = 'form-feedback-item';
		this._requiredVocabularyErrorMarkupText =
			'<div class="' +
			this._feedbackErrorClass +
			'">' +
			Liferay.Language.get('this-field-is-required') +
			'</div>';
	}

	/**
	 * Open the modal and get the
	 * commont categories.
	 */
	open(fileEntries, selectAll, folderId) {
		this.fileEntries = fileEntries;
		this.selectAll = selectAll;
		this.folderId = folderId;
		this.showModal = true;

		this._getCommonCategories();
	}

	/**
	 * Checks if the vocabulary is empty or not.
	 *
	 * @param  {String} vocabularyId
	 * @return {Boolean} true if it has a category, false if is empty.
	 */
	_checkRequiredVocabulary(vocabularyId) {
		const inputNode = this._getVocabularyInputNode(vocabularyId);
		let valid = true;

		if (inputNode.value) {
			inputNode.parentElement.parentElement.classList.remove('has-error');
		} else {
			inputNode.parentElement.parentElement.classList.add('has-error');

			const feedbackErrorNode = inputNode.parentElement.querySelector(
				'.' + this._feedbackErrorClass
			);

			if (!feedbackErrorNode) {
				inputNode.parentElement.insertAdjacentHTML(
					'beforeend',
					this._requiredVocabularyErrorMarkupText
				);
			}

			valid = false;
		}

		return valid;
	}

	/**
	 * Checks if the vocabulary have errors
	 *
	 * @private
	 * @review
	 * @return {Boolean} true if it has a error, false if has not error.
	 */
	_checkErrors() {
		return !!this.element.querySelector('.has-error');
	}

	/**
	 * Creates the ajax request.
	 *
	 * @param {String} url Url of the request
	 * @param {Object} bodyData The body of the request
	 * @param {Function} callback Callback function
	 */
	_fetchCategoriesRequest(url, method, bodyData) {
		const init = {
			body: JSON.stringify(bodyData),
			headers: {
				'content-type': 'application/json'
			},
			method
		};

		return fetch(this.pathModule + url, init)
			.then(response => response.json())
			.catch(() => {
				this.close();
			});
	}

	/**
	 * Gets the common categories for the selected
	 * file entries and updates the state.
	 *
	 * @private
	 * @review
	 */
	_getCommonCategories() {
		this.loading = true;

		const selection = this._getSelection();

		Promise.all([
			this._fetchCategoriesRequest(this.urlCategories, 'POST', selection),
			this._fetchCategoriesRequest(this.urlSelection, 'POST', selection)
		]).then(([responseCategories, responseSelection]) => {
			if (responseCategories && responseSelection) {
				this.loading = false;
				this.description = this._getDescription(responseSelection.size);
				this.multiple = this.selectAll || this.fileEntries.length > 1;
				this.vocabularies = this._parseVocabularies(
					responseCategories.items || []
				);
			}
		});
	}

	_getDescription(size) {
		if (size === 1) {
			return Liferay.Language.get(
				'you-are-editing-the-categories-for-the-selected-item'
			);
		}

		return Liferay.Util.sub(
			Liferay.Language.get(
				'you-are-editing-the-common-categories-for-x-items.-select-edit-or-replace-current-categories'
			),
			size
		);
	}

	/**
	 * Get all the categoryIds selected for all
	 * the vocabularies.
	 *
	 * @return {List<Long>} List of categoryIds.
	 */
	_getFinalCategories() {
		let finalCategories = [];

		this._assetVocabularyCategories.forEach(category => {
			const categoryIds = category.map(item => item.value);
			finalCategories = finalCategories.concat(categoryIds);
		});

		return finalCategories;
	}

	_getSelection() {
		return {
			documentIds: this.fileEntries,
			selectionScope: {
				folderId: this.folderId,
				repositoryId: this.repositoryId,
				selectAll: this.selectAll
			}
		};
	}

	/**
	 * Gets the input where categories are saved for a vocabulary.
	 *
	 * @param  {String} vocabularyId [description]
	 * @return {DOMElement} input node.
	 */
	_getVocabularyInputNode(vocabularyId) {
		return document.getElementById(
			this.namespace + this.hiddenInput + vocabularyId
		);
	}

	_handleInputFocus(event) {
		const dataProvider = event.target.refs.autocomplete.refs.dataProvider;
		const modal = this.element.querySelector('.modal');

		if (modal && dataProvider && !modal.contains(dataProvider.element)) {
			modal.appendChild(dataProvider.element);
		}
	}

	/**
	 * Sync the input radio with the state
	 * @param {!Event} event
	 * @private
	 * @review
	 */
	_handleRadioChange(event) {
		this.append = event.target.value === 'add';
	}

	_handleSelectedItemsChange(event) {
		const vocabularyId = event.vocabularyId;

		this._assetVocabularyCategories.set(vocabularyId, event.selectedItems);

		if (this._requiredVocabularies.includes(parseInt(vocabularyId, 10))) {
			setTimeout(() => {
				this._checkRequiredVocabulary(vocabularyId);
			}, 0);
		}
	}

	/**
	 * Sends request to backend services
	 * to update the categories.
	 * @param {!Event} event
	 *
	 * @private
	 * @review
	 */
	_handleFormSubmit(event) {
		event.preventDefault();

		setTimeout(() => {
			if (this._checkErrors()) {
				return;
			}

			const finalCategories = this._getFinalCategories();

			let addedCategories = [];

			if (!this.append) {
				addedCategories = finalCategories;
			} else {
				addedCategories = finalCategories.filter(
					categoryId =>
						this.initialCategories.indexOf(categoryId) == -1
				);
			}

			const removedCategories = this.initialCategories.filter(
				category => finalCategories.indexOf(category) == -1
			);

			const instance = this;

			this._fetchCategoriesRequest(
				this.urlUpdateCategories,
				this.append ? 'PATCH' : 'PUT',
				{
					documentBulkSelection: this._getSelection(),
					taxonomyCategoryIdsToAdd: addedCategories,
					taxonomyCategoryIdsToRemove: removedCategories
				}
			).then(() => {
				instance.close();

				if (instance._bulkStatusComponent) {
					instance._bulkStatusComponent.startWatch();
				}
			});
		}, 250);
	}

	_parseVocabularies(vocabularies) {
		let initialCategories = [];
		const requiredVocabularies = [];
		const vocabulariesList = [];

		vocabularies.forEach(vocabulary => {
			const categories = this._parseCategories(
				vocabulary.taxonomyCategories || []
			);

			const categoryIds = categories.map(item => item.value);

			const obj = {
				id: vocabulary.taxonomyVocabularyId.toString(),
				required: vocabulary.required,
				selectedCategoryIds: categoryIds.join(','),
				selectedItems: categories,
				singleSelect: !vocabulary.multiValued,
				title: vocabulary.name
			};

			vocabulariesList.push(obj);

			if (vocabulary.required) {
				requiredVocabularies.push(vocabulary.taxonomyVocabularyId);
			}

			initialCategories = initialCategories.concat(categoryIds);
		});

		this.initialCategories = initialCategories;
		this._requiredVocabularies = requiredVocabularies;

		return vocabulariesList;
	}

	/**
	 * Transforms the categories list in the object needed
	 * for the ClayMultiSelect component.
	 *
	 * @param {List<Long, String>} categories
	 * @return {List<{label, value}>} new commonItems object list
	 */
	_parseCategories(categories) {
		const categoriesObjList = [];

		if (categories.length > 0) {
			categories.forEach(item => {
				const itemObj = {
					label: item.taxonomyCategoryName,
					value: item.taxonomyCategoryId
				};

				categoriesObjList.push(itemObj);
			});
		}

		return categoriesObjList;
	}
}

/**
 * State definition.
 * @ignore
 * @static
 * @type {!Object}
 */
EditCategories.STATE = {
	/**
	 * Description
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	description: Config.string(),

	/**
	 * List of selected file entries.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {List<String>}
	 */
	fileEntries: Config.array(),

	/**
	 * Folder Id
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	folderId: Config.string(),

	/**
	 * Group Ids.
	 *
	 * @type {List<String>}
	 */
	groupIds: Config.array().required(),

	/**
	 * Hidden input name
	 *
	 * @type {String}
	 */
	hiddenInput: Config.string()
		.value('assetCategoryIds_')
		.internal(),

	/**
	 * Original categoryIds
	 *
	 * @type {List<Long>}
	 */
	initialCategories: Config.array().internal(),

	/**
	 * Flag that indicate if loading icon must
	 * be shown.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {Boolean}
	 */
	loading: Config.bool()
		.value(false)
		.internal(),

	/**
	 * Flag that indicate if multiple
	 * file entries has been selected.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {Boolean}
	 */
	multiple: Config.bool().value(false),

	/**
	 * Portlet's namespace
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {string}
	 */
	namespace: Config.string().required(),

	/**
	 * PathModule
	 *
	 * @instance
	 * @memberof EditTags
	 * @review
	 * @type {String}
	 */
	pathModule: Config.string().required(),

	/**
	 * RepositoryId
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	repositoryId: Config.string().required(),

	/**
	 * Flag that indicate if "select all" checkbox
	 * is checked.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {Boolean}
	 */
	selectAll: Config.bool(),

	/**
	 * Url to the categories selector page
	 * @type {String}
	 */
	selectCategoriesUrl: Config.string().required(),

	/**
	 * Flag that indicate if the modal must
	 * be shown.
	 *
	 * @instance
	 * @memberof EditTags
	 * @review
	 * @type {Boolean}
	 */
	showModal: Config.bool()
		.value(false)
		.internal(),

	/**
	 * Path to images.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	spritemap: Config.string().required(),

	/**
	 * Url to backend service that provides
	 * the common categories info.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	urlCategories: Config.string(),

	/**
	 * Url to backend service that provides
	 * the selection description.
	 *
	 * @instance
	 * @memberof EditTags
	 * @review
	 * @type {String}
	 */
	urlSelection: Config.string().value('/bulk/v1.0/bulk-selection'),

	/**
	 * Url to backend service that updates
	 * the categories.
	 *
	 * @instance
	 * @memberof EditCategories
	 * @review
	 * @type {String}
	 */
	urlUpdateCategories: Config.string().value(
		'/bulk/v1.0/taxonomy-categories/batch'
	),

	/**
	 * List of vocabularies
	 *
	 * @type {Array}
	 */
	vocabularies: Config.array().value([])
};

// Register component

Soy.register(EditCategories, templates);

export default EditCategories;