feat(systemtags): add colors in bulk tagging action

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
This commit is contained in:
skjnldsv 2024-11-15 11:07:37 +01:00
parent adf8a454dd
commit cb472bebfe
11 changed files with 487 additions and 320 deletions

View file

@ -11,7 +11,7 @@
<summary>Collaborative tagging functionality which shares tags among people.</summary>
<description>Collaborative tagging functionality which shares tags among people. Great for teams.
(If you are a provider with a multi-tenancy installation, it is advised to deactivate this app as tags are shared.)</description>
<version>1.21.0</version>
<version>1.21.1</version>
<licence>agpl</licence>
<author>Vincent Petry</author>
<author>Joas Schilling</author>

View file

@ -31,34 +31,57 @@
</div>
<!-- Tags list -->
<div class="systemtags-picker__tags"
<ul class="systemtags-picker__tags"
data-cy-systemtags-picker-tags>
<NcCheckboxRadioSwitch v-for="tag in filteredTags"
<li v-for="tag in filteredTags"
:key="tag.id"
:label="tag.displayName"
:checked="isChecked(tag)"
:indeterminate="isIndeterminate(tag)"
:disabled="!tag.canAssign"
:data-cy-systemtags-picker-tag="tag.id"
class="systemtags-picker__tag"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>
<NcButton v-if="canCreateTag"
:disabled="status === Status.CREATING_TAG"
alignment="start"
class="systemtags-picker__tag-create"
native-type="submit"
type="tertiary"
data-cy-systemtags-picker-button-create
@click="onNewTag">
{{ input.trim() }}<br>
<span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
<template #icon>
<PlusIcon />
</template>
</NcButton>
</div>
:style="tagListStyle(tag)"
class="systemtags-picker__tag">
<NcCheckboxRadioSwitch :checked="isChecked(tag)"
:disabled="!tag.canAssign"
:indeterminate="isIndeterminate(tag)"
:label="tag.displayName"
class="systemtags-picker__tag-checkbox"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>
<!-- Color picker -->
<NcColorPicker :data-cy-systemtags-picker-tag-color="tag.id"
:value="`#${tag.color}`"
:shown.sync="openedPicker"
class="systemtags-picker__tag-color"
@update:value="onColorChange(tag, $event)"
@submit="openedPicker = false">
<NcButton :aria-label="t('systemtags', 'Change tag color')" type="tertiary">
<template #icon>
<CircleIcon v-if="tag.color" :size="24" fill-color="var(--color-circle-icon)" />
<CircleOutlineIcon v-else :size="24" fill-color="var(--color-circle-icon)" />
<PencilIcon />
</template>
</NcButton>
</NcColorPicker>
</li>
<!-- Create new tag -->
<li>
<NcButton v-if="canCreateTag"
:disabled="status === Status.CREATING_TAG"
alignment="start"
class="systemtags-picker__tag-create"
native-type="submit"
type="tertiary"
data-cy-systemtags-picker-button-create
@click="onNewTag">
{{ input.trim() }}<br>
<span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
<template #icon>
<PlusIcon />
</template>
</NcButton>
</li>
</ul>
<!-- Note -->
<div class="systemtags-picker__note">
@ -110,19 +133,28 @@ import escapeHTML from 'escape-html'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import CheckIcon from 'vue-material-design-icons/CheckCircle.vue'
import CircleIcon from 'vue-material-design-icons/Circle.vue'
import CircleOutlineIcon from 'vue-material-design-icons/CircleOutline.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag } from '../services/api'
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api'
import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils'
import logger from '../services/logger'
const mainBackgroundColor = getComputedStyle(document.body)
.getPropertyValue('--color-main-background')
.replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')
type TagListCount = {
string: number
}
@ -139,15 +171,19 @@ export default defineComponent({
components: {
CheckIcon,
CircleIcon,
CircleOutlineIcon,
NcButton,
NcCheckboxRadioSwitch,
// eslint-disable-next-line vue/no-unused-components
NcChip,
NcColorPicker,
NcDialog,
NcEmptyContent,
NcLoadingIcon,
NcNoteCard,
NcTextField,
PencilIcon,
PlusIcon,
TagIcon,
},
@ -171,6 +207,7 @@ export default defineComponent({
return {
status: Status.BASE,
opened: true,
openedPicker: false,
input: '',
tags: [] as TagWithId[],
@ -329,7 +366,14 @@ export default defineComponent({
// Format & sanitize a tag chip for v-html tag rendering
formatTagChip(tag: TagWithId): string {
const chip = this.$refs.chip as NcChip
const chipHtml = chip.$el.outerHTML
const chipCloneEl = chip.$el.cloneNode(true) as HTMLElement
if (tag.color) {
const style = this.tagListStyle(tag)
Object.entries(style).forEach(([key, value]) => {
chipCloneEl.style.setProperty(key, value)
})
}
const chipHtml = chipCloneEl.outerHTML
return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName)))
},
@ -345,6 +389,11 @@ export default defineComponent({
return tag.displayName
},
onColorChange(tag: TagWithId, color: string) {
tag.color = color.replace('#', '')
updateTag(tag)
},
isChecked(tag: TagWithId): boolean {
return tag.displayName in this.tagList
&& this.tagList[tag.displayName] === this.nodes.length
@ -480,6 +529,28 @@ export default defineComponent({
showInfo(t('systemtags', 'File tags modification canceled'))
this.$emit('close', null)
},
tagListStyle(tag: TagWithId): Record<string, string> {
// No color, no style
if (!tag.color) {
return {
// See inline system tag color
'--color-circle-icon': 'var(--color-text-maxcontrast)',
}
}
// Make the checkbox color the same as the tag color
// as well as the circle icon color picker
const primaryElement = elementColor(`#${tag.color}`, `#${mainBackgroundColor}`)
const textColor = invertTextColor(primaryElement) ? '#000000' : '#ffffff'
return {
'--color-circle-icon': 'var(--color-primary-element)',
'--color-primary': primaryElement,
'--color-primary-text': textColor,
'--color-primary-element': primaryElement,
'--color-primary-element-text': textColor,
}
},
},
})
</script>
@ -506,6 +577,48 @@ export default defineComponent({
gap: var(--default-grid-baseline);
display: flex;
flex-direction: column;
li {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
// Make switch full width
:deep(.checkbox-radio-switch) {
width: 100%;
.checkbox-content {
// adjust width
max-width: none;
// recalculate padding
box-sizing: border-box;
min-height: calc(var(--default-grid-baseline) * 2 + var(--default-clickable-area));
}
}
}
.systemtags-picker__tag-color button {
margin-inline-start: calc(var(--default-grid-baseline) * 2);
span.pencil-icon {
display: none;
color: var(--color-main-text);
}
&:focus,
&:hover,
&[aria-expanded='true'] {
.pencil-icon {
display: block;
}
.circle-icon,
.circle-outline-icon {
display: none;
}
}
}
.systemtags-picker__tag-create {
:deep(span) {
text-align: start;

View file

@ -22,7 +22,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 22px; // min-size - 2 * 5px padding
line-height: 20px; // min-size - 2 * 5px padding - 2 * 1px border
text-align: center;
&--more {
@ -34,6 +34,14 @@
& + .files-list__system-tag {
margin-inline-start: 5px;
}
// With color
&[data-systemtag-color] {
border-color: var(--systemtag-color);
color: var(--systemtag-color);
border-width: 2px;
line-height: 18px; // min-size - 2 * 5px padding - 2 * 2px border
}
}
@media (min-width: 512px) {

View file

@ -3,10 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { TagWithId } from './types'
declare module '@nextcloud/event-bus' {
interface NextcloudEvents {
'systemtags:node:updated': Node
'systemtags:tag:deleted': TagWithId
'systemtags:tag:updated': TagWithId
'systemtags:tag:created': TagWithId
}
}

View file

@ -3,10 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { action } from './inlineSystemTagsAction'
import { describe, expect, test } from 'vitest'
import { File, Permission, View, FileAction } from '@nextcloud/files'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { emit, subscribe } from '@nextcloud/event-bus'
import { File, Permission, View, FileAction } from '@nextcloud/files'
import { setNodeSystemTags } from '../utils'
import * as serviceTagApi from '../services/api'
import { set } from 'lodash'
const view = {
id: 'files',
@ -53,6 +55,13 @@ describe('Inline system tags action conditions tests', () => {
})
describe('Inline system tags action render tests', () => {
beforeEach(() => {
vi.spyOn(serviceTagApi, 'fetchTags').mockImplementation(async () => {
return []
})
})
test('Render something even when Node does not have system tags', async () => {
const file = new File({
id: 1,
@ -165,7 +174,9 @@ describe('Inline system tags action render tests', () => {
// Subscribe to the event
const eventPromise = new Promise((resolve) => {
subscribe('systemtags:node:updated', resolve)
subscribe('systemtags:node:updated', () => {
setTimeout(resolve, 100)
})
})
// Change tags

View file

@ -3,18 +3,38 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { TagWithId } from '../types'
import { FileAction } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { t } from '@nextcloud/l10n'
import '../css/fileEntryInlineSystemTags.scss'
import { elementColor, isDarkModeEnabled } from '../utils/colorUtils'
import { fetchTags } from '../services/api'
import { getNodeSystemTags } from '../utils'
import logger from '../services/logger'
// Init tag cache
const cache: TagWithId[] = []
const renderTag = function(tag: string, isMore = false): HTMLElement {
const tagElement = document.createElement('li')
tagElement.classList.add('files-list__system-tag')
tagElement.setAttribute('data-systemtag-name', tag)
tagElement.textContent = tag
// Set the color if it exists
const cachedTag = cache.find((t) => t.displayName === tag)
if (cachedTag?.color) {
// Make sure contrast is good and follow WCAG guidelines
const mainBackgroundColor = getComputedStyle(document.body)
.getPropertyValue('--color-main-background')
.replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')
const primaryElement = elementColor(`#${cachedTag.color}`, `#${mainBackgroundColor}`)
tagElement.style.setProperty('--systemtag-color', primaryElement)
tagElement.setAttribute('data-systemtag-color', 'true')
}
if (isMore) {
tagElement.classList.add('files-list__system-tag--more')
}
@ -35,6 +55,17 @@ const renderInline = async function(node: Node): Promise<HTMLElement> {
return systemTagsElement
}
// Fetch the tags if the cache is empty
if (cache.length === 0) {
try {
// Best would be to support attributes from webdav,
// but currently the library does not support it
cache.push(...await fetchTags())
} catch (error) {
logger.error('Failed to fetch tags', { error })
}
}
systemTagsElement.append(renderTag(tags[0]))
if (tags.length === 2) {
// Special case only two tags:
@ -84,6 +115,7 @@ export const action = new FileAction({
order: 0,
})
// Update the system tags html when the node is updated
const updateSystemTagsHtml = function(node: Node) {
renderInline(node).then((systemTagsHtml) => {
document.querySelectorAll(`[data-systemtags-fileid="${node.fileid}"]`).forEach((element) => {
@ -92,4 +124,29 @@ const updateSystemTagsHtml = function(node: Node) {
})
}
// Add and remove tags from the cache
const addTag = function(tag: TagWithId) {
cache.push(tag)
}
const removeTag = function(tag: TagWithId) {
cache.splice(cache.findIndex((t) => t.id === tag.id), 1)
}
const updateTag = function(tag: TagWithId) {
const index = cache.findIndex((t) => t.id === tag.id)
if (index !== -1) {
cache[index] = tag
}
updateSystemTagsColorAttribute(tag)
}
// Update the color attribute of the system tags
const updateSystemTagsColorAttribute = function(tag: TagWithId) {
document.querySelectorAll(`[data-systemtag-name="${tag.displayName}"]`).forEach((element) => {
(element as HTMLElement).style.setProperty('--systemtag-color', `#${tag.color}`)
})
}
// Subscribe to the events
subscribe('systemtags:node:updated', updateSystemTagsHtml)
subscribe('systemtags:tag:created', addTag)
subscribe('systemtags:tag:deleted', removeTag)
subscribe('systemtags:tag:updated', updateTag)

View file

@ -13,9 +13,10 @@ import { t } from '@nextcloud/l10n'
import { davClient } from './davClient.js'
import { formatTag, parseIdFromLocation, parseTags } from '../utils'
import { logger } from '../logger.js'
import { emit } from '@nextcloud/event-bus'
export const fetchTagsPayload = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
@ -23,6 +24,7 @@ export const fetchTagsPayload = `<?xml version="1.0"?>
<oc:user-assignable />
<oc:can-assign />
<d:getetag />
<nc:color />
</d:prop>
</d:propfind>`
@ -81,6 +83,7 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
})
const contentLocation = headers.get('content-location')
if (contentLocation) {
emit('systemtags:tag:created', tag)
return parseIdFromLocation(contentLocation)
}
logger.error(t('systemtags', 'Missing "Content-Location" header'))
@ -98,12 +101,13 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
export const updateTag = async (tag: TagWithId): Promise<void> => {
const path = '/systemtags/' + tag.id
const data = `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
<oc:display-name>${tag.displayName}</oc:display-name>
<oc:user-visible>${tag.userVisible}</oc:user-visible>
<oc:user-assignable>${tag.userAssignable}</oc:user-assignable>
<nc:color>${tag.color}</nc:color>
</d:prop>
</d:set>
</d:propertyupdate>`
@ -113,6 +117,7 @@ export const updateTag = async (tag: TagWithId): Promise<void> => {
method: 'PROPPATCH',
data,
})
emit('systemtags:tag:updated', tag)
} catch (error) {
logger.error(t('systemtags', 'Failed to update tag'), { error })
throw new Error(t('systemtags', 'Failed to update tag'))
@ -123,6 +128,7 @@ export const deleteTag = async (tag: TagWithId): Promise<void> => {
const path = '/systemtags/' + tag.id
try {
await davClient.deleteFile(path)
emit('systemtags:tag:deleted', tag)
} catch (error) {
logger.error(t('systemtags', 'Failed to delete tag'), { error })
throw new Error(t('systemtags', 'Failed to delete tag'))

View file

@ -8,6 +8,8 @@ export interface BaseTag {
userVisible: boolean
userAssignable: boolean
readonly canAssign: boolean // Computed server-side
etag?: string
color?: string
}
export type Tag = BaseTag & {

View file

@ -0,0 +1,193 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Color from 'color'
type hexColor = `#${string & (
`${string}${string}${string}` |
`${string}${string}${string}${string}${string}${string}`
)}`;
/**
* Is the current theme dark?
*/
export function isDarkModeEnabled() {
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches
const darkModeSetting = document.body.getAttribute('data-themes')?.includes('dark')
return darkModeSetting || darkModePreference || false
}
/**
* Is the current theme high contrast?
*/
export function isHighContrastModeEnabled() {
const highContrastPreference = window.matchMedia('(forced-colors: active)').matches
const highContrastSetting = document.body.getAttribute('data-themes')?.includes('highcontrast')
return highContrastSetting || highContrastPreference || false
}
/**
* Should we invert the text on this background color?
* @param color RGB color value as a hex string
* @return boolean
*/
export function invertTextColor(color: hexColor): boolean {
return colorContrast(color, '#ffffff') < 4.5
}
/**
* Is this color too bright?
* @param color RGB color value as a hex string
* @return boolean
*/
export function isBrightColor(color: hexColor): boolean {
return calculateLuma(color) > 0.6
}
/**
* Get color for on-page elements
* theme color by default, grey if theme color is too bright.
* @param color the color to contrast against, e.g. #ffffff
* @param backgroundColor the background color to contrast against, e.g. #000000
*/
export function elementColor(
color: hexColor,
backgroundColor: hexColor,
): hexColor {
const brightBackground = isBrightColor(backgroundColor)
const blurredBackground = mix(
backgroundColor,
brightBackground ? color : '#ffffff',
66,
)
let contrast = colorContrast(color, blurredBackground)
const minContrast = isHighContrastModeEnabled() ? 5.6 : 3.2
let iteration = 0
let result = color
const epsilon = 1.0 / 255.0
while (contrast < minContrast && iteration++ < 100) {
const hsl = hexToHSL(result)
const l = Math.max(
0,
Math.min(255, hsl.l + (brightBackground ? -epsilon : epsilon)),
)
result = hslToHex({ h: hsl.h, s: hsl.s, l })
contrast = colorContrast(result, blurredBackground)
}
return result
}
/**
* Get color for on-page text:
* black if background is bright, white if background is dark.
* @param color1 the color to contrast against, e.g. #ffffff
* @param color2 the background color to contrast against, e.g. #000000
* @param factor the factor to mix the colors between -100 and 100, e.g. 66
*/
export function mix(color1: hexColor, color2: hexColor, factor: number): hexColor {
if (factor < -100 || factor > 100) {
throw new RangeError('Factor must be between -100 and 100')
}
return new Color(color2).mix(new Color(color1), (factor + 100) / 200).hex()
}
/**
* Lighten a color by a factor
* @param color the color to lighten, e.g. #000000
* @param factor the factor to lighten the color by between -100 and 100, e.g. -41
*/
export function lighten(color: hexColor, factor: number): hexColor {
if (factor < -100 || factor > 100) {
throw new RangeError('Factor must be between -100 and 100')
}
return new Color(color).lighten((factor + 100) / 200).hex()
}
/**
* Darken a color by a factor
* @param color the color to darken, e.g. #ffffff
* @param factor the factor to darken the color by between -100 and 100, e.g. 32
*/
export function darken(color: hexColor, factor: number): hexColor {
if (factor < -100 || factor > 100) {
throw new RangeError('Factor must be between -100 and 100')
}
return new Color(color).darken((factor + 100) / 200).hex()
}
/**
* Calculate the luminance of a color
* @param color the color to calculate the luminance of, e.g. #ffffff
*/
export function calculateLuminance(color: hexColor): number {
return hexToHSL(color).l
}
/**
* Calculate the luma of a color
* @param color the color to calculate the luma of, e.g. #ffffff
*/
export function calculateLuma(color: hexColor): number {
const rgb = hexToRGB(color).map((value) => {
value /= 255
return value <= 0.03928
? value / 12.92
: Math.pow((value + 0.055) / 1.055, 2.4)
})
const [red, green, blue] = rgb
return 0.2126 * red + 0.7152 * green + 0.0722 * blue
}
/**
* Calculate the contrast between two colors
* @param color1 the first color to calculate the contrast of, e.g. #ffffff
* @param color2 the second color to calculate the contrast of, e.g. #000000
*/
export function colorContrast(color1: hexColor, color2: hexColor): number {
const luminance1 = calculateLuma(color1) + 0.05
const luminance2 = calculateLuma(color2) + 0.05
return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2)
}
/**
* Convert hex color to RGB
* @param color RGB color value as a hex string
*/
export function hexToRGB(color: hexColor): [number, number, number] {
return new Color(color).rgb().array()
}
/**
* Convert RGB color to hex
* @param color RGB color value as a hex string
*/
export function hexToHSL(color: hexColor): { h: number; s: number; l: number } {
const hsl = new Color(color).hsl()
return { h: hsl.color[0], s: hsl.color[1], l: hsl.color[2] }
}
/**
* Convert HSL color to hex
* @param hsl HSL color value as an object
* @param hsl.h hue
* @param hsl.s saturation
* @param hsl.l lightness
*/
export function hslToHex(hsl: { h: number; s: number; l: number }): hexColor {
return new Color(hsl).hex()
}
/**
* Convert RGB color to hex
* @param r red
* @param g green
* @param b blue
*/
export function rgbToHex(r: number, g: number, b: number): hexColor {
const hex = ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1)
return `#${hex}`
}

342
package-lock.json generated
View file

@ -42,6 +42,7 @@
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
"color": "^4.2.3",
"core-js": "^3.38.1",
"davclient.js": "github:owncloud/davclient.js.git#0.2.2",
"debounce": "^2.1.0",
@ -5378,26 +5379,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5471,26 +5452,6 @@
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/jest-dom/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
@ -5617,26 +5578,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@testing-library/vue/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@testing-library/vue/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/vue/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -9179,6 +9120,47 @@
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@ -10141,26 +10123,6 @@
"node": ">=8"
}
},
"node_modules/cypress/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/cypress/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/cypress/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -12444,28 +12406,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/eslint/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/eslint/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -16125,24 +16065,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jake/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/jake/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/jake/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -17173,26 +17095,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/log-symbols/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/log-symbols/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/log-symbols/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -17251,26 +17153,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/log-update/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
@ -20468,13 +20350,6 @@
"postcss": "^8.2.9"
}
},
"node_modules/postcss-values-parser/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/precinct": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.1.2.tgz",
@ -20915,24 +20790,6 @@
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/qrcode/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/qrcode/node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@ -22765,6 +22622,21 @@
"integrity": "sha512-+OmPgi01yHK/bRNQDoehUcV8fqs9nNJkG2DoWCnnLvj0lmowab7BH3v9776BG0y7dGEOLh0F7mfd37k+ht26Yw==",
"license": "MIT"
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/sinon": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.7.tgz",
@ -22823,26 +22695,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/slice-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -24250,26 +24102,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/table/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/table/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/table/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -24979,26 +24811,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ts-loader/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/ts-loader/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-loader/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -27496,26 +27308,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi-cjs/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -27532,26 +27324,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View file

@ -73,6 +73,7 @@
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
"color": "^4.2.3",
"core-js": "^3.38.1",
"davclient.js": "github:owncloud/davclient.js.git#0.2.2",
"debounce": "^2.1.0",