From 10965956e49319f8d7e4f58f8fb9cf006cfa7dea Mon Sep 17 00:00:00 2001 From: dizzy Date: Fri, 31 Dec 2021 01:07:46 -0800 Subject: [Vue] migrate ajax-form directive to vue (#18443) * migrate most of reporting menu directive and model * Use themed font family for input forms to override materialize.css styling * rebuild vue * add a missing div * ui test fixes * update styling * get to build * get to load in the UI w/o error * clone result of functions * fix compile issue * migrate widget loader and get to load in UI * rebuild vue * migrate widgetcontainer * migrate widget bydimension container * migrate widget + add tooltips directive * quick fix * Updating version to 4.6.0 * loading in page * update expected screenshot * add wait just in case travis is slow * fix ordering bug * add another wait * rebuild vue * css tweak * fix some bugs and tests * undo screenshot changes * Menus test passing locally * [Vue] date picker viewDate property is not kept up to date (#18385) * viewDate ref is not kept up to date * rebuild corehome * reporting menu subcategory items are meant to be normal links * update some screenshots * use innerText instead of text() since angularjs maintains newlines in HTML that vue does not add * trigger angularjs digest after ajaxhelper request * rebuild vue * update screenshots, fix bug in link generation in reporting menu and allow syncing multiple screenshot regexes at a time * undo box-shadow change for UI tests * fix more issues & update more tests * update some screenshots * fix some tests * rebuild CoreHome * quick fix * built vue files * fix angularjs issue * add comment * update umd files * 4.6.1-rc1 * 4.6.1 * fix field array title * apply some pr feedback * apply more pr feedback * another fix * tweak * fix ng-change not executed before ng-model * fix another set of issues * fix another issue * rebuild vue * better ng-change/ng-model fix * update some screenshots * rebuild vue * remove some TODOs * initiate initial ng-change ONLY for site selectors where this behavior applies * emit/broadcast on correct scope in wrapper * rebuild vue * fix some issues * couple more fixes * fix another title issue * rebuild vue * do not report on ajax errors in notifications if not logged in * migrate reporting page and model * rebuild vue * create sites selector model adapter * fix siteselector vue bug, initial site is only set if there is just one site available * rebuild vue * migrate plugin settings directive * remove TODO * migrate plugin filter directive * migrate two more plugins directives * migrate save button * fix a bunch of bugs * fix another widget bug * allow change event name between angularjs and vue * rebuild vue * migrate plugin form directive * get to work * migrate select-on-focus directive and start migrating report-export directive * finish migrating report export directive & popover component + create reusable function to create vue app and add globals to it * rebuild vue * remove angularjs files and move less contents to vue dir * migrate sparkline component * migrate progressbar component * migrate content intro & content table directives * rebuild vue * migrate piwik-ajax-form component to vue * built vue files * fix function signature * fix vue warning * fix ajax request race condition * rebuild vue * add new notification type "help" so the help notification is not cleared when clearing transient notifications * fix some bugs and tests * update screenshot * update screenshot & fix a test * allow using unminified jquery ui + fix bug in last fix * fix error when enrichedheadline is used in modal * add polyfill min.js * remove two todos * fix widget url logic * update some screenshots and fix sanitization/escape issue * update screenshots * rebuild vue * fix url location updating regression in MatomoUrl.updateLocation use * submodule * update screenshots and fix possible error in json parse * built vue files * Merge branch 'vue-period-selector-regression' into vue-reporting-menu * rebuild vue * use correct variable * rebuild vue * fix widget url logic * segment parameter can be undefined now for some reason * fix ngmodel binding in siteselector adapter (for last time hopefully) * the original site selector only set the first site to the first site in the initial sites query if there was only one site in the entire matomo instance * fix sitesmanager ui test failure * fix usersettings test failure * rebuild vue * more siteselector tweaks. * build CoreHome * more siteselector tweaks. * another siteselector issue * update screenshots * update screenshot and try to fix random failure * fix some issues in widget.vue when containerid is specified * fix couple tests * fix several test failures * fix string concat * fix test failure * extra change * fix last change and random failure * styling fix * fix last fix * real fix this time * fix stray request * proper fix * update build files * try to fix random failure * do not submit form * check for api errors in promise chain in ajaxhelper.ts * force a digest after a location change * use proper abortcontroller method instead of promise hack, have to add new polyfill + try to fix random test failure * some UI test fixes * fix some report export issues * several save button fixes + make replace approximation in createAngularJsAdapter better * fix sparkline * apply after manual click triggering in savebutton * css fixes and piwik-content-table was never applied by angularjs in installtion * rebuild vue * add names to divs so they can still be queried as they were in angularjs * rebuild vue * now that format_metrics checkbox works, need to check it * remove unintended changes * update screenshots * update screenshot * fix notification scroll and simplify adapter a little Co-authored-by: Justin Velluppillai Co-authored-by: justinvelluppillai Co-authored-by: Matthieu Aubry --- plugins/CoreHome/CoreHome.php | 3 - .../angularjs/ajax-form/ajax-form.controller.js | 85 ------ .../angularjs/ajax-form/ajax-form.directive.js | 144 ----------- plugins/CoreHome/vue/dist/CoreHome.umd.js | 284 +++++++++++++++++++++ plugins/CoreHome/vue/dist/CoreHome.umd.min.js | 108 ++++---- .../CoreHome/vue/src/AjaxForm/AjaxForm.adapter.ts | 188 ++++++++++++++ plugins/CoreHome/vue/src/AjaxForm/AjaxForm.vue | 113 ++++++++ plugins/CoreHome/vue/src/index.ts | 2 + 8 files changed, 644 insertions(+), 283 deletions(-) delete mode 100644 plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js delete mode 100644 plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js create mode 100644 plugins/CoreHome/vue/src/AjaxForm/AjaxForm.adapter.ts create mode 100644 plugins/CoreHome/vue/src/AjaxForm/AjaxForm.vue diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index ace87ca8d1..daac059bae 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -227,9 +227,6 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "plugins/CoreHome/angularjs/history/history.service.js"; - $jsFiles[] = "plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js"; - $jsFiles[] = "plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js"; - $jsFiles[] = "plugins/CoreHome/angularjs/popover-handler/popover-handler.directive.js"; // we have to load these CoreAdminHome files here. If we loaded them in CoreAdminHome, diff --git a/plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js b/plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js deleted file mode 100644 index 5bc6310962..0000000000 --- a/plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * Matomo - free/libre analytics platform - * - * @link https://matomo.org - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - */ -(function () { - angular.module('piwikApp').controller('AjaxFormController', AjaxFormController); - - AjaxFormController.$inject = ['piwikApi', '$filter']; - - function AjaxFormController(piwikApi, $filter) { - var vm = this; - - /** - * Set to non-null when a form submit request returns successfully. When successful, it will - * be the entire JSON parsed response of the request. - * - * @type {null|string} - */ - vm.successfulPostResponse = null; - - /** - * Set to non-null when a form submit request results in an error. When an error occurs, - * it will be set to the string error message. - * - * @type {null|string} - */ - vm.errorPostResponse = null; - - /** - * true if currently submitting a POST request, false if otherwise. - * - * @type {bool} - */ - vm.isSubmitting = false; - - vm.submitForm = submitForm; - - /** - * Sends a POST to the configured API method. - */ - function submitForm() { - var postParams; - - vm.successfulPostResponse = null; - vm.errorPostResponse = null; - - if (vm.sendJsonPayload) { - postParams = {data: JSON.stringify(vm.data)}; - } else { - postParams = vm.data; - } - - vm.isSubmitting = true; - piwikApi.post( - { // GET params - module: 'API', - method: vm.submitApiMethod - }, - postParams, - { // request options - createErrorNotification: !vm.noErrorNotification - } - ).then(function (response) { - vm.successResponse = response; - - if (!vm.noSuccessNotification) { - var UI = require('piwik/UI'); - var notification = new UI.Notification(); - notification.show($filter('translate')('General_YourChangesHaveBeenSaved'), { - context: 'success', - type: 'toast', - id: 'ajaxHelper' - }); - notification.scrollToNotification(); - } - }).catch(function (errorMessage) { - vm.errorPostResponse = errorMessage; - }).finally(function () { - vm.isSubmitting = false; - }); - } - } -})(); \ No newline at end of file diff --git a/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js b/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js deleted file mode 100644 index caacca2384..0000000000 --- a/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js +++ /dev/null @@ -1,144 +0,0 @@ -/*! - * Matomo - free/libre analytics platform - * - * @link https://matomo.org - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - */ - -/** - * AngularJS directive that manages an AJAX form. - * - * This directive will detect inputs & selects defined within an element and when a - * submit button is clicked, will post data from the inputs & selects to a Piwik API method. - * - * When the POST request is finished the result will, by default, be displayed as a - * notification. - * - * This directive accepts the following attributes: - * - * - **submit-api-method**: **required** The Piwik API method that handles the POST request. - * - **send-json-payload**: Whether to send the data as a form encoded URL or to send it as JSON. - * If sending as JSON, the payload will still be a form encoded value, - * but will contain a JSON object like `{data: {...form data...}}`. - * - * This is for forms with lots of fields where having the same number - * of parameters in an API method would not be desired. - * - **no-error-notification**: If true, does not display an error notification if the AJAX post - * fails. - * - **no-success-notification**: If true, does not display an error notification if the AJAX - * results in success. - * - * **Custom Success/Error Handling** - * - * On success/failure, the response will be stored in controller scope. Child elements of a - * piwik-ajax-form element can access this data, and thus, can customize what happens when - * a form submit succeeds/fails. - * - * See the ajax-form.controller.js file for more info. - * - * Usage: - * - *
- * - *

My Form

- * - * - * - * - *
ERROR!
- *
- */ -(function () { - angular.module('piwikApp').directive('piwikAjaxForm', piwikAjaxForm); - - piwikAjaxForm.$inject = ['$parse']; - - function piwikAjaxForm($parse) { - return { - restrict: 'A', - scope: { - submitApiMethod: '=', - sendJsonPayload: '=', - noErrorNotification: '=', - noSuccessNotification: '=', - useCustomDataBinding: '=' - }, - require: '?ngModel', - controller: 'AjaxFormController', - controllerAs: 'ajaxForm', - transclude: true, - compile: function (element, attrs) { - attrs.noErrorNotification = !! attrs.noErrorNotification; - - return function (scope, element, attrs, ngModel, transclude) { - if (!scope.submitApiMethod) { - throw new Error("submitApiMethod is required"); - } - - scope.ajaxForm.submitApiMethod = scope.submitApiMethod; - scope.ajaxForm.sendJsonPayload = scope.sendJsonPayload; - scope.ajaxForm.noErrorNotification = scope.noErrorNotification; - scope.ajaxForm.noSuccessNotification = scope.noSuccessNotification; - - scope.ajaxForm.data = {}; - - // if a model is supplied, initiate form data w/ model value - if (ngModel) { - var ngModelGetter = $parse(attrs.ngModel); // probably redundant, but I cannot find another way to - // get the ng model value here - scope.ajaxForm.data = ngModelGetter(scope.$parent); - } - - // on change of any input, change appropriate value in model, but only if requested - if (!scope.useCustomDataBinding) { - element.on('change', 'input,select', function () { - setFormValueFromInput(this); - }); - } - - // on submit call controller submit method - element.on('click', 'input[type=submit]', function () { - scope.ajaxForm.submitForm(); - }); - - // make sure child elements can access this directive's scope - transclude(scope, function(clone, scope) { - if (!scope.useCustomDataBinding) { - var $inputs = clone.find('input,select').not('[type=submit]'); - - // initialize form data to input values (include + * + * + * + *
ERROR!
+ * + * @deprecated + */ + +function piwikAjaxForm($parse) { + return { + restrict: 'A', + scope: { + submitApiMethod: '=', + sendJsonPayload: '=', + noErrorNotification: '=', + noSuccessNotification: '=', + useCustomDataBinding: '=' + }, + require: '?ngModel', + transclude: true, + compile: function piwikAjaxFormCompile(compileElement, compileAttrs) { + compileAttrs.noErrorNotification = !!compileAttrs.noErrorNotification; + return function piwikAjaxFormLink(scope, element, attrs, ngModel, transclude) { + if (!scope.submitApiMethod) { + throw new Error('submitApiMethod is required'); + } + + scope.ajaxForm = {}; + scope.ajaxForm.submitApiMethod = scope.submitApiMethod; + scope.ajaxForm.sendJsonPayload = scope.sendJsonPayload; + scope.ajaxForm.noErrorNotification = scope.noErrorNotification; + scope.ajaxForm.noSuccessNotification = scope.noSuccessNotification; + scope.ajaxForm.data = {}; // if a model is supplied, initiate form data w/ model value + + if (ngModel) { + // probably redundant, but I cannot find another way to get the ng model value here + var ngModelGetter = $parse(attrs.ngModel); + scope.ajaxForm.data = ngModelGetter(scope.$parent); + } + + var specialBindDirective = { + mounted: function mounted(el, binding) { + scope.ajaxForm.submitForm = binding.value.submitForm; + } + }; + var rootTemplate = "\n \n \n "; + var app = createVueApp({ + template: rootTemplate, + data: function data() { + return scope.ajaxForm; + }, + setup: function setup() { + var transcludeTarget = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(null); + return { + transcludeTarget: transcludeTarget + }; + } + }); + app.component('AjaxForm', AjaxForm); + app.directive('SpecialBindDirective', specialBindDirective); + var vm = app.mount(element[0]); + element.on('$destroy', function () { + app.unmount(); + }); + + function setFormValueFromInput(inputElement, skipScopeApply) { + var $ = angular.element; + var name = $(inputElement).attr('name'); + var val; + + if ($(inputElement).attr('type') === 'checkbox') { + val = $(inputElement).is(':checked'); + } else { + val = $(inputElement).val(); + } + + scope.ajaxForm.data[name] = val; + + if (!skipScopeApply) { + setTimeout(function () { + scope.$apply(); + }, 0); + } + } // on change of any input, change appropriate value in model, but only if requested + + + if (!scope.useCustomDataBinding) { + element.on('change', 'input,select', function (event) { + setFormValueFromInput(event.target); + }); + } // make sure child elements can access this directive's scope + + + transclude(scope, function (clone, transcludeScope) { + if (!transcludeScope.useCustomDataBinding) { + var $inputs = clone.find('input,select').not('[type=submit]'); // initialize form data to input values (include + * + * + * + *
ERROR!
+ * + * @deprecated + */ + +function piwikAjaxForm($parse) { + return { + restrict: 'A', + scope: { + submitApiMethod: '=', + sendJsonPayload: '=', + noErrorNotification: '=', + noSuccessNotification: '=', + useCustomDataBinding: '=', + }, + require: '?ngModel', + transclude: true, + compile: function piwikAjaxFormCompile(compileElement, compileAttrs) { + compileAttrs.noErrorNotification = !!compileAttrs.noErrorNotification; + + return function piwikAjaxFormLink(scope, element, attrs, ngModel, transclude) { + if (!scope.submitApiMethod) { + throw new Error('submitApiMethod is required'); + } + + scope.ajaxForm = {}; + scope.ajaxForm.submitApiMethod = scope.submitApiMethod; + scope.ajaxForm.sendJsonPayload = scope.sendJsonPayload; + scope.ajaxForm.noErrorNotification = scope.noErrorNotification; + scope.ajaxForm.noSuccessNotification = scope.noSuccessNotification; + + scope.ajaxForm.data = {}; + + // if a model is supplied, initiate form data w/ model value + if (ngModel) { + // probably redundant, but I cannot find another way to get the ng model value here + const ngModelGetter = $parse(attrs.ngModel); + scope.ajaxForm.data = ngModelGetter(scope.$parent); + } + + interface SpecialDirectiveBinding { + submitForm: () => void; + } + + const specialBindDirective = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + scope.ajaxForm.submitForm = binding.value.submitForm; + }, + }; + + const rootTemplate = ` + + + `; + + const app = createVueApp({ + template: rootTemplate, + data() { + return scope.ajaxForm; + }, + setup() { + const transcludeTarget = ref(null); + return { + transcludeTarget, + }; + }, + }); + app.component('AjaxForm', AjaxForm); + app.directive('SpecialBindDirective', specialBindDirective); + const vm = app.mount(element[0]); + + element.on('$destroy', () => { + app.unmount(); + }); + + function setFormValueFromInput(inputElement: HTMLElement, skipScopeApply?: boolean) { + const $ = angular.element; + const name = $(inputElement).attr('name'); + let val; + + if ($(inputElement).attr('type') === 'checkbox') { + val = $(inputElement).is(':checked'); + } else { + val = $(inputElement).val(); + } + + scope.ajaxForm.data[name] = val; + + if (!skipScopeApply) { + setTimeout(() => { + scope.$apply(); + }, 0); + } + } + + // on change of any input, change appropriate value in model, but only if requested + if (!scope.useCustomDataBinding) { + element.on('change', 'input,select', (event) => { + setFormValueFromInput(event.target); + }); + } + + // make sure child elements can access this directive's scope + transclude(scope, (clone, transcludeScope) => { + if (!transcludeScope.useCustomDataBinding) { + const $inputs = clone.find('input,select').not('[type=submit]'); + + // initialize form data to input values (include