Replace OC.Notification with toastify js (#15124)

Replace OC.Notification with toastify js
This commit is contained in:
John Molakvoæ 2019-06-07 08:24:15 +02:00 committed by GitHub
commit b968987ed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 348 additions and 264 deletions

View File

@ -8,4 +8,5 @@
@import 'fixes.scss';
@import 'mobile.scss';
@import 'tooltip.scss';
@import 'public.scss';
@import 'toast.scss';
@import 'public.scss';

49
core/css/toast.scss Normal file
View File

@ -0,0 +1,49 @@
.toastify.toast {
min-width: 200px;
background: none;
background-color: var(--color-main-background);
color: var(--color-main-text);
box-shadow: 0 0 6px 0 var(--color-box-shadow);
padding: 12px;
padding-right: 34px;
margin-top: 45px;
position: absolute;
z-index: 9000;
border-radius: var(--border-radius);
.toast-close {
position: absolute;
top: 0;
right: 0;
width: 38px;
opacity: 0.4;
padding: 12px;
@include icon-color('close', 'actions', $color-black, 2, true);
background-position: center;
background-repeat: no-repeat;
text-indent: 200%;
white-space: nowrap;
overflow: hidden;
&:hover, &:focus, &:active {
cursor: pointer;
opacity: 1;
}
}
}
.toastify.toastify-top {
right: 10px;
}
.toast-error {
border-left: 3px solid var(--color-error);
}
.toast-info {
border-left: 3px solid var(--color-primary);
}
.toast-warning {
border-left: 3px solid var(--color-warning);
}
.toast-success {
border-left: 3px solid var(--color-success);
}

26
core/js/dist/login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

54
core/js/dist/main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -571,7 +571,7 @@ describe('Core base tests', function() {
// fore show more apps icon since otherwise it would be hidden since no icons are available
clock.tick(1 * 1000);
$('#more-apps').show();
expect($navigation.is(':visible')).toEqual(false);
$toggle.click();
clock.tick(1 * 1000);
@ -900,75 +900,74 @@ describe('Core base tests', function() {
var hideSpy;
var clock;
var getInnerText = function($node) {
return $node.contents().filter(function(){
return this.nodeType === 3;
})[0].nodeValue;
}
beforeEach(function() {
clock = sinon.useFakeTimers();
showSpy = sinon.spy(OC.Notification, 'show');
showHtmlSpy = sinon.spy(OC.Notification, 'showHtml');
showSpy = sinon.spy(OCP.Toast, 'message');
hideSpy = sinon.spy(OC.Notification, 'hide');
$('#testArea').append('<div id="notification"></div>');
$('#testArea').append('<div id="content"></div>');
});
afterEach(function() {
showSpy.restore();
showHtmlSpy.restore();
hideSpy.restore();
// jump past animations
clock.tick(10000);
clock.restore();
$('#testArea .toastify').remove();
});
describe('showTemporary', function() {
it('shows a plain text notification with default timeout', function() {
var $row = OC.Notification.showTemporary('My notification test');
OC.Notification.showTemporary('My notification test');
expect(showSpy.calledOnce).toEqual(true);
expect(showSpy.firstCall.args[0]).toEqual('My notification test');
expect(showSpy.firstCall.args[1]).toEqual({isHTML: false, timeout: 7});
//expect(showSpy.firstCall.args[1]).toEqual({isHTML: false, timeout: 7});
var $row = $('#testArea .toastify');
expect($row).toBeDefined();
expect($row.text()).toEqual('My notification test');
expect(getInnerText($row)).toEqual('My notification test');
});
it('shows a HTML notification with default timeout', function() {
var $row = OC.Notification.showTemporary('<a>My notification test</a>', { isHTML: true });
OC.Notification.showTemporary('<a>My notification test</a>', { isHTML: true });
expect(showSpy.notCalled).toEqual(true);
expect(showHtmlSpy.calledOnce).toEqual(true);
expect(showHtmlSpy.firstCall.args[0]).toEqual('<a>My notification test</a>');
expect(showHtmlSpy.firstCall.args[1]).toEqual({isHTML: true, timeout: 7});
expect(showSpy.calledOnce).toEqual(true);
expect(showSpy.firstCall.args[0]).toEqual('<a>My notification test</a>');
expect(showSpy.firstCall.args[1].isHTML).toEqual(true)
var $row = $('#testArea .toastify');
expect($row).toBeDefined();
expect($row.text()).toEqual('My notification test');
expect(getInnerText($row)).toEqual('<a>My notification test</a>');
});
it('hides itself after 7 seconds', function() {
var $row = OC.Notification.showTemporary('');
OC.Notification.showTemporary('');
var $row = $('#testArea .toastify');
expect($row).toBeDefined();
// travel in time +7000 milliseconds
clock.tick(7000);
clock.tick(7500);
expect(hideSpy.calledOnce).toEqual(true);
expect(hideSpy.firstCall.args[0]).toEqual($row);
$row = $('#testArea .toastify');
expect($row.length).toEqual(0);
});
});
describe('show', function() {
it('hides itself after a given time', function() {
OC.Notification.show('', { timeout: 10 });
OC.Notification.showTemporary('', {timeout: 10});
// travel in time +9 seconds
clock.tick(9000);
var $row = $('#testArea .toastify');
expect($row).toBeDefined();
expect(hideSpy.notCalled).toEqual(true);
clock.tick(11500);
// travel in time +1 seconds
clock.tick(1000);
expect(hideSpy.calledOnce).toEqual(true);
});
it('does not hide itself after a given time if a timeout of 0 is defined', function() {
OC.Notification.show('', { timeout: 0 });
// travel in time +1000 seconds
clock.tick(1000000);
expect(hideSpy.notCalled).toEqual(true);
$row = $('#testArea .toastify');
expect($row.length).toEqual(0);
});
it('does not hide itself if no timeout given to show', function() {
OC.Notification.show('');
@ -984,92 +983,34 @@ describe('Core base tests', function() {
var $row2 = OC.Notification.showTemporary('Two', {timeout: 2});
var $row3 = OC.Notification.showTemporary('Three');
var $el = $('#notification');
var $rows = $el.find('.row');
var $el = $('#testArea');
var $rows = $el.find('.toastify');
expect($rows.length).toEqual(3);
expect($rows.eq(0).is($row1)).toEqual(true);
expect($rows.eq(0).is($row3)).toEqual(true);
expect($rows.eq(1).is($row2)).toEqual(true);
expect($rows.eq(2).is($row3)).toEqual(true);
expect($rows.eq(2).is($row1)).toEqual(true);
clock.tick(3000);
$rows = $el.find('.row');
$rows = $el.find('.toastify');
expect($rows.length).toEqual(2);
expect($rows.eq(0).is($row1)).toEqual(true);
expect($rows.eq(1).is($row3)).toEqual(true);
});
it('shows close button for error types', function() {
var $row = OC.Notification.showTemporary('One');
var $rowError = OC.Notification.showTemporary('Two', {type: 'error'});
expect($row.find('.close').length).toEqual(0);
expect($rowError.find('.close').length).toEqual(1);
// after clicking, row is gone
$rowError.find('.close').click();
var $rows = $('#notification').find('.row');
expect($rows.length).toEqual(1);
expect($rows.eq(0).is($row)).toEqual(true);
});
it('fades out the last notification but not the other ones', function() {
var fadeOutStub = sinon.stub($.fn, 'fadeOut');
var $row1 = OC.Notification.show('One', {type: 'error'});
var $row2 = OC.Notification.show('Two', {type: 'error'});
OC.Notification.showTemporary('Three', {timeout: 2});
var $el = $('#notification');
var $rows = $el.find('.row');
expect($rows.length).toEqual(3);
clock.tick(3000);
$rows = $el.find('.row');
expect($rows.length).toEqual(2);
$row1.find('.close').click();
clock.tick(1000);
expect(fadeOutStub.notCalled).toEqual(true);
$row2.find('.close').click();
clock.tick(1000);
expect(fadeOutStub.calledOnce).toEqual(true);
expect($el.is(':empty')).toEqual(false);
fadeOutStub.yield();
expect($el.is(':empty')).toEqual(true);
fadeOutStub.restore();
});
it('hides the first notification when calling hide without arguments', function() {
OC.Notification.show('One');
var $row2 = OC.Notification.show('Two');
spyOn(console, 'warn');
var $el = $('#notification');
var $rows = $el.find('.row');
expect($rows.length).toEqual(2);
OC.Notification.hide();
expect(console.warn).toHaveBeenCalled();
$rows = $el.find('.row');
expect($rows.length).toEqual(1);
expect($rows.eq(0).is($row2)).toEqual(true);
expect($rows.eq(0).is($row3)).toEqual(true);
expect($rows.eq(1).is($row1)).toEqual(true);
});
it('hides the given notification when calling hide with argument', function() {
var $row1 = OC.Notification.show('One');
var $row2 = OC.Notification.show('Two');
var $el = $('#notification');
var $rows = $el.find('.row');
var $el = $('#testArea');
var $rows = $el.find('.toastify');
expect($rows.length).toEqual(2);
OC.Notification.hide($row2);
clock.tick(3000);
$rows = $el.find('.row');
$rows = $el.find('.toastify');
expect($rows.length).toEqual(1);
expect($rows.eq(0).is($row1)).toEqual(true);
});

View File

@ -957,7 +957,7 @@ describe('OC.Share.ShareDialogView', function() {
var showTemporaryNotificationStub;
beforeEach(function() {
showTemporaryNotificationStub = sinon.stub(OC.Notification, 'show');
showTemporaryNotificationStub = sinon.stub(OC.Notification, 'showTemporary');
});
afterEach(function() {

View File

@ -21,24 +21,22 @@
import _ from 'underscore'
import $ from 'jquery'
import Toastify from 'toastify-js'
/**
* @todo Write documentation
* @deprecated 17.0.0 use OCP.Toast
* @namespace OC.Notification
*/
export default {
queuedNotifications: [],
updatableNotification: null,
getDefaultNotificationFunction: null,
/**
* @type Array<int>
* @description array of notification timers
*/
notificationTimers: [],
/**
* @param callback
* @todo Write documentation
* @deprecated 17.0.0 use OCP.Toast
*/
setDefault: function (callback) {
this.getDefaultNotificationFunction = callback;
@ -52,10 +50,11 @@ export default {
*
* @param {jQuery} [$row] notification row
* @param {Function} [callback] callback
* @deprecated 17.0.0 use OCP.Toast
*/
hide: function ($row, callback) {
var self = this;
var $notification = $('#notification');
var $notification = $('#content');
if (_.isFunction($row)) {
// first arg is the callback
@ -64,46 +63,23 @@ export default {
}
if (!$row) {
console.warn('Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification');
// assume that the row to be hidden is the first one
$row = $notification.find('.row:first');
}
if ($row && $notification.find('.row').length > 1) {
// remove the row directly
$row.remove();
if (callback) {
callback.call();
}
console.error('Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification');
return;
}
_.defer(function () {
// fade out is supposed to only fade when there is a single row
// however, some code might call hide() and show() directly after,
// which results in more than one element
// in this case, simply delete that one element that was supposed to
// fade out
//
// FIXME: remove once all callers are adjusted to only hide their own notifications
if ($notification.find('.row').length > 1) {
$row.remove();
return;
// remove the row directly
$row.each(function () {
$(this)[0].toastify.hideToast()
if (this === this.updatableNotification) {
this.updatableNotification = null
}
// else, fade out whatever was present
$notification.fadeOut('400', function () {
if (self.isHidden()) {
if (self.getDefaultNotificationFunction) {
self.getDefaultNotificationFunction.call();
}
}
if (callback) {
callback.call();
}
$notification.empty();
});
});
})
if (callback) {
callback.call()
}
if (this.getDefaultNotificationFunction) {
this.getDefaultNotificationFunction()
}
},
/**
@ -116,45 +92,14 @@ export default {
* @param {string} [options.type] notification type
* @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent)
* @return {jQuery} jQuery element for notification row
* @deprecated 17.0.0 use OCP.Toast
*/
showHtml: function (html, options) {
options = options || {};
_.defaults(options, {
timeout: 0
});
var self = this;
var $notification = $('#notification');
if (this.isHidden()) {
$notification.fadeIn().css('display', 'inline-block');
}
var $row = $('<div class="row"></div>');
if (options.type) {
$row.addClass('type-' + options.type);
}
if (options.type === 'error') {
// add a close button
var $closeButton = $('<a class="action close icon-close" href="#"></a>');
$closeButton.attr('alt', t('core', 'Dismiss'));
$row.append($closeButton);
$closeButton.one('click', function () {
self.hide($row);
return false;
});
$row.addClass('closeable');
}
$row.prepend(html);
$notification.append($row);
if (options.timeout > 0) {
// register timeout to vanish notification
this.notificationTimers.push(setTimeout(function () {
self.hide($row);
}, (options.timeout * 1000)));
}
return $row;
options = options || {}
options.showHtml = true
options.timeout = (options.timeout === 0) ? -1 : options.timeout
const toast = window.OCP.Toast.message(html, options)
return $(toast.toastElement)
},
/**
@ -165,9 +110,11 @@ export default {
* @param {string} [options.type] notification type
* @param {int} [options.timeout=0] timeout value, defaults to 0 (permanent)
* @return {jQuery} jQuery element for notification row
* @deprecated 17.0.0 use OCP.Toast
*/
show: function (text, options) {
return this.showHtml($('<div/>').text(text).html(), options);
const toast = window.OCP.Toast.message(text, options);
return $(toast.toastElement);
},
/**
@ -175,23 +122,14 @@ export default {
*
* @param {string} text Message to display
* @return {jQuery} JQuery element for notificaiton row
* @deprecated 17.0.0 use OCP.Toast
*/
showUpdate: function (text) {
var $notification = $('#notification');
// sanitise
var $html = $('<div/>').text(text).html();
// new notification
if (text && $notification.find('.row').length == 0) {
return this.showHtml($html);
if (this.updatableNotification) {
this.updatableNotification.hideToast();
}
var $row = $('<div class="row"></div>').prepend($html);
// just update html in notification
$notification.html($row);
return $row;
this.updatableNotification = OCP.Toast.message(text, {timeout: -1})
return $(this.updatableNotification.toastElement);
},
/**
@ -203,30 +141,21 @@ export default {
* @param {int} [options.timeout=7] timeout in seconds, if this is 0 it will show the message permanently
* @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false)
* @param {string} [options.type] notification type
* @deprecated 17.0.0 use OCP.Toast
*/
showTemporary: function (text, options) {
var defaults = {
isHTML: false,
timeout: 7
};
options = options || {};
// merge defaults with passed in options
_.defaults(options, defaults);
var $row;
if (options.isHTML) {
$row = this.showHtml(text, options);
} else {
$row = this.show(text, options);
}
return $row;
options = options || {}
options.timeout = options.timeout || 7;
const toast = window.OCP.Toast.message(text, options);
return $(toast.toastElement);
},
/**
* Returns whether a notification is hidden.
* @return {boolean}
* @deprecated 17.0.0 use OCP.Toast
*/
isHidden: function () {
return !$("#notification").find('.row').length;
return !$('#content').find('.toastify').length;
}
}

View File

@ -6,14 +6,16 @@ import * as Comments from './comments'
import * as InitialState from './initialstate'
import Loader from './loader'
import Collaboration from './collaboration'
import Toast from './toast'
import * as WhatsNew from './whatsnew'
/** @namespace OCP */
export default {
AppConfig,
Collaboration,
Comments,
InitialState,
Loader,
WhatsNew,
Collaboration
Toast,
WhatsNew
};

89
core/src/OCP/toast.js Normal file
View File

@ -0,0 +1,89 @@
/*
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Toastify from 'toastify-js'
const TOAST_TYPE_CLASES = {
error: 'toast-error',
info: 'toast-info',
warning: 'toast-warning',
success: 'toast-success',
permanent: 'permanent'
}
const Toast = {
success(text, options = {}) {
options.type = 'success';
return this.message(text, options)
},
warning(text, options = {}) {
options.type = 'warning';
return this.message(text, options)
},
error(text, options = {}) {
options.type = 'error';
return this.message(text, options)
},
info(text, options = {}) {
options.type = 'info';
return this.message(text, options)
},
message(text, options) {
options = options || {};
_.defaults(options, {
timeout: 7,
showHtml: false,
type: undefined,
close: true,
callback: () => {}
});
if (!options.showHtml) {
text = $('<div/>').text(text).html()
}
let classes = ''
if (options.type) {
classes = TOAST_TYPE_CLASES[options.type]
}
const toast = Toastify({
text: text,
duration: options.timeout ? options.timeout*1000 : null,
callback: options.callback,
close: options.close,
gravity: 'top',
selector: !window.TESTING ? 'content' : 'testArea',
positionLeft: false,
backgroundColor: '',
className: 'toast ' + classes
})
toast.showToast()
// add toastify object to the element for reference in legacy OC.Notification
toast.toastElement.toastify = toast;
return toast
}
}
export default Toast

54
package-lock.json generated
View File

@ -2330,6 +2330,34 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
},
"regexpu-core": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
"integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=",
"requires": {
"regenerate": "^1.2.1",
"regjsgen": "^0.2.0",
"regjsparser": "^0.1.4"
}
},
"regjsgen": {
"version": "0.2.0",
"resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc="
},
"regjsparser": {
"version": "0.1.5",
"resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
"requires": {
"jsesc": "~0.5.0"
}
}
}
},
@ -3782,6 +3810,21 @@
"optimist": "^0.6.1",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4"
},
"dependencies": {
"async": {
"version": "2.6.1",
"resolved": "http://registry.npmjs.org/async/-/async-2.6.1.tgz",
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
"requires": {
"lodash": "^4.17.10"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"handlebars-loader": {
@ -4740,9 +4783,9 @@
}
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mississippi": {
"version": "3.0.0",
@ -6950,6 +6993,11 @@
"repeat-string": "^1.6.1"
}
},
"toastify-js": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.5.0.tgz",
"integrity": "sha512-tupU/X7DqwxYxTgT6n9SSEZLIGuwL1hFWg9uGQOzi8G04FLXoziw0GRF/TmuARrSQQCfIarfzoKEdDPG14Pr3Q=="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",

View File

@ -48,6 +48,7 @@
"query-string": "^5.1.1",
"snap.js": "^2.0.9",
"strengthify": "git+https://github.com/MorrisJobke/strengthify.git#0.5.8",
"toastify-js": "^1.5.0",
"underscore": "^1.9.1",
"v-tooltip": "^2.0.2",
"vue": "^2.6.10",