diff options
author | dizzy <diosmosis@users.noreply.github.com> | 2022-03-31 16:20:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-31 16:20:54 +0300 |
commit | 19489d103c6b4e91a745492e7ccffd957c3dcb4f (patch) | |
tree | 5bfcf0bbb6d2725876a01935f35500e7380e1595 | |
parent | 09e1412dcd0e142c760eb5447af254a817b7b056 (diff) |
[Vue] migrate segment generator directive to Vue (#18993)
* start migrating segment generator directive
* get to build
* remove some TODO
* rebuilt
* get UI tests to pass
* fix ng-model handling
* remote todo
* built vue files
15 files changed, 1781 insertions, 118 deletions
diff --git a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js index a13e37bf35..2f43775a4c 100644 --- a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js +++ b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js @@ -80,7 +80,7 @@ describe("PrivacyManager", function () { $(this).val(theVal).change(); }); }, value); - await page.waitForTimeout(100); + await page.waitForTimeout(200); } async function selectVisitColumn(title) diff --git a/plugins/SegmentEditor/SegmentEditor.php b/plugins/SegmentEditor/SegmentEditor.php index c22ba33bda..e3c9e16ea9 100644 --- a/plugins/SegmentEditor/SegmentEditor.php +++ b/plugins/SegmentEditor/SegmentEditor.php @@ -294,15 +294,12 @@ class SegmentEditor extends \Piwik\Plugin public function getJsFiles(&$jsFiles) { $jsFiles[] = "plugins/SegmentEditor/javascripts/Segmentation.js"; - $jsFiles[] = "plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator-model.js"; - $jsFiles[] = "plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.controller.js"; - $jsFiles[] = "plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.directive.js"; } public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = "plugins/SegmentEditor/stylesheets/segmentation.less"; - $stylesheets[] = "plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.directive.less"; + $stylesheets[] = "plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.less"; } /** diff --git a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.controller.js b/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.controller.js index b3bcca5e68..1726d44c38 100644 --- a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.controller.js +++ b/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.controller.js @@ -9,71 +9,8 @@ SegmentGeneratorController.$inject = ['$scope', 'piwik', 'piwikApi', 'segmentGeneratorModel', '$filter', '$timeout']; - var findAndExplodeByMatch = function(metric){ - var matches = ["==" , "!=" , "<=", ">=", "=@" , "!@","<",">", "=^", "=$"]; - var newMetric = {}; - var minPos = metric.length; - var match, index; - var singleChar = false; - - for (var key=0; key < matches.length; key++) { - match = matches[key]; - index = metric.indexOf(match); - if(index !== -1){ - if(index < minPos){ - minPos = index; - if(match.length === 1){ - singleChar = true; - } - } - } - } - - if (minPos < metric.length) { - // sth found - explode - if(singleChar == true){ - newMetric.segment = metric.substr(0,minPos); - newMetric.matches = metric.substr(minPos,1); - newMetric.value = decodeURIComponent(metric.substr(minPos+1)); - } else { - newMetric.segment = metric.substr(0,minPos); - newMetric.matches = metric.substr(minPos,2); - newMetric.value = decodeURIComponent(metric.substr(minPos+2)); - } - // if value is only "" -> change to empty string - if(newMetric.value === '""') - { - newMetric.value = ""; - } - } - - try { - // Decode again to deal with double-encoded segments in database - newMetric.value = decodeURIComponent(newMetric.value); - } catch (e) { - // Expected if the segment was not double-encoded - } - return newMetric; - }; - function generateUniqueId() { - var id = ''; - var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - - for (var i = 1; i <= 10; i++) { - id += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return id; - } - - function stripTags(text) { - if (text) { - text = ('' + text).replace(/(<([^>]+)>)/ig,""); - } - return text; - } function SegmentGeneratorController($scope, piwik, piwikApi, segmentGeneratorModel, $filter, $timeout) { var translate = $filter('translate'); @@ -149,13 +86,13 @@ orCondition.isLoading = true; this.updateSegmentDefinition(); - + var inputElement = $('.orCondId' + orCondition.id + " .metricValueBlock input"); inputElement.autocomplete({ source: [], minLength: 0 }); - + var resolved = false; var promise = piwikApi.fetch({ @@ -230,51 +167,6 @@ }; this.getSegmentString = function () { - var segmentStr = ''; - - angular.forEach(this.conditions, function (conditions) { - var subSegmentStr = ''; - - angular.forEach(conditions.orConditions, function (orCondition){ - if (subSegmentStr !== ''){ - subSegmentStr += ","; // OR operator - } - - // one encode for urldecode on value, one encode for urldecode on condition - subSegmentStr += orCondition.segment + orCondition.matches + encodeURIComponent(encodeURIComponent(orCondition.value)); - }); - - if (segmentStr !== '') { - segmentStr += ";"; // add AND operator between segment blocks - } - - segmentStr += subSegmentStr; - }); - - return segmentStr; - }; - - this.setSegmentString = function (segmentStr) { - var orCondition, condition; - - this.conditions = []; - - if (!segmentStr) { - return; - } - - var blocks = segmentStr.split(';'); - - for (var key = 0; key < blocks.length; key++) { - condition = {orConditions: []}; - this.addAndCondition(condition); - - blocks[key] = blocks[key].split(','); - for (var innerkey = 0; innerkey < blocks[key].length; innerkey++) { - orCondition = findAndExplodeByMatch(blocks[key][innerkey]); - this.addOrCondition(condition, orCondition); - } - } }; this.updateSegmentDefinition = function () { diff --git a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator-model.js b/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.model.js index 5db3eeab64..5db3eeab64 100644 --- a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator-model.js +++ b/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.model.js diff --git a/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js b/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js index cb183b952c..e5a05fcf4a 100644 --- a/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js +++ b/plugins/SegmentEditor/tests/UI/SegmentSelectorEditor_spec.js @@ -114,14 +114,16 @@ describe("SegmentSelectorEditorTest", function () { it("should save a new segment and add it to the segment list when the form is filled out and the save button is clicked", async function() { await page.evaluate(function () { - $('.metricValueBlock input').each(function (index) { - $(this).val('value ' + index).change(); + $('.metricValueBlock input').each(function (index, elem) { + $(elem).val('value ' + index).change(); }); }); await page.type('input.edit_segment_name', 'new segment'); await page.click('.segmentRow0 .segment-or'); // click somewhere else to save new name + await page.waitForTimeout(200); + await page.evaluate(function () { $('button.saveAndApply').click(); }); @@ -164,8 +166,10 @@ describe("SegmentSelectorEditorTest", function () { }); }); + await page.waitFor(200); + await page.evaluate(function () { - $('button.saveAndApply').click(); + $('button.saveAndApply').click(); }); await page.waitForSelector('.modal.open'); await page.waitForTimeout(500); // animation to show confirm @@ -289,6 +293,9 @@ describe("SegmentSelectorEditorTest", function () { var complexValue = 's#2&#--_*+?# #5"\'&<>.22,3'; $('.segmentRow1 .metricValueBlock input').val(complexValue).change(); }); + + await page.waitFor(200); + await page.evaluate(function () { $('button.saveAndApply').click(); }); @@ -331,6 +338,8 @@ describe("SegmentSelectorEditorTest", function () { console.log(dialog.message()); }); + await page.waitFor(200); + await page.evaluate(function () { $('button.saveAndApply').click(); }); diff --git a/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.js b/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.js new file mode 100644 index 0000000000..25b9c0f751 --- /dev/null +++ b/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.js @@ -0,0 +1,957 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", , "CorePluginsAdmin"], factory); + else if(typeof exports === 'object') + exports["SegmentEditor"] = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin")); + else + root["SegmentEditor"] = factory(root["CoreHome"], root["Vue"], root["CorePluginsAdmin"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__, __WEBPACK_EXTERNAL_MODULE_a5a2__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/SegmentEditor/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "a5a2": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE_a5a2__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "SegmentGeneratorStore", function() { return /* reexport */ SegmentGenerator_store; }); +__webpack_require__.d(__webpack_exports__, "SegmentGenerator", function() { return /* reexport */ SegmentGenerator; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue?vue&type=template&id=3e4a5f7a + +var _hoisted_1 = { + class: "segment-generator", + ref: "root" +}; +var _hoisted_2 = { + class: "segment-rows" +}; +var _hoisted_3 = { + class: "segment-row" +}; +var _hoisted_4 = ["onClick"]; +var _hoisted_5 = { + href: "#", + class: "segment-loading" +}; +var _hoisted_6 = { + class: "segment-row-inputs valign-wrapper" +}; +var _hoisted_7 = { + class: "segment-input metricListBlock valign-wrapper" +}; +var _hoisted_8 = { + style: { + "width": "100%" + } +}; +var _hoisted_9 = { + class: "segment-input metricMatchBlock valign-wrapper" +}; +var _hoisted_10 = { + style: { + "display": "inline-block" + } +}; +var _hoisted_11 = { + class: "segment-input metricValueBlock valign-wrapper" +}; +var _hoisted_12 = { + class: "form-group row", + style: { + "width": "100%" + } +}; +var _hoisted_13 = { + class: "input-field col s12" +}; + +var _hoisted_14 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + role: "status", + "aria-live": "polite", + class: "ui-helper-hidden-accessible" +}, null, -1); + +var _hoisted_15 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "clear" +}, null, -1); + +var _hoisted_16 = { + class: "segment-or" +}; +var _hoisted_17 = ["onClick"]; +var _hoisted_18 = ["innerHTML"]; +var _hoisted_19 = { + class: "segment-and" +}; +var _hoisted_20 = ["innerHTML"]; +function render(_ctx, _cache, $props, $setup, $data, $options) { + var _this = this; + + var _component_ActivityIndicator = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ActivityIndicator"); + + var _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field"); + + var _component_ValueInput = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ValueInput"); + + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { + loading: _ctx.isLoading + }, null, 8, ["loading"]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.conditions, function (condition, conditionIndex) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])("segmentRow".concat(conditionIndex)), + key: conditionIndex + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_2, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(condition.orConditions, function (orCondition, orConditionIndex) { + var _ctx$segments$orCondi, _ctx$segments$orCondi2; + + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])("orCondId".concat(orCondition.id)), + key: orConditionIndex + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "segment-close", + onClick: function onClick($event) { + return _ctx.removeOrCondition(condition, orCondition); + } + }, null, 8, _hoisted_4), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", _hoisted_5, null, 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.conditionValuesLoading[orCondition.id]]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_6, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_8, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, { + uicontrol: "expandable-select", + name: "segments", + "model-value": orCondition.segment, + "onUpdate:modelValue": function onUpdateModelValue($event) { + orCondition.segment = $event; + + _ctx.updateAutocomplete(orCondition); + + _ctx.computeSegmentDefinition(); + }, + title: (_ctx$segments$orCondi = _ctx.segments[orCondition.segment]) === null || _ctx$segments$orCondi === void 0 ? void 0 : _ctx$segments$orCondi.name, + "full-width": true, + options: _ctx.segmentList + }, null, 8, ["model-value", "onUpdate:modelValue", "title", "options"])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_9, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, { + uicontrol: "select", + name: "matchType", + "model-value": orCondition.matches, + "onUpdate:modelValue": function onUpdateModelValue($event) { + orCondition.matches = $event; + + _ctx.computeSegmentDefinition(); + }, + "full-width": true, + options: _ctx.matches[(_ctx$segments$orCondi2 = _ctx.segments[orCondition.segment]) === null || _ctx$segments$orCondi2 === void 0 ? void 0 : _ctx$segments$orCondi2.type] + }, null, 8, ["model-value", "onUpdate:modelValue", "options"])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_12, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_13, [_hoisted_14, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ValueInput, { + or: orCondition, + onUpdate: function onUpdate($event) { + orCondition.value = $event; // deep watch doesn't catch this change + + _this.computeSegmentDefinition(); + } + }, null, 8, ["or", "onUpdate"])])])]), _hoisted_15])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_16, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('SegmentEditor_OperatorOR')), 1)], 2); + }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "segment-add-or", + onClick: function onClick($event) { + return _ctx.addNewOrCondition(condition); + } + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + innerHTML: _ctx.$sanitize(_ctx.addNewOrConditionLinkText) + }, null, 8, _hoisted_18)])], 8, _hoisted_17)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_19, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('SegmentEditor_OperatorAND')), 1)], 2); + }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "segment-add-row initial", + onClick: _cache[0] || (_cache[0] = function ($event) { + return _ctx.addNewAndCondition(); + }) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + innerHTML: _ctx.$sanitize(_ctx.addNewAndConditionLinkText) + }, null, 8, _hoisted_20)])])], 512); +} +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue?vue&type=template&id=3e4a5f7a + +// EXTERNAL MODULE: external "CorePluginsAdmin" +var external_CorePluginsAdmin_ = __webpack_require__("a5a2"); + +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.store.ts +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + +var SegmentGenerator_store_SegmentGeneratorStore = /*#__PURE__*/function () { + function SegmentGeneratorStore() { + var _this = this; + + _classCallCheck(this, SegmentGeneratorStore); + + _defineProperty(this, "privateState", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["reactive"])({ + isLoading: false, + segments: [] + })); + + _defineProperty(this, "state", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(function () { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["readonly"])(_this.privateState); + })); + + _defineProperty(this, "loadSegmentsAbort", void 0); + + _defineProperty(this, "loadSegmentsPromise", void 0); + + _defineProperty(this, "fetchedSiteId", void 0); + } + + _createClass(SegmentGeneratorStore, [{ + key: "loadSegments", + value: function loadSegments(siteId, visitSegmentsOnly) { + var _this2 = this; + + if (this.loadSegmentsAbort) { + this.loadSegmentsAbort.abort(); + this.loadSegmentsAbort = undefined; + } + + this.privateState.isLoading = true; + + if (this.fetchedSiteId !== siteId) { + this.loadSegmentsAbort = undefined; + this.fetchedSiteId = siteId; + } + + if (!this.loadSegmentsPromise) { + var idSites = undefined; + var idSite = undefined; + + if (siteId === 'all' || !siteId) { + idSites = 'all'; + idSite = 'all'; + } else if (siteId) { + idSites = siteId; + idSite = siteId; + } + + this.loadSegmentsAbort = new AbortController(); + this.loadSegmentsPromise = external_CoreHome_["AjaxHelper"].fetch({ + method: 'API.getSegmentsMetadata', + filter_limit: '-1', + _hideImplementationData: 0, + idSites: idSites, + idSite: idSite + }); + } + + return this.loadSegmentsPromise.then(function (response) { + _this2.privateState.isLoading = false; + + if (response) { + if (visitSegmentsOnly) { + _this2.privateState.segments = response.filter(function (s) { + return s.sqlSegment && s.sqlSegment.match(/log_visit\./); + }); + } else { + _this2.privateState.segments = response; + } + } + + return _this2.state.value.segments; + }).finally(function () { + _this2.privateState.isLoading = false; + }); + } + }]); + + return SegmentGeneratorStore; +}(); + +/* harmony default export */ var SegmentGenerator_store = (new SegmentGenerator_store_SegmentGeneratorStore()); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue?vue&type=template&id=17b64c0b + +var ValueInputvue_type_template_id_17b64c0b_hoisted_1 = ["placeholder", "title", "value"]; +function ValueInputvue_type_template_id_17b64c0b_render(_ctx, _cache, $props, $setup, $data, $options) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("input", { + placeholder: _ctx.translate('General_Value'), + type: "text", + class: "autocomplete", + title: _ctx.translate('General_Value'), + autocomplete: "off", + value: _ctx.or.value, + onKeydown: _cache[0] || (_cache[0] = function ($event) { + return _ctx.onKeydownOrConditionValue($event); + }), + onChange: _cache[1] || (_cache[1] = function ($event) { + return _ctx.onKeydownOrConditionValue($event); + }) + }, null, 40, ValueInputvue_type_template_id_17b64c0b_hoisted_1); +} +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue?vue&type=template&id=17b64c0b + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--14-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--14-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue?vue&type=script&lang=ts + + +/* harmony default export */ var ValueInputvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + or: Object + }, + created: function created() { + this.onKeydownOrConditionValue = Object(external_CoreHome_["debounce"])(this.onKeydownOrConditionValue, 50); + }, + emits: ['update'], + methods: { + onKeydownOrConditionValue: function onKeydownOrConditionValue(event) { + this.$emit('update', event.target.value); + } + } +})); +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue + + + +ValueInputvue_type_script_lang_ts.render = ValueInputvue_type_template_id_17b64c0b_render + +/* harmony default export */ var ValueInput = (ValueInputvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--14-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--14-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue?vue&type=script&lang=ts + + + + + + +function initialMatches() { + return { + metric: [{ + key: '==', + value: Object(external_CoreHome_["translate"])('General_OperationEquals') + }, { + key: '!=', + value: Object(external_CoreHome_["translate"])('General_OperationNotEquals') + }, { + key: '<=', + value: Object(external_CoreHome_["translate"])('General_OperationAtMost') + }, { + key: '>=', + value: Object(external_CoreHome_["translate"])('General_OperationAtLeast') + }, { + key: '<', + value: Object(external_CoreHome_["translate"])('General_OperationLessThan') + }, { + key: '>', + value: Object(external_CoreHome_["translate"])('General_OperationGreaterThan') + }], + dimension: [{ + key: '==', + value: Object(external_CoreHome_["translate"])('General_OperationIs') + }, { + key: '!=', + value: Object(external_CoreHome_["translate"])('General_OperationIsNot') + }, { + key: '=@', + value: Object(external_CoreHome_["translate"])('General_OperationContains') + }, { + key: '!@', + value: Object(external_CoreHome_["translate"])('General_OperationDoesNotContain') + }, { + key: '=^', + value: Object(external_CoreHome_["translate"])('General_OperationStartsWith') + }, { + key: '=$', + value: Object(external_CoreHome_["translate"])('General_OperationEndsWith') + }] + }; +} + +function generateUniqueId() { + var id = ''; + var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for (var i = 1; i <= 10; i += 1) { + id += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return id; +} + +function findAndExplodeByMatch(metric) { + var matches = ['==', '!=', '<=', '>=', '=@', '!@', '<', '>', '=^', '=$']; + var newMetric = {}; + var minPos = metric.length; + var match; + var index; + var singleChar = false; + + for (var key = 0; key < matches.length; key += 1) { + match = matches[key]; + index = metric.indexOf(match); + + if (index !== -1) { + if (index < minPos) { + minPos = index; + + if (match.length === 1) { + singleChar = true; + } + } + } + } + + if (minPos < metric.length) { + // sth found - explode + if (singleChar === true) { + newMetric.segment = metric.substr(0, minPos); + newMetric.matches = metric.substr(minPos, 1); + newMetric.value = decodeURIComponent(metric.substr(minPos + 1)); + } else { + newMetric.segment = metric.substr(0, minPos); + newMetric.matches = metric.substr(minPos, 2); + newMetric.value = decodeURIComponent(metric.substr(minPos + 2)); + } // if value is only '' -> change to empty string + + + if (newMetric.value === '""') { + newMetric.value = ''; + } + } + + try { + // Decode again to deal with double-encoded segments in database + newMetric.value = decodeURIComponent(newMetric.value); + } catch (e) {// Expected if the segment was not double-encoded + } + + return newMetric; +} + +function stripTags(text) { + return text ? "".concat(text).replace(/(<([^>]+)>)/ig, '') : text; +} + +var _window = window, + $ = _window.$; +/* harmony default export */ var SegmentGeneratorvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + addInitialCondition: Boolean, + visitSegmentsOnly: Boolean, + idsite: [String, Number], + modelValue: { + type: String, + default: '' + } + }, + components: { + ActivityIndicator: external_CoreHome_["ActivityIndicator"], + Field: external_CorePluginsAdmin_["Field"], + ValueInput: ValueInput + }, + data: function data() { + return { + conditions: [], + queriedSegments: [], + matches: initialMatches(), + conditionValuesLoading: {}, + segmentDefinition: '' + }; + }, + emits: ['update:modelValue'], + watch: { + modelValue: function modelValue(newVal) { + if (newVal !== this.segmentDefinition) { + this.setSegmentString(newVal); + } + }, + conditions: { + deep: true, + handler: function handler() { + this.computeSegmentDefinition(); + } + }, + segmentDefinition: function segmentDefinition(newVal) { + if (newVal !== this.modelValue) { + this.$emit('update:modelValue', newVal); + } + }, + idsite: function idsite(newVal) { + this.reloadSegments(newVal, this.visitSegmentsOnly); + } + }, + created: function created() { + this.matches[''] = this.matches.dimension; + this.setSegmentString(this.modelValue); + this.segmentDefinition = this.modelValue; + this.reloadSegments(this.idsite, this.visitSegmentsOnly); + }, + methods: { + reloadSegments: function reloadSegments(idsite, visitSegmentsOnly) { + var _this = this; + + SegmentGenerator_store.loadSegments(idsite, visitSegmentsOnly).then(function (segments) { + _this.queriedSegments = segments.map(function (s) { + return Object.assign(Object.assign({}, s), {}, { + category: s.category || 'Others' + }); + }); + + if (_this.addInitialCondition && _this.conditions.length === 0) { + _this.addNewAndCondition(); + } + }); + }, + addAndCondition: function addAndCondition(condition) { + this.conditions.push(condition); + }, + addNewOrCondition: function addNewOrCondition(condition) { + var orCondition = { + segment: this.firstSegment, + matches: this.firstMatch, + value: '' + }; + this.addOrCondition(condition, orCondition); + }, + addOrCondition: function addOrCondition(condition, orCondition) { + var _this2 = this; + + this.conditionValuesLoading[orCondition.id] = false; + orCondition.id = generateUniqueId(); + condition.orConditions.push(orCondition); + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["nextTick"])(function () { + _this2.updateAutocomplete(orCondition); + }); + }, + updateAutocomplete: function updateAutocomplete(orCondition) { + var _this3 = this; + + this.conditionValuesLoading[orCondition.id] = true; + $(".orCondId".concat(orCondition.id, " .metricValueBlock input"), this.$refs.root).autocomplete({ + source: [], + minLength: 0 + }); + var abortController = new AbortController(); + var resolved = false; + external_CoreHome_["AjaxHelper"].fetch({ + module: 'API', + format: 'json', + method: 'API.getSuggestedValuesForSegment', + segmentName: orCondition.segment + }).then(function (response) { + _this3.conditionValuesLoading[orCondition.id] = false; + resolved = true; + var inputElement = $(".orCondId".concat(orCondition.id, " .metricValueBlock input")).autocomplete({ + source: response, + minLength: 0, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + select: function select(event, ui) { + event.preventDefault(); + orCondition.value = ui.item.value; + + _this3.computeSegmentDefinition(); // deep watch doesn't catch this change + + + _this3.$forceUpdate(); + } + }).off('click').click(function () { + $(inputElement).autocomplete('search', orCondition.value); + }); + }).catch(function () { + resolved = true; + _this3.conditionValuesLoading[orCondition.id] = false; + $(".orCondId".concat(orCondition.id, " .metricValueBlock input")).autocomplete({ + source: [], + minLength: 0 + }).autocomplete('search', orCondition.value); + }); + setTimeout(function () { + if (!resolved) { + abortController.abort(); + } + }, 20000); + }, + removeOrCondition: function removeOrCondition(condition, orCondition) { + var index = condition.orConditions.indexOf(orCondition); + + if (index > -1) { + condition.orConditions.splice(index, 1); + } + + if (condition.orConditions.length === 0) { + var andCondIndex = this.conditions.indexOf(condition); + + if (index > -1) { + this.conditions.splice(andCondIndex, 1); + } + } + }, + setSegmentString: function setSegmentString(segmentStr) { + var _this4 = this; + + this.conditions = []; + + if (!segmentStr) { + return; + } + + var blocks = segmentStr.split(';').map(function (b) { + return b.split(','); + }); + this.conditions = blocks.map(function (block) { + var condition = { + orConditions: [] + }; + block.forEach(function (innerBlock) { + var orCondition = findAndExplodeByMatch(innerBlock); + + _this4.addOrCondition(condition, orCondition); + }); + return condition; + }); + }, + addNewAndCondition: function addNewAndCondition() { + var condition = { + orConditions: [] + }; + this.addAndCondition(condition); + this.addNewOrCondition(condition); + return condition; + }, + // NOTE: can't use a computed property since we need to recompute on changes inside the + // structure. don't have to if we don't do in-place changes, but with nested structures, + // that's complicated. + computeSegmentDefinition: function computeSegmentDefinition() { + var segmentStr = ''; + this.conditions.forEach(function (condition) { + if (!condition.orConditions.length) { + return; + } + + var subSegmentStr = ''; + condition.orConditions.forEach(function (orCondition) { + if (!orCondition.value && !orCondition.segment && !orCondition.matches) { + return; + } + + if (subSegmentStr !== '') { + subSegmentStr += ','; // OR operator + } // one encode for urldecode on value, one encode for urldecode on condition + + + var value = encodeURIComponent(encodeURIComponent(orCondition.value)); + subSegmentStr += "".concat(orCondition.segment).concat(orCondition.matches).concat(value); + }); + + if (segmentStr !== '') { + segmentStr += ';'; // add AND operator between segment blocks + } + + segmentStr += subSegmentStr; + }); + this.segmentDefinition = segmentStr; + } + }, + computed: { + firstSegment: function firstSegment() { + return this.queriedSegments[0].segment; + }, + firstMatch: function firstMatch() { + var segment = this.queriedSegments[0]; + + if (!segment) { + return null; + } + + if (segment.type && this.matches[segment.type]) { + return this.matches[segment.type][0].key; + } + + return this.matches[''][0].key; + }, + segments: function segments() { + var result = {}; + this.queriedSegments.forEach(function (s) { + result[s.segment] = s; + }); + return result; + }, + segmentList: function segmentList() { + return this.queriedSegments.map(function (s) { + return { + group: s.category, + key: s.segment, + value: s.name, + tooltip: s.acceptedValues ? stripTags(s.acceptedValues) : undefined + }; + }); + }, + addNewOrConditionLinkText: function addNewOrConditionLinkText() { + return "+".concat(Object(external_CoreHome_["translate"])('SegmentEditor_AddANDorORCondition', "<span>".concat(Object(external_CoreHome_["translate"])('SegmentEditor_OperatorOR'), "</span>"))); + }, + andConditionLabel: function andConditionLabel() { + return this.conditions.length ? Object(external_CoreHome_["translate"])('SegmentEditor_OperatorAND') : ''; + }, + addNewAndConditionLinkText: function addNewAndConditionLinkText() { + return "+".concat(Object(external_CoreHome_["translate"])('SegmentEditor_AddANDorORCondition', "<span>".concat(this.andConditionLabel, "</span>"))); + }, + isLoading: function isLoading() { + return SegmentGenerator_store.state.value.isLoading; + } + } +})); +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue + + + +SegmentGeneratorvue_type_script_lang_ts.render = render + +/* harmony default export */ var SegmentGenerator = (SegmentGeneratorvue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.adapter.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + +/* harmony default export */ var SegmentGenerator_adapter = (Object(external_CoreHome_["createAngularJsAdapter"])({ + component: SegmentGenerator, + require: '?ngModel', + scope: { + segmentDefinition: { + angularJsBind: '@', + vue: 'modelValue' + }, + addInitialCondition: { + angularJsBind: '=', + transform: external_CoreHome_["transformAngularJsBoolAttr"] + }, + visitSegmentsOnly: { + angularJsBind: '=', + transform: external_CoreHome_["transformAngularJsBoolAttr"] + }, + idsite: { + angularJsBind: '=' + } + }, + directiveName: 'piwikSegmentGenerator', + $inject: ['$timeout'], + events: { + 'update:modelValue': function updateModelValue(newValue, vm, scope, element, attrs, ngModel, $timeout) { + var currentValue = ngModel ? ngModel.$viewValue : scope.segmentDefinition; + + if (newValue !== currentValue) { + $timeout(function () { + if (!ngModel) { + scope.segmentDefinition = newValue; + return; + } // ngModel being used + + + ngModel.$setViewValue(newValue); + ngModel.$render(); // not detected by the watch for some reason + }); + } + } + }, + postCreate: function postCreate(vm, scope, element, attrs, controller) { + // methods to forward for BC + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element.scope().segmentGenerator = { + getSegmentString: function getSegmentString() { + return vm.modelValue; + } + }; + var ngModel = controller; + + if (!ngModel) { + scope.$watch('segmentDefinition', function (newVal) { + if (newVal !== vm.modelValue) { + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["nextTick"])(function () { + vm.modelValue = newVal; + }); + } + }); + return; + } // ngModel being used + + + ngModel.$render = function () { + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["nextTick"])(function () { + vm.modelValue = Object(external_CoreHome_["removeAngularJsSpecificProperties"])(ngModel.$viewValue); + }); + }; + + if (typeof scope.segmentDefinition !== 'undefined') { + ngModel.$setViewValue(scope.segmentDefinition); + } else { + ngModel.$setViewValue(vm.modelValue); + } + } +})); +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/types.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +// CONCATENATED MODULE: ./plugins/SegmentEditor/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=SegmentEditor.umd.js.map
\ No newline at end of file diff --git a/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.min.js b/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.min.js new file mode 100644 index 0000000000..38e66de7f3 --- /dev/null +++ b/plugins/SegmentEditor/vue/dist/SegmentEditor.umd.min.js @@ -0,0 +1,14 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):"function"===typeof define&&define.amd?define(["CoreHome",,"CorePluginsAdmin"],t):"object"===typeof exports?exports["SegmentEditor"]=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):e["SegmentEditor"]=t(e["CoreHome"],e["Vue"],e["CorePluginsAdmin"])})("undefined"!==typeof self?self:this,(function(e,t,n){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="plugins/SegmentEditor/vue/dist/",n(n.s="fae3")}({"19dc":function(t,n){t.exports=e},"8bbf":function(e,n){e.exports=t},a5a2:function(e,t){e.exports=n},fae3:function(e,t,n){"use strict";if(n.r(t),n.d(t,"SegmentGeneratorStore",(function(){return B})),n.d(t,"SegmentGenerator",(function(){return F})),"undefined"!==typeof window){var o=window.document.currentScript,i=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);i&&(n.p=i[1])}var a=n("8bbf"),r=n("19dc"),c={class:"segment-generator",ref:"root"},l={class:"segment-rows"},s={class:"segment-row"},d=["onClick"],u={href:"#",class:"segment-loading"},m={class:"segment-row-inputs valign-wrapper"},f={class:"segment-input metricListBlock valign-wrapper"},p={style:{width:"100%"}},g={class:"segment-input metricMatchBlock valign-wrapper"},v={style:{display:"inline-block"}},h={class:"segment-input metricValueBlock valign-wrapper"},b={class:"form-group row",style:{width:"100%"}},O={class:"input-field col s12"},j=Object(a["createElementVNode"])("span",{role:"status","aria-live":"polite",class:"ui-helper-hidden-accessible"},null,-1),S=Object(a["createElementVNode"])("div",{class:"clear"},null,-1),V={class:"segment-or"},y=["onClick"],C=["innerHTML"],w={class:"segment-and"},k=["innerHTML"];function A(e,t,n,o,i,r){var A=this,N=Object(a["resolveComponent"])("ActivityIndicator"),E=Object(a["resolveComponent"])("Field"),L=Object(a["resolveComponent"])("ValueInput");return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",c,[Object(a["createVNode"])(N,{loading:e.isLoading},null,8,["loading"]),(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.conditions,(function(t,n){return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",{class:Object(a["normalizeClass"])("segmentRow".concat(n)),key:n},[Object(a["createElementVNode"])("div",l,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(t.orConditions,(function(n,o){var i,r;return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",{class:Object(a["normalizeClass"])("orCondId".concat(n.id)),key:o},[Object(a["createElementVNode"])("div",s,[Object(a["createElementVNode"])("a",{class:"segment-close",onClick:function(o){return e.removeOrCondition(t,n)}},null,8,d),Object(a["withDirectives"])(Object(a["createElementVNode"])("a",u,null,512),[[a["vShow"],e.conditionValuesLoading[n.id]]]),Object(a["createElementVNode"])("div",m,[Object(a["createElementVNode"])("div",f,[Object(a["createElementVNode"])("div",p,[Object(a["createVNode"])(E,{uicontrol:"expandable-select",name:"segments","model-value":n.segment,"onUpdate:modelValue":function(t){n.segment=t,e.updateAutocomplete(n),e.computeSegmentDefinition()},title:null===(i=e.segments[n.segment])||void 0===i?void 0:i.name,"full-width":!0,options:e.segmentList},null,8,["model-value","onUpdate:modelValue","title","options"])])]),Object(a["createElementVNode"])("div",g,[Object(a["createElementVNode"])("div",v,[Object(a["createVNode"])(E,{uicontrol:"select",name:"matchType","model-value":n.matches,"onUpdate:modelValue":function(t){n.matches=t,e.computeSegmentDefinition()},"full-width":!0,options:e.matches[null===(r=e.segments[n.segment])||void 0===r?void 0:r.type]},null,8,["model-value","onUpdate:modelValue","options"])])]),Object(a["createElementVNode"])("div",h,[Object(a["createElementVNode"])("div",b,[Object(a["createElementVNode"])("div",O,[j,Object(a["createVNode"])(L,{or:n,onUpdate:function(e){n.value=e,A.computeSegmentDefinition()}},null,8,["or","onUpdate"])])])]),S])]),Object(a["createElementVNode"])("div",V,Object(a["toDisplayString"])(e.translate("SegmentEditor_OperatorOR")),1)],2)})),128)),Object(a["createElementVNode"])("div",{class:"segment-add-or",onClick:function(n){return e.addNewOrCondition(t)}},[Object(a["createElementVNode"])("div",null,[Object(a["createElementVNode"])("a",{innerHTML:e.$sanitize(e.addNewOrConditionLinkText)},null,8,C)])],8,y)]),Object(a["createElementVNode"])("div",w,Object(a["toDisplayString"])(e.translate("SegmentEditor_OperatorAND")),1)],2)})),128)),Object(a["createElementVNode"])("div",{class:"segment-add-row initial",onClick:t[0]||(t[0]=function(t){return e.addNewAndCondition()})},[Object(a["createElementVNode"])("div",null,[Object(a["createElementVNode"])("a",{innerHTML:e.$sanitize(e.addNewAndConditionLinkText)},null,8,k)])])],512)}var N=n("a5a2");function E(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function L(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}function _(e,t,n){return t&&L(e.prototype,t),n&&L(e,n),e}function D(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e} +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */var x=function(){function e(){var t=this;E(this,e),D(this,"privateState",Object(a["reactive"])({isLoading:!1,segments:[]})),D(this,"state",Object(a["computed"])((function(){return Object(a["readonly"])(t.privateState)}))),D(this,"loadSegmentsAbort",void 0),D(this,"loadSegmentsPromise",void 0),D(this,"fetchedSiteId",void 0)}return _(e,[{key:"loadSegments",value:function(e,t){var n=this;if(this.loadSegmentsAbort&&(this.loadSegmentsAbort.abort(),this.loadSegmentsAbort=void 0),this.privateState.isLoading=!0,this.fetchedSiteId!==e&&(this.loadSegmentsAbort=void 0,this.fetchedSiteId=e),!this.loadSegmentsPromise){var o=void 0,i=void 0;"all"!==e&&e?e&&(o=e,i=e):(o="all",i="all"),this.loadSegmentsAbort=new AbortController,this.loadSegmentsPromise=r["AjaxHelper"].fetch({method:"API.getSegmentsMetadata",filter_limit:"-1",_hideImplementationData:0,idSites:o,idSite:i})}return this.loadSegmentsPromise.then((function(e){return n.privateState.isLoading=!1,e&&(n.privateState.segments=t?e.filter((function(e){return e.sqlSegment&&e.sqlSegment.match(/log_visit\./)})):e),n.state.value.segments})).finally((function(){n.privateState.isLoading=!1}))}}]),e}(),B=new x,I=["placeholder","title","value"];function G(e,t,n,o,i,r){return Object(a["openBlock"])(),Object(a["createElementBlock"])("input",{placeholder:e.translate("General_Value"),type:"text",class:"autocomplete",title:e.translate("General_Value"),autocomplete:"off",value:e.or.value,onKeydown:t[0]||(t[0]=function(t){return e.onKeydownOrConditionValue(t)}),onChange:t[1]||(t[1]=function(t){return e.onKeydownOrConditionValue(t)})},null,40,I)}var P=Object(a["defineComponent"])({props:{or:Object},created:function(){this.onKeydownOrConditionValue=Object(r["debounce"])(this.onKeydownOrConditionValue,50)},emits:["update"],methods:{onKeydownOrConditionValue:function(e){this.$emit("update",e.target.value)}}});P.render=G;var $=P;function T(){return{metric:[{key:"==",value:Object(r["translate"])("General_OperationEquals")},{key:"!=",value:Object(r["translate"])("General_OperationNotEquals")},{key:"<=",value:Object(r["translate"])("General_OperationAtMost")},{key:">=",value:Object(r["translate"])("General_OperationAtLeast")},{key:"<",value:Object(r["translate"])("General_OperationLessThan")},{key:">",value:Object(r["translate"])("General_OperationGreaterThan")}],dimension:[{key:"==",value:Object(r["translate"])("General_OperationIs")},{key:"!=",value:Object(r["translate"])("General_OperationIsNot")},{key:"=@",value:Object(r["translate"])("General_OperationContains")},{key:"!@",value:Object(r["translate"])("General_OperationDoesNotContain")},{key:"=^",value:Object(r["translate"])("General_OperationStartsWith")},{key:"=$",value:Object(r["translate"])("General_OperationEndsWith")}]}}function q(){for(var e="",t="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",n=1;n<=10;n+=1)e+=t.charAt(Math.floor(Math.random()*t.length));return e}function M(e){for(var t,n,o=["==","!=","<=",">=","=@","!@","<",">","=^","=$"],i={},a=e.length,r=!1,c=0;c<o.length;c+=1)t=o[c],n=e.indexOf(t),-1!==n&&n<a&&(a=n,1===t.length&&(r=!0));a<e.length&&(!0===r?(i.segment=e.substr(0,a),i.matches=e.substr(a,1),i.value=decodeURIComponent(e.substr(a+1))):(i.segment=e.substr(0,a),i.matches=e.substr(a,2),i.value=decodeURIComponent(e.substr(a+2))),'""'===i.value&&(i.value=""));try{i.value=decodeURIComponent(i.value)}catch(l){}return i}function U(e){return e?"".concat(e).replace(/(<([^>]+)>)/gi,""):e}var H=window,R=H.$,J=Object(a["defineComponent"])({props:{addInitialCondition:Boolean,visitSegmentsOnly:Boolean,idsite:[String,Number],modelValue:{type:String,default:""}},components:{ActivityIndicator:r["ActivityIndicator"],Field:N["Field"],ValueInput:$},data:function(){return{conditions:[],queriedSegments:[],matches:T(),conditionValuesLoading:{},segmentDefinition:""}},emits:["update:modelValue"],watch:{modelValue:function(e){e!==this.segmentDefinition&&this.setSegmentString(e)},conditions:{deep:!0,handler:function(){this.computeSegmentDefinition()}},segmentDefinition:function(e){e!==this.modelValue&&this.$emit("update:modelValue",e)},idsite:function(e){this.reloadSegments(e,this.visitSegmentsOnly)}},created:function(){this.matches[""]=this.matches.dimension,this.setSegmentString(this.modelValue),this.segmentDefinition=this.modelValue,this.reloadSegments(this.idsite,this.visitSegmentsOnly)},methods:{reloadSegments:function(e,t){var n=this;B.loadSegments(e,t).then((function(e){n.queriedSegments=e.map((function(e){return Object.assign(Object.assign({},e),{},{category:e.category||"Others"})})),n.addInitialCondition&&0===n.conditions.length&&n.addNewAndCondition()}))},addAndCondition:function(e){this.conditions.push(e)},addNewOrCondition:function(e){var t={segment:this.firstSegment,matches:this.firstMatch,value:""};this.addOrCondition(e,t)},addOrCondition:function(e,t){var n=this;this.conditionValuesLoading[t.id]=!1,t.id=q(),e.orConditions.push(t),Object(a["nextTick"])((function(){n.updateAutocomplete(t)}))},updateAutocomplete:function(e){var t=this;this.conditionValuesLoading[e.id]=!0,R(".orCondId".concat(e.id," .metricValueBlock input"),this.$refs.root).autocomplete({source:[],minLength:0});var n=new AbortController,o=!1;r["AjaxHelper"].fetch({module:"API",format:"json",method:"API.getSuggestedValuesForSegment",segmentName:e.segment}).then((function(n){t.conditionValuesLoading[e.id]=!1,o=!0;var i=R(".orCondId".concat(e.id," .metricValueBlock input")).autocomplete({source:n,minLength:0,select:function(n,o){n.preventDefault(),e.value=o.item.value,t.computeSegmentDefinition(),t.$forceUpdate()}}).off("click").click((function(){R(i).autocomplete("search",e.value)}))})).catch((function(){o=!0,t.conditionValuesLoading[e.id]=!1,R(".orCondId".concat(e.id," .metricValueBlock input")).autocomplete({source:[],minLength:0}).autocomplete("search",e.value)})),setTimeout((function(){o||n.abort()}),2e4)},removeOrCondition:function(e,t){var n=e.orConditions.indexOf(t);if(n>-1&&e.orConditions.splice(n,1),0===e.orConditions.length){var o=this.conditions.indexOf(e);n>-1&&this.conditions.splice(o,1)}},setSegmentString:function(e){var t=this;if(this.conditions=[],e){var n=e.split(";").map((function(e){return e.split(",")}));this.conditions=n.map((function(e){var n={orConditions:[]};return e.forEach((function(e){var o=M(e);t.addOrCondition(n,o)})),n}))}},addNewAndCondition:function(){var e={orConditions:[]};return this.addAndCondition(e),this.addNewOrCondition(e),e},computeSegmentDefinition:function(){var e="";this.conditions.forEach((function(t){if(t.orConditions.length){var n="";t.orConditions.forEach((function(e){if(e.value||e.segment||e.matches){""!==n&&(n+=",");var t=encodeURIComponent(encodeURIComponent(e.value));n+="".concat(e.segment).concat(e.matches).concat(t)}})),""!==e&&(e+=";"),e+=n}})),this.segmentDefinition=e}},computed:{firstSegment:function(){return this.queriedSegments[0].segment},firstMatch:function(){var e=this.queriedSegments[0];return e?e.type&&this.matches[e.type]?this.matches[e.type][0].key:this.matches[""][0].key:null},segments:function(){var e={};return this.queriedSegments.forEach((function(t){e[t.segment]=t})),e},segmentList:function(){return this.queriedSegments.map((function(e){return{group:e.category,key:e.segment,value:e.name,tooltip:e.acceptedValues?U(e.acceptedValues):void 0}}))},addNewOrConditionLinkText:function(){return"+".concat(Object(r["translate"])("SegmentEditor_AddANDorORCondition","<span>".concat(Object(r["translate"])("SegmentEditor_OperatorOR"),"</span>")))},andConditionLabel:function(){return this.conditions.length?Object(r["translate"])("SegmentEditor_OperatorAND"):""},addNewAndConditionLinkText:function(){return"+".concat(Object(r["translate"])("SegmentEditor_AddANDorORCondition","<span>".concat(this.andConditionLabel,"</span>")))},isLoading:function(){return B.state.value.isLoading}}});J.render=A;var F=J; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */Object(r["createAngularJsAdapter"])({component:F,require:"?ngModel",scope:{segmentDefinition:{angularJsBind:"@",vue:"modelValue"},addInitialCondition:{angularJsBind:"=",transform:r["transformAngularJsBoolAttr"]},visitSegmentsOnly:{angularJsBind:"=",transform:r["transformAngularJsBoolAttr"]},idsite:{angularJsBind:"="}},directiveName:"piwikSegmentGenerator",$inject:["$timeout"],events:{"update:modelValue":function(e,t,n,o,i,a,r){var c=a?a.$viewValue:n.segmentDefinition;e!==c&&r((function(){a?(a.$setViewValue(e),a.$render()):n.segmentDefinition=e}))}},postCreate:function(e,t,n,o,i){n.scope().segmentGenerator={getSegmentString:function(){return e.modelValue}};var c=i;c?(c.$render=function(){Object(a["nextTick"])((function(){e.modelValue=Object(r["removeAngularJsSpecificProperties"])(c.$viewValue)}))},"undefined"!==typeof t.segmentDefinition?c.$setViewValue(t.segmentDefinition):c.$setViewValue(e.modelValue)):t.$watch("segmentDefinition",(function(t){t!==e.modelValue&&Object(a["nextTick"])((function(){e.modelValue=t}))}))}})}})})); +//# sourceMappingURL=SegmentEditor.umd.min.js.map
\ No newline at end of file diff --git a/plugins/SegmentEditor/vue/dist/umd.metadata.json b/plugins/SegmentEditor/vue/dist/umd.metadata.json new file mode 100644 index 0000000000..dce4477a3c --- /dev/null +++ b/plugins/SegmentEditor/vue/dist/umd.metadata.json @@ -0,0 +1,6 @@ +{ + "dependsOn": [ + "CoreHome", + "CorePluginsAdmin" + ] +}
\ No newline at end of file diff --git a/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.adapter.ts b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.adapter.ts new file mode 100644 index 0000000000..49642797f3 --- /dev/null +++ b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.adapter.ts @@ -0,0 +1,91 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { INgModelController, ITimeoutService } from 'angular'; +import { nextTick } from 'vue'; +import { + createAngularJsAdapter, + removeAngularJsSpecificProperties, + transformAngularJsBoolAttr, +} from 'CoreHome'; +import SegmentGenerator from './SegmentGenerator.vue'; + +export default createAngularJsAdapter<[ITimeoutService]>({ + component: SegmentGenerator, + require: '?ngModel', + scope: { + segmentDefinition: { + angularJsBind: '@', + vue: 'modelValue', + }, + addInitialCondition: { + angularJsBind: '=', + transform: transformAngularJsBoolAttr, + }, + visitSegmentsOnly: { + angularJsBind: '=', + transform: transformAngularJsBoolAttr, + }, + idsite: { + angularJsBind: '=', + }, + }, + directiveName: 'piwikSegmentGenerator', + $inject: ['$timeout'], + events: { + 'update:modelValue': (newValue, vm, scope, element, attrs, ngModel, $timeout) => { + const currentValue = ngModel ? ngModel.$viewValue : scope.segmentDefinition; + if (newValue !== currentValue) { + $timeout(() => { + if (!ngModel) { + scope.segmentDefinition = newValue; + return; + } + + // ngModel being used + (ngModel as INgModelController).$setViewValue(newValue); + (ngModel as INgModelController).$render(); // not detected by the watch for some reason + }); + } + }, + }, + postCreate(vm, scope, element, attrs, controller) { + // methods to forward for BC + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (element.scope() as any).segmentGenerator = { + getSegmentString(): string { + return vm.modelValue; + }, + }; + + const ngModel = controller as INgModelController; + if (!ngModel) { + scope.$watch('segmentDefinition', (newVal: unknown) => { + if (newVal !== vm.modelValue) { + nextTick(() => { + vm.modelValue = newVal; + }); + } + }); + + return; + } + + // ngModel being used + ngModel.$render = () => { + nextTick(() => { + vm.modelValue = removeAngularJsSpecificProperties(ngModel.$viewValue); + }); + }; + + if (typeof scope.segmentDefinition !== 'undefined') { + (ngModel as INgModelController).$setViewValue(scope.segmentDefinition); + } else { + ngModel.$setViewValue(vm.modelValue); + } + }, +}); diff --git a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.directive.less b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.less index 778021f446..778021f446 100644 --- a/plugins/SegmentEditor/angularjs/segment-generator/segmentgenerator.directive.less +++ b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.less diff --git a/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.store.ts b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.store.ts new file mode 100644 index 0000000000..46d68274d4 --- /dev/null +++ b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.store.ts @@ -0,0 +1,94 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import { + reactive, + computed, + readonly, + DeepReadonly, +} from 'vue'; +import { AjaxHelper } from 'CoreHome'; +import { SegmentMetadata } from '../types'; + +interface SegmentGeneratorStoreState { + isLoading: boolean; + segments: SegmentMetadata[]; +} + +class SegmentGeneratorStore { + private privateState: SegmentGeneratorStoreState = reactive<SegmentGeneratorStoreState>({ + isLoading: false, + segments: [], + }); + + readonly state = computed(() => readonly(this.privateState)); + + private loadSegmentsAbort?: AbortController; + + private loadSegmentsPromise?: Promise<SegmentMetadata[]>; + + private fetchedSiteId?: string|number; + + loadSegments( + siteId?: string|number, + visitSegmentsOnly?: boolean, + ): Promise<DeepReadonly<SegmentMetadata[]>> { + if (this.loadSegmentsAbort) { + this.loadSegmentsAbort.abort(); + this.loadSegmentsAbort = undefined; + } + + this.privateState.isLoading = true; + + if (this.fetchedSiteId !== siteId) { + this.loadSegmentsAbort = undefined; + this.fetchedSiteId = siteId; + } + + if (!this.loadSegmentsPromise) { + let idSites: string|number|undefined = undefined; + let idSite: string|number|undefined = undefined; + + if (siteId === 'all' || !siteId) { + idSites = 'all'; + idSite = 'all'; + } else if (siteId) { + idSites = siteId; + idSite = siteId; + } + + this.loadSegmentsAbort = new AbortController(); + this.loadSegmentsPromise = AjaxHelper.fetch<SegmentMetadata[]>({ + method: 'API.getSegmentsMetadata', + filter_limit: '-1', + _hideImplementationData: 0, + idSites, + idSite, + }); + } + + return this.loadSegmentsPromise.then((response) => { + this.privateState.isLoading = false; + + if (response) { + if (visitSegmentsOnly) { + this.privateState.segments = response.filter( + (s) => s.sqlSegment && s.sqlSegment.match(/log_visit\./), + ); + } else { + this.privateState.segments = response; + } + } + + return this.state.value.segments; + }).finally(() => { + this.privateState.isLoading = false; + }); + } +} + +export default new SegmentGeneratorStore(); diff --git a/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue new file mode 100644 index 0000000000..7fd9b5db91 --- /dev/null +++ b/plugins/SegmentEditor/vue/src/SegmentGenerator/SegmentGenerator.vue @@ -0,0 +1,523 @@ +<!-- + Matomo - free/libre analytics platform + @link https://matomo.org + @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +--> + +<template> + <div class="segment-generator" ref="root"> + <ActivityIndicator :loading="isLoading" /> + <div + :class="`segmentRow${conditionIndex}`" + v-for="(condition, conditionIndex) in conditions" + :key="conditionIndex" + > + <div class="segment-rows"> + <div + :class="`orCondId${orCondition.id}`" + v-for="(orCondition, orConditionIndex) in condition.orConditions" + :key="orConditionIndex" + > + <div class="segment-row"> + <a + class="segment-close" + @click="removeOrCondition(condition, orCondition)" + /> + <a + href="#" + class="segment-loading" + v-show="conditionValuesLoading[orCondition.id]" + /> + <div class="segment-row-inputs valign-wrapper"> + <div class="segment-input metricListBlock valign-wrapper"> + <div style="width: 100%;"> + <Field + uicontrol="expandable-select" + name="segments" + :model-value="orCondition.segment" + @update:model-value="orCondition.segment = $event; + updateAutocomplete(orCondition); computeSegmentDefinition();" + :title="segments[orCondition.segment]?.name" + :full-width="true" + :options="segmentList" + > + </Field> + </div> + </div> + <div class="segment-input metricMatchBlock valign-wrapper"> + <div style="display: inline-block"> + <Field + uicontrol="select" + name="matchType" + :model-value="orCondition.matches" + @update:model-value="orCondition.matches = $event; computeSegmentDefinition();" + :full-width="true" + :options="matches[segments[orCondition.segment]?.type]" + > + </Field> + </div> + </div> + <div class="segment-input metricValueBlock valign-wrapper"> + <div + class="form-group row" + style="width: 100%;" + > + <div class="input-field col s12"> + <span + role="status" + aria-live="polite" + class="ui-helper-hidden-accessible" + /> + <ValueInput + :or="orCondition" + @update="orCondition.value = $event; + // deep watch doesn't catch this change + this.computeSegmentDefinition();" + /> + </div> + </div> + </div> + <div class="clear" /> + </div> + </div> + <div class="segment-or">{{ translate('SegmentEditor_OperatorOR') }}</div> + </div> + <div + class="segment-add-or" + @click="addNewOrCondition(condition)" + > + <div> + <a v-html="$sanitize(addNewOrConditionLinkText)" /> + </div> + </div> + </div> + <div class="segment-and">{{ translate('SegmentEditor_OperatorAND') }}</div> + </div> + <div + class="segment-add-row initial" + @click="addNewAndCondition()" + > + <div> + <a v-html="$sanitize(addNewAndConditionLinkText)" /> + </div> + </div> + </div> +</template> + +<script lang="ts"> +import { DeepReadonly, defineComponent, nextTick } from 'vue'; +import { + translate, + AjaxHelper, + ActivityIndicator, +} from 'CoreHome'; +import { Field } from 'CorePluginsAdmin'; +import SegmentGeneratorStore from './SegmentGenerator.store'; +import { SegmentAndCondition, SegmentMetadata, SegmentOrCondition } from '../types'; +import ValueInput from './ValueInput.vue'; + +interface SegmentGeneratorState { + conditions: SegmentAndCondition[]; + matches: Record<string, { key: string, value: string }[]>; + queriedSegments: DeepReadonly<SegmentMetadata[]>; + conditionValuesLoading: Record<string, boolean>; + segmentDefinition: string; +} + +function initialMatches() { + return { + metric: [ + { + key: '==', + value: translate('General_OperationEquals'), + }, + { + key: '!=', + value: translate('General_OperationNotEquals'), + }, + { + key: '<=', + value: translate('General_OperationAtMost'), + }, + { + key: '>=', + value: translate('General_OperationAtLeast'), + }, + { + key: '<', + value: translate('General_OperationLessThan'), + }, + { + key: '>', + value: translate('General_OperationGreaterThan'), + }, + ], + dimension: [ + { + key: '==', + value: translate('General_OperationIs'), + }, + { + key: '!=', + value: translate('General_OperationIsNot'), + }, + { + key: '=@', + value: translate('General_OperationContains'), + }, + { + key: '!@', + value: translate('General_OperationDoesNotContain'), + }, + { + key: '=^', + value: translate('General_OperationStartsWith'), + }, + { + key: '=$', + value: translate('General_OperationEndsWith'), + }, + ], + }; +} + +function generateUniqueId() { + let id = ''; + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for (let i = 1; i <= 10; i += 1) { + id += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return id; +} + +function findAndExplodeByMatch(metric: string) { + const matches = ['==', '!=', '<=', '>=', '=@', '!@', '<', '>', '=^', '=$']; + const newMetric: SegmentOrCondition = {} as unknown as SegmentOrCondition; + let minPos = metric.length; + let match; + let index: number; + let singleChar = false; + + for (let key = 0; key < matches.length; key += 1) { + match = matches[key]; + index = metric.indexOf(match); + if (index !== -1) { + if (index < minPos) { + minPos = index; + if (match.length === 1) { + singleChar = true; + } + } + } + } + + if (minPos < metric.length) { + // sth found - explode + if (singleChar === true) { + newMetric.segment = metric.substr(0, minPos); + newMetric.matches = metric.substr(minPos, 1); + newMetric.value = decodeURIComponent(metric.substr(minPos + 1)); + } else { + newMetric.segment = metric.substr(0, minPos); + newMetric.matches = metric.substr(minPos, 2); + newMetric.value = decodeURIComponent(metric.substr(minPos + 2)); + } + + // if value is only '' -> change to empty string + if (newMetric.value === '""') { + newMetric.value = ''; + } + } + + try { + // Decode again to deal with double-encoded segments in database + newMetric.value = decodeURIComponent(newMetric.value); + } catch (e) { + // Expected if the segment was not double-encoded + } + + return newMetric; +} + +function stripTags(text?: unknown) { + return text ? `${text}`.replace(/(<([^>]+)>)/ig, '') : text; +} + +const { $ } = window; + +export default defineComponent({ + props: { + addInitialCondition: Boolean, + visitSegmentsOnly: Boolean, + idsite: [String, Number], + modelValue: { + type: String, + default: '', + }, + }, + components: { + ActivityIndicator, + Field, + ValueInput, + }, + data(): SegmentGeneratorState { + return { + conditions: [], + queriedSegments: [], + matches: initialMatches(), + conditionValuesLoading: {}, + segmentDefinition: '', + }; + }, + emits: ['update:modelValue'], + watch: { + modelValue(newVal) { + if (newVal !== this.segmentDefinition) { + this.setSegmentString(newVal); + } + }, + conditions: { + deep: true, + handler() { + this.computeSegmentDefinition(); + }, + }, + segmentDefinition(newVal) { + if (newVal !== this.modelValue) { + this.$emit('update:modelValue', newVal); + } + }, + idsite(newVal) { + this.reloadSegments(newVal, this.visitSegmentsOnly); + }, + }, + created() { + this.matches[''] = this.matches.dimension; + this.setSegmentString(this.modelValue); + this.segmentDefinition = this.modelValue; + + this.reloadSegments(this.idsite, this.visitSegmentsOnly); + }, + methods: { + reloadSegments(idsite?: string|number, visitSegmentsOnly?: boolean) { + SegmentGeneratorStore.loadSegments(idsite, visitSegmentsOnly).then((segments) => { + this.queriedSegments = segments.map((s) => ({ + ...s, + category: s.category || 'Others', + })); + + if (this.addInitialCondition && this.conditions.length === 0) { + this.addNewAndCondition(); + } + }); + }, + addAndCondition(condition: SegmentAndCondition) { + this.conditions.push(condition); + }, + addNewOrCondition(condition: SegmentAndCondition) { + const orCondition = { + segment: this.firstSegment, + matches: this.firstMatch!, + value: '', + }; + + this.addOrCondition(condition, orCondition); + }, + addOrCondition(condition: SegmentAndCondition, orCondition: SegmentOrCondition) { + this.conditionValuesLoading[orCondition.id!] = false; + orCondition.id = generateUniqueId(); + + condition.orConditions.push(orCondition); + + nextTick(() => { + this.updateAutocomplete(orCondition); + }); + }, + updateAutocomplete(orCondition: SegmentOrCondition) { + this.conditionValuesLoading[orCondition.id!] = true; + + $(`.orCondId${orCondition.id} .metricValueBlock input`, this.$refs.root as HTMLElement) + .autocomplete({ + source: [], + minLength: 0, + }); + + const abortController = new AbortController(); + + let resolved = false; + AjaxHelper.fetch<string[]>( + { + module: 'API', + format: 'json', + method: 'API.getSuggestedValuesForSegment', + segmentName: orCondition.segment, + }, + ).then((response) => { + this.conditionValuesLoading[orCondition.id!] = false; + resolved = true; + + const inputElement = $(`.orCondId${orCondition.id} .metricValueBlock input`) + .autocomplete({ + source: response, + minLength: 0, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + select: (event: Event, ui: any) => { + event.preventDefault(); + + orCondition.value = ui.item.value; + this.computeSegmentDefinition(); // deep watch doesn't catch this change + this.$forceUpdate(); + }, + }) + .off('click') + .click(() => { + $(inputElement).autocomplete('search', orCondition.value); + }); + }).catch(() => { + resolved = true; + + this.conditionValuesLoading[orCondition.id!] = false; + + $(`.orCondId${orCondition.id} .metricValueBlock input`) + .autocomplete({ + source: [], + minLength: 0, + }) + .autocomplete('search', orCondition.value); + }); + + setTimeout(() => { + if (!resolved) { + abortController.abort(); + } + }, 20000); + }, + removeOrCondition(condition: SegmentAndCondition, orCondition: SegmentOrCondition) { + const index = condition.orConditions.indexOf(orCondition); + + if (index > -1) { + condition.orConditions.splice(index, 1); + } + + if (condition.orConditions.length === 0) { + const andCondIndex = this.conditions.indexOf(condition); + + if (index > -1) { + this.conditions.splice(andCondIndex, 1); + } + } + }, + setSegmentString(segmentStr: string) { + this.conditions = []; + + if (!segmentStr) { + return; + } + + const blocks = segmentStr.split(';').map((b) => b.split(',')); + this.conditions = blocks.map((block) => { + const condition: SegmentAndCondition = { orConditions: [] }; + + block.forEach((innerBlock) => { + const orCondition: SegmentOrCondition = findAndExplodeByMatch(innerBlock); + this.addOrCondition(condition, orCondition); + }); + + return condition; + }); + }, + addNewAndCondition() { + const condition = { orConditions: [] }; + + this.addAndCondition(condition); + this.addNewOrCondition(condition); + + return condition; + }, + // NOTE: can't use a computed property since we need to recompute on changes inside the + // structure. don't have to if we don't do in-place changes, but with nested structures, + // that's complicated. + computeSegmentDefinition() { + let segmentStr = ''; + + this.conditions.forEach((condition) => { + if (!condition.orConditions.length) { + return; + } + + let subSegmentStr = ''; + condition.orConditions.forEach((orCondition) => { + if (!orCondition.value && !orCondition.segment && !orCondition.matches) { + return; + } + + if (subSegmentStr !== '') { + subSegmentStr += ','; // OR operator + } + + // one encode for urldecode on value, one encode for urldecode on condition + const value = encodeURIComponent(encodeURIComponent(orCondition.value)); + subSegmentStr += `${orCondition.segment}${orCondition.matches}${value}`; + }); + + if (segmentStr !== '') { + segmentStr += ';'; // add AND operator between segment blocks + } + + segmentStr += subSegmentStr; + }); + + this.segmentDefinition = segmentStr; + }, + }, + computed: { + firstSegment() { + return this.queriedSegments[0].segment; + }, + firstMatch() { + const segment = this.queriedSegments[0]; + if (!segment) { + return null; + } + + if (segment.type && this.matches[segment.type]) { + return this.matches[segment.type][0].key; + } + + return this.matches[''][0].key; + }, + segments() { + const result: Record<string, SegmentMetadata> = {}; + this.queriedSegments.forEach((s) => { + result[s.segment] = s; + }); + return result; + }, + segmentList() { + return this.queriedSegments.map((s) => ({ + group: s.category, + key: s.segment, + value: s.name, + tooltip: s.acceptedValues ? stripTags(s.acceptedValues) : undefined, + })); + }, + addNewOrConditionLinkText() { + return `+${translate( + 'SegmentEditor_AddANDorORCondition', + `<span>${translate('SegmentEditor_OperatorOR')}</span>`, + )}`; + }, + andConditionLabel() { + return this.conditions.length ? translate('SegmentEditor_OperatorAND') : ''; + }, + addNewAndConditionLinkText() { + return `+${translate('SegmentEditor_AddANDorORCondition', `<span>${this.andConditionLabel}</span>`)}`; + }, + isLoading() { + return SegmentGeneratorStore.state.value.isLoading; + }, + }, +}); +</script> diff --git a/plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue b/plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue new file mode 100644 index 0000000000..15880bb0db --- /dev/null +++ b/plugins/SegmentEditor/vue/src/SegmentGenerator/ValueInput.vue @@ -0,0 +1,38 @@ +<!-- + Matomo - free/libre analytics platform + @link https://matomo.org + @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later +--> + +<template> + <input + :placeholder="translate('General_Value')" + type="text" + class="autocomplete" + :title="translate('General_Value')" + autocomplete="off" + :value="or.value" + @keydown="onKeydownOrConditionValue($event)" + @change="onKeydownOrConditionValue($event)" + /> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { debounce } from 'CoreHome'; + +export default defineComponent({ + props: { + or: Object, + }, + created() { + this.onKeydownOrConditionValue = debounce(this.onKeydownOrConditionValue, 50); + }, + emits: ['update'], + methods: { + onKeydownOrConditionValue(event: Event) { + this.$emit('update', (event.target as HTMLInputElement).value); + }, + }, +}); +</script> diff --git a/plugins/SegmentEditor/vue/src/index.ts b/plugins/SegmentEditor/vue/src/index.ts new file mode 100644 index 0000000000..5258822104 --- /dev/null +++ b/plugins/SegmentEditor/vue/src/index.ts @@ -0,0 +1,12 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +import './SegmentGenerator/SegmentGenerator.adapter'; + +export * from './types'; +export { default as SegmentGeneratorStore } from './SegmentGenerator/SegmentGenerator.store'; +export { default as SegmentGenerator } from './SegmentGenerator/SegmentGenerator.vue'; diff --git a/plugins/SegmentEditor/vue/src/types.ts b/plugins/SegmentEditor/vue/src/types.ts new file mode 100644 index 0000000000..62aab9d1f5 --- /dev/null +++ b/plugins/SegmentEditor/vue/src/types.ts @@ -0,0 +1,30 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export interface SegmentMetadata { + acceptedValues: string; + category: string; + name: string; + needsMostFrequentValues: boolean; + segment: string; + sqlFilterValue: unknown; + sqlSegment: string; + type: string; +} + +export interface SegmentOrCondition { + segment: string; + matches: string; + value: string; + + id?: string; + isLoading?: boolean; +} + +export interface SegmentAndCondition { + orConditions: SegmentOrCondition[]; +} |