refactor(core): migrate OC.msg away from jQuery

Make the class jQuery free to be able to drop it as a dependency.
Also added some unit tests for it.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-01-16 12:07:11 +01:00
parent 6a67456574
commit f175e421b3
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
3 changed files with 335 additions and 99 deletions

View file

@ -1,99 +0,0 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
/**
* A little class to manage a status field for a "saving" process.
* It can be used to display a starting message (e.g. "Saving...") and then
* replace it with a green success message or a red error message.
*
* @namespace OC.msg
*/
export default {
/**
* Displayes a "Saving..." message in the given message placeholder
*
* @param {object} selector Placeholder to display the message in
*/
startSaving(selector) {
this.startAction(selector, t('core', 'Saving …'))
},
/**
* Displayes a custom message in the given message placeholder
*
* @param {object} selector Placeholder to display the message in
* @param {string} message Plain text message to display (no HTML allowed)
*/
startAction(selector, message) {
$(selector).text(message)
.removeClass('success')
.removeClass('error')
.stop(true, true)
.show()
},
/**
* Displayes an success/error message in the given selector
*
* @param {object} selector Placeholder to display the message in
* @param {object} response Response of the server
* @param {object} response.data Data of the servers response
* @param {string} response.data.message Plain text message to display (no HTML allowed)
* @param {string} response.status is being used to decide whether the message
* is displayed as an error/success
*/
finishedSaving(selector, response) {
this.finishedAction(selector, response)
},
/**
* Displayes an success/error message in the given selector
*
* @param {object} selector Placeholder to display the message in
* @param {object} response Response of the server
* @param {object} response.data Data of the servers response
* @param {string} response.data.message Plain text message to display (no HTML allowed)
* @param {string} response.status is being used to decide whether the message
* is displayed as an error/success
*/
finishedAction(selector, response) {
if (response.status === 'success') {
this.finishedSuccess(selector, response.data.message)
} else {
this.finishedError(selector, response.data.message)
}
},
/**
* Displayes an success message in the given selector
*
* @param {object} selector Placeholder to display the message in
* @param {string} message Plain text success message to display (no HTML allowed)
*/
finishedSuccess(selector, message) {
$(selector).text(message)
.addClass('success')
.removeClass('error')
.stop(true, true)
.delay(3000)
.fadeOut(900)
.show()
},
/**
* Displayes an error message in the given selector
*
* @param {object} selector Placeholder to display the message in
* @param {string} message Plain text error message to display (no HTML allowed)
*/
finishedError(selector, message) {
$(selector).text(message)
.addClass('error')
.removeClass('success')
.show()
},
}

195
core/src/OC/msg.spec.ts Normal file
View file

@ -0,0 +1,195 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { afterAll, describe, expect, it, test, vi } from 'vitest'
import msg from './msg.ts'
describe('start action', () => {
it('sets the message text content', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
msg.startAction(selector, 'the message')
const el = document.querySelector(selector)
expect(el).not.toBeNull()
expect(el!.textContent).toBe('the message')
})
it('removes old classes', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" class="success"></div>'
const el = document.querySelector(selector)
expect(el).not.toBeNull()
expect(el!.classList.contains('success')).toBe(true)
msg.startAction(selector, 'the message')
expect(el!.classList.contains('success')).toBe(false)
})
it('sets element visible', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" style="display: none;"></div>'
const el = document.querySelector(selector) as HTMLElement
expect(el).not.toBeNull()
expect(el.style.display).toBe('none')
msg.startAction(selector, 'the message')
expect(el.style.display).toBe('block')
})
})
test('start saving message', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
msg.startSaving(selector)
const el = document.querySelector(selector)
expect(el).not.toBeNull()
expect(el!.textContent).toBe('Saving …')
})
describe('finish with error', () => {
it('sets the message text content', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedError(selector, 'error message')
expect(el!.textContent).toBe('error message')
})
it('adds error class', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedError(selector, 'error message')
expect(el!.classList.contains('error')).toBe(true)
})
it('removes old classes', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" class="success"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedError(selector, 'error message')
expect(el!.classList.contains('success')).toBe(false)
})
it('sets element visible', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" style="display: none;"></div>'
const el = document.querySelector(selector) as HTMLElement
msg.startSaving(selector)
msg.finishedError(selector, 'error message')
expect(el.style.display).toBe('block')
})
})
describe('finish with success', () => {
afterAll(() => vi.useRealTimers())
it('sets the message text content', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedSuccess(selector, 'success message')
expect(el!.textContent).toBe('success message')
})
it('adds success class', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedSuccess(selector, 'success message')
expect(el!.classList.contains('success')).toBe(true)
})
it('removes old classes', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" class="error"></div>'
const el = document.querySelector(selector)
msg.startSaving(selector)
msg.finishedSuccess(selector, 'success message')
expect(el!.classList.contains('error')).toBe(false)
})
it('sets element visible', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder" style="display: none;"></div>'
const el = document.querySelector(selector) as HTMLElement
msg.startSaving(selector)
msg.finishedSuccess(selector, 'success message')
expect(el.style.display).toBe('block')
})
it('fades out element', () => {
vi.useFakeTimers()
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const el = document.querySelector(selector) as HTMLElement
msg.startSaving(selector)
msg.finishedSuccess(selector, 'success message')
expect(el!.style.display).toBe('block')
vi.advanceTimersByTime(3900)
expect(el.style.display).toBe('none')
})
})
describe('finished action', () => {
it('calls finishedSuccess on success response', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const finishedSuccessSpy = vi.spyOn(msg, 'finishedSuccess')
const response = { data: { message: 'all good' }, status: 'success' }
msg.finishedAction(selector, response)
expect(finishedSuccessSpy).toHaveBeenCalledWith(selector, 'all good')
})
it('calls finishedError on error response', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const finishedErrorSpy = vi.spyOn(msg, 'finishedError')
const response = { data: { message: 'something went wrong' }, status: 'error' }
msg.finishedAction(selector, response)
expect(finishedErrorSpy).toHaveBeenCalledWith(selector, 'something went wrong')
})
})
test('finished saving delegates to finished action', () => {
const selector = '#msg-placeholder'
document.body.innerHTML = '<div id="msg-placeholder"></div>'
const finishedActionSpy = vi.spyOn(msg, 'finishedAction')
const response = { data: { message: 'done saving' }, status: 'success' }
msg.finishedSaving(selector, response)
expect(finishedActionSpy).toHaveBeenCalledWith(selector, response)
})

140
core/src/OC/msg.ts Normal file
View file

@ -0,0 +1,140 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { t } from '@nextcloud/l10n'
/**
* A little class to manage a status field for a "saving" process.
* It can be used to display a starting message (e.g. "Saving...") and then
* replace it with a green success message or a red error message.
*/
export default {
/**
* Displayes a "Saving..." message in the given message placeholder
*
* @param selector - Query selectior for the element to display the message in
*/
startSaving(selector: string) {
this.startAction(selector, t('core', 'Saving …'))
},
/**
* Displayes a custom message in the given message placeholder
*
* @param selector - Query selectior for the element to display the message in
* @param message - Plain text message to display (no HTML allowed)
*/
startAction(selector: string, message: string) {
const el = document.querySelector(selector)
if (!el || !(el instanceof HTMLElement)) {
return
}
el.textContent = message
el.classList.remove('success')
el.classList.remove('error')
el.getAnimations?.().forEach((animation) => animation.cancel())
el.style.display = 'block'
},
/**
* Displayes an success/error message in the given selector
*
* @param selector - Query selectior for the element to display the message in
* @param response - Response of the server
* @param response.data - Data of the servers response
* @param response.data.message - Plain text message to display (no HTML allowed)
* @param response.status - is being used to decide whether the message is displayed as an error/success
*/
finishedSaving(selector: string, response: { data: { message: string }, status: string }) {
this.finishedAction(selector, response)
},
/**
* Displayes an success/error message in the given selector
*
* @param selector - Query selector for the element to display the message in
* @param response - Response of the server
* @param response.data - Data of the servers response
* @param response.data.message - Plain text message to display (no HTML allowed)
* @param response.status . Is being used to decide whether the message is displayed as an error/success
*/
finishedAction(selector: string, response: { data: { message: string }, status: string }) {
if (response.status === 'success') {
this.finishedSuccess(selector, response.data.message)
} else {
this.finishedError(selector, response.data.message)
}
},
/**
* Displayes an success message in the given selector
*
* @param selector - Query selector for the element to display the message in
* @param message - Plain text success message to display (no HTML allowed)
*/
finishedSuccess(selector: string, message: string) {
const el = document.querySelector(selector)
if (!el || !(el instanceof HTMLElement)) {
return
}
el.textContent = message
el.classList.remove('error')
el.classList.add('success')
el.getAnimations?.().forEach((animation) => animation.cancel())
window.setTimeout(fadeOut, 3000)
el.style.display = 'block'
/**
* Fades out the message element
*/
function fadeOut() {
if (!el || !(el instanceof HTMLElement)) {
return
}
const animation = el.animate?.(
[
{ opacity: 1 },
{ opacity: 0 },
],
{
duration: 900,
fill: 'forwards',
},
)
if (animation) {
animation.addEventListener('finish', () => {
el.style.display = 'none'
})
} else {
window.setTimeout(() => {
el.style.display = 'none'
}, 900)
}
}
},
/**
* Displayes an error message in the given selector
*
* @param selector - Query selector for the element to display the message in
* @param message - Plain text error message to display (no HTML allowed)
*/
finishedError(selector: string, message: string) {
const el = document.querySelector(selector)
if (!el || !(el instanceof HTMLElement)) {
return
}
el.textContent = message
el.classList.remove('success')
el.classList.add('error')
el.style.display = 'block'
},
}