Merge pull request #49295 from nextcloud/feat/tags-colors

This commit is contained in:
John Molakvoæ 2024-12-06 10:43:01 +01:00 committed by GitHub
commit 9684c3d2d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 857 additions and 454 deletions

View file

@ -13,6 +13,7 @@ use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;
use OCP\SystemTag\TagNotFoundException;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
@ -86,12 +87,13 @@ class SystemTagNode implements \Sabre\DAV\ICollection {
* @param string $name new tag name
* @param bool $userVisible user visible
* @param bool $userAssignable user assignable
* @param string $color color
*
* @throws NotFound whenever the given tag id does not exist
* @throws Forbidden whenever there is no permission to update said tag
* @throws Conflict whenever a tag already exists with the given attributes
*/
public function update($name, $userVisible, $userAssignable): void {
public function update($name, $userVisible, $userAssignable, $color): void {
try {
if (!$this->tagManager->canUserSeeTag($this->tag, $this->user)) {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist');
@ -110,7 +112,12 @@ class SystemTagNode implements \Sabre\DAV\ICollection {
}
}
$this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable);
// Make sure color is a proper hex
if ($color !== null && (strlen($color) !== 6 || !ctype_xdigit($color))) {
throw new BadRequest('Color must be a 6-digit hexadecimal value');
}
$this->tagManager->updateTag($this->tag->getId(), $name, $userVisible, $userAssignable, $color);
} catch (TagNotFoundException $e) {
throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist');
} catch (TagAlreadyExistsException $e) {

View file

@ -49,6 +49,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids';
public const COLOR_PROPERTYNAME = '{http://nextcloud.org/ns}color';
/**
* @var \Sabre\DAV\Server $server
@ -243,6 +244,10 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
return $this->tagManager->canUserAssignTag($node->getSystemTag(), $this->userSession->getUser()) ? 'true' : 'false';
});
$propFind->handle(self::COLOR_PROPERTYNAME, function () use ($node) {
return $node->getSystemTag()->getColor() ?? '';
});
$propFind->handle(self::GROUPS_PROPERTYNAME, function () use ($node) {
if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
// property only available for admins
@ -406,6 +411,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
self::GROUPS_PROPERTYNAME,
self::NUM_FILES_PROPERTYNAME,
self::REFERENCE_FILEID_PROPERTYNAME,
self::COLOR_PROPERTYNAME,
], function ($props) use ($node) {
if (!($node instanceof SystemTagNode)) {
return false;
@ -415,6 +421,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$name = $tag->getName();
$userVisible = $tag->isUserVisible();
$userAssignable = $tag->isUserAssignable();
$color = $tag->getColor();
$updateTag = false;
@ -435,6 +442,15 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$updateTag = true;
}
if (isset($props[self::COLOR_PROPERTYNAME])) {
$propValue = $props[self::COLOR_PROPERTYNAME];
if ($propValue === '' || $propValue === 'null') {
$propValue = null;
}
$color = $propValue;
$updateTag = true;
}
if (isset($props[self::GROUPS_PROPERTYNAME])) {
if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID())) {
// property only available for admins
@ -452,7 +468,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
}
if ($updateTag) {
$node->update($name, $userVisible, $userAssignable);
$node->update($name, $userVisible, $userAssignable, $color);
}
return true;

View file

@ -73,7 +73,7 @@ class SystemTagsInUseCollection extends SimpleCollection {
$result = $this->systemTagsInFilesDetector->detectAssignedSystemTagsIn($userFolder, $this->mediaType);
$children = [];
foreach ($result as $tagData) {
$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable'], $tagData['etag']);
$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable'], $tagData['etag'], $tagData['color']);
// read only, so we can submit the isAdmin parameter as false generally
$node = new SystemTagNode($tag, $user, false, $this->systemTagManager, $this->tagMapper);
$node->setNumberOfFiles((int)$tagData['number_files']);

View file

@ -85,19 +85,19 @@ class SystemTagNodeTest extends \Test\TestCase {
[
true,
new SystemTag(1, 'Original', true, true),
['Renamed', true, true]
['Renamed', true, true, null]
],
[
true,
new SystemTag(1, 'Original', true, true),
['Original', false, false]
['Original', false, false, null]
],
// non-admin
[
// renaming allowed
false,
new SystemTag(1, 'Original', true, true),
['Rename', true, true]
['Rename', true, true, '0082c9']
],
];
}
@ -116,9 +116,9 @@ class SystemTagNodeTest extends \Test\TestCase {
->willReturn($originalTag->isUserAssignable() || $isAdmin);
$this->tagManager->expects($this->once())
->method('updateTag')
->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2]);
->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
$this->getTagNode($isAdmin, $originalTag)
->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
}
public function tagNodeProviderPermissionException() {
@ -126,37 +126,37 @@ class SystemTagNodeTest extends \Test\TestCase {
[
// changing permissions not allowed
new SystemTag(1, 'Original', true, true),
['Original', false, true],
['Original', false, true, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing permissions not allowed
new SystemTag(1, 'Original', true, true),
['Original', true, false],
['Original', true, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing permissions not allowed
new SystemTag(1, 'Original', true, true),
['Original', false, false],
['Original', false, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing non-assignable not allowed
new SystemTag(1, 'Original', true, false),
['Rename', true, false],
['Rename', true, false, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// changing non-assignable not allowed
new SystemTag(1, 'Original', true, false),
['Original', true, true],
['Original', true, true, ''],
'Sabre\DAV\Exception\Forbidden',
],
[
// invisible tag does not exist
new SystemTag(1, 'Original', false, false),
['Rename', false, false],
['Rename', false, false, ''],
'Sabre\DAV\Exception\NotFound',
],
];
@ -181,7 +181,7 @@ class SystemTagNodeTest extends \Test\TestCase {
try {
$this->getTagNode(false, $originalTag)
->update($changedArgs[0], $changedArgs[1], $changedArgs[2]);
->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]);
} catch (\Exception $e) {
$thrown = $e;
}
@ -206,7 +206,7 @@ class SystemTagNodeTest extends \Test\TestCase {
->method('updateTag')
->with(1, 'Renamed', true, true)
->will($this->throwException(new TagAlreadyExistsException()));
$this->getTagNode(false, $tag)->update('Renamed', true, true);
$this->getTagNode(false, $tag)->update('Renamed', true, true, null);
}
@ -226,7 +226,7 @@ class SystemTagNodeTest extends \Test\TestCase {
->method('updateTag')
->with(1, 'Renamed', true, true)
->will($this->throwException(new TagNotFoundException()));
$this->getTagNode(false, $tag)->update('Renamed', true, true);
$this->getTagNode(false, $tag)->update('Renamed', true, true, null);
}
/**

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

@ -16,6 +16,8 @@ return array(
'OCA\\SystemTags\\Listeners\\BeforeSabrePubliclyLoadedListener' => $baseDir . '/../lib/Listeners/BeforeSabrePubliclyLoadedListener.php',
'OCA\\SystemTags\\Listeners\\BeforeTemplateRenderedListener' => $baseDir . '/../lib/Listeners/BeforeTemplateRenderedListener.php',
'OCA\\SystemTags\\Listeners\\LoadAdditionalScriptsListener' => $baseDir . '/../lib/Listeners/LoadAdditionalScriptsListener.php',
'OCA\\SystemTags\\Migration\\Version31000Date20241018063111' => $baseDir . '/../lib/Migration/Version31000Date20241018063111.php',
'OCA\\SystemTags\\Migration\\Version31000Date20241114171300' => $baseDir . '/../lib/Migration/Version31000Date20241114171300.php',
'OCA\\SystemTags\\Search\\TagSearchProvider' => $baseDir . '/../lib/Search/TagSearchProvider.php',
'OCA\\SystemTags\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
);

View file

@ -31,6 +31,8 @@ class ComposerStaticInitSystemTags
'OCA\\SystemTags\\Listeners\\BeforeSabrePubliclyLoadedListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeSabrePubliclyLoadedListener.php',
'OCA\\SystemTags\\Listeners\\BeforeTemplateRenderedListener' => __DIR__ . '/..' . '/../lib/Listeners/BeforeTemplateRenderedListener.php',
'OCA\\SystemTags\\Listeners\\LoadAdditionalScriptsListener' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScriptsListener.php',
'OCA\\SystemTags\\Migration\\Version31000Date20241018063111' => __DIR__ . '/..' . '/../lib/Migration/Version31000Date20241018063111.php',
'OCA\\SystemTags\\Migration\\Version31000Date20241114171300' => __DIR__ . '/..' . '/../lib/Migration/Version31000Date20241114171300.php',
'OCA\\SystemTags\\Search\\TagSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TagSearchProvider.php',
'OCA\\SystemTags\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
);

View file

@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
namespace OCA\SystemTags\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
@ -26,12 +26,6 @@ use OCP\Migration\SimpleMigrationStep;
#[AddIndex(table: 'systemtag_object_mapping', type: IndexType::INDEX, description: 'Adding objecttype index to systemtag_object_mapping')]
class Version31000Date20241018063111 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\SystemTags\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Add objecttype index to systemtag_object_mapping
*/
#[AddColumn(table: 'systemtag', name: 'color', type: ColumnType::STRING, description: 'Adding color for systemtag table')]
class Version31000Date20241114171300 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('systemtag')) {
$table = $schema->getTable('systemtag');
if (!$table->hasColumn('color')) {
$table->addColumn('color', Types::STRING, [
'notnull' => false,
'length' => 6,
]);
}
}
return $schema;
}
}

View file

@ -5,6 +5,7 @@
<template>
<NcDialog data-cy-systemtags-picker
:can-close="status !== Status.LOADING"
:name="t('systemtags', 'Manage tags')"
:open="opened"
:class="'systemtags-picker--' + status"
@ -31,34 +32,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">
@ -102,27 +126,38 @@ import type { Tag, TagWithId } from '../types'
import { defineComponent } from 'vue'
import { emit } from '@nextcloud/event-bus'
import { getLanguage, n, t } from '@nextcloud/l10n'
import { sanitize } from 'dompurify'
import { showError, showInfo } from '@nextcloud/dialogs'
import { getLanguage, n, t } from '@nextcloud/l10n'
import debounce from 'debounce'
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 debounceUpdateTag = debounce(updateTag, 500)
const mainBackgroundColor = getComputedStyle(document.body)
.getPropertyValue('--color-main-background')
.replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')
type TagListCount = {
string: number
}
@ -139,15 +174,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 +210,7 @@ export default defineComponent({
return {
status: Status.BASE,
opened: true,
openedPicker: false,
input: '',
tags: [] as TagWithId[],
@ -329,7 +369,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 +392,11 @@ export default defineComponent({
return tag.displayName
},
onColorChange(tag: TagWithId, color: `#${string}`) {
tag.color = color.replace('#', '')
debounceUpdateTag(tag)
},
isChecked(tag: TagWithId): boolean {
return tag.displayName in this.tagList
&& this.tagList[tag.displayName] === this.nodes.length
@ -480,6 +532,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 +580,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,11 @@
* 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'
const view = {
id: 'files',
@ -53,6 +54,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,
@ -86,7 +94,7 @@ describe('Inline system tags action render tests', () => {
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag">Confidential</li></ul>"',
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential">Confidential</li></ul>"',
)
})
@ -107,7 +115,7 @@ describe('Inline system tags action render tests', () => {
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag">Important</li><li class="files-list__system-tag">Confidential</li></ul>"',
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag" data-systemtag-name="Confidential">Confidential</li></ul>"',
)
})
@ -133,7 +141,7 @@ describe('Inline system tags action render tests', () => {
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag">Important</li><li class="files-list__system-tag files-list__system-tag--more" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually">Confidential</li><li class="files-list__system-tag hidden-visually">Secret</li><li class="files-list__system-tag hidden-visually">Classified</li></ul>"',
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag files-list__system-tag--more" data-systemtag-name="+3" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Confidential">Confidential</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Secret">Secret</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Classified">Classified</li></ul>"',
)
})
@ -160,12 +168,14 @@ describe('Inline system tags action render tests', () => {
document.body.appendChild(result)
expect(result).toBeInstanceOf(HTMLElement)
expect(document.body.innerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag">Important</li><li class="files-list__system-tag files-list__system-tag--more" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually">Confidential</li><li class="files-list__system-tag hidden-visually">Secret</li><li class="files-list__system-tag hidden-visually">Classified</li></ul>"',
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag files-list__system-tag--more" data-systemtag-name="+3" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Confidential">Confidential</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Secret">Secret</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Classified">Classified</li></ul>"',
)
// Subscribe to the event
const eventPromise = new Promise((resolve) => {
subscribe('systemtags:node:updated', resolve)
subscribe('systemtags:node:updated', () => {
setTimeout(resolve, 100)
})
})
// Change tags
@ -177,7 +187,113 @@ describe('Inline system tags action render tests', () => {
await eventPromise
expect(document.body.innerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag">Public</li></ul>"',
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Public">Public</li></ul>"',
)
})
})
describe('Inline system tags action colors', () => {
const tag = {
id: 1,
displayName: 'Confidential',
color: '000000',
etag: '123',
userVisible: true,
userAssignable: true,
canAssign: true,
}
beforeEach(() => {
document.body.innerHTML = ''
vi.spyOn(serviceTagApi, 'fetchTags').mockImplementation(async () => {
return [tag]
})
})
test('Render a single system tag', async () => {
const file = new File({
id: 1,
source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'system-tags': {
'system-tag': 'Confidential',
},
},
})
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #000000;" data-systemtag-color="true">Confidential</li></ul>"',
)
})
test('Render a single system tag with invalid WCAG color', async () => {
const file = new File({
id: 1,
source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'system-tags': {
'system-tag': 'Confidential',
},
},
})
document.body.setAttribute('data-themes', 'theme-dark')
const result = await action.renderInline!(file, view)
expect(result).toBeInstanceOf(HTMLElement)
expect(result!.outerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #646464;" data-systemtag-color="true">Confidential</li></ul>"',
)
document.body.removeAttribute('data-themes')
})
test('Rendered color gets updated on system tag change', async () => {
const file = new File({
id: 1,
source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'system-tags': {
'system-tag': 'Confidential',
},
},
})
const result = await action.renderInline!(file, view) as HTMLElement
document.body.appendChild(result)
expect(result).toBeInstanceOf(HTMLElement)
expect(document.body.innerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #000000;" data-systemtag-color="true">Confidential</li></ul>"',
)
// Subscribe to the event
const eventPromise = new Promise((resolve) => {
subscribe('systemtags:tag:updated', () => {
setTimeout(resolve, 100)
})
})
// Change tag color
tag.color = '456789'
emit('systemtags:tag:updated', tag)
// Wait for the event to be processed
await eventPromise
expect(document.body.innerHTML).toMatchInlineSnapshot(
'"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #456789;" data-systemtag-color="true">Confidential</li></ul>"',
)
})
})

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 || null}</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 = (brightBackground ? -100 : 100) / 255
while (contrast < minContrast && iteration++ < 100) {
const hsl = hexToHSL(result)
const l = Math.max(
0,
Math.min(255, hsl.l + 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}`
}

View file

@ -40,6 +40,12 @@ class Edit extends Base {
null,
InputOption::VALUE_OPTIONAL,
'sets the access control level (public, restricted, invisible)',
)
->addOption(
'color',
null,
InputOption::VALUE_OPTIONAL,
'set the tag color',
);
}
@ -80,9 +86,24 @@ class Edit extends Base {
}
}
$color = $tag->getColor();
if ($input->hasOption('color')) {
$color = $input->getOption('color');
if (substr($color, 0, 1) === '#') {
$color = substr($color, 1);
}
if ($input->getOption('color') === '') {
$color = null;
} elseif (strlen($color) !== 6 || !ctype_xdigit($color)) {
$output->writeln('<error>Color must be a 6-digit hexadecimal value</error>');
return 2;
}
}
try {
$this->systemTagManager->updateTag($input->getArgument('id'), $name, $userVisible, $userAssignable);
$output->writeln('<info>Tag updated ("' . $name . '", ' . $userVisible . ', ' . $userAssignable . ')</info>');
$this->systemTagManager->updateTag($input->getArgument('id'), $name, $userVisible, $userAssignable, $color);
$output->writeln('<info>Tag updated ("' . $name . '", ' . json_encode($userVisible) . ', ' . json_encode($userAssignable) . ', "' . ($color ? "#$color" : '') . '")</info>');
return 0;
} catch (TagNotFoundException $e) {
$output->writeln('<error>Tag not found</error>');

2
dist/226-226.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: xiaokai <kexiaokai@gmail.com>
SPDX-FileCopyrightText: string_decoder developers
SPDX-FileCopyrightText: readable-stream developers
SPDX-FileCopyrightText: qs developers
@ -11,6 +12,8 @@ SPDX-FileCopyrightText: jden <jason@denizac.org>
SPDX-FileCopyrightText: inherits developers
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: defunctzombie
SPDX-FileCopyrightText: debounce developers
SPDX-FileCopyrightText: color developers
SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
@ -18,6 +21,7 @@ SPDX-FileCopyrightText: Sindre Sorhus
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
SPDX-FileCopyrightText: Raynos <raynos2@gmail.com>
SPDX-FileCopyrightText: Qix (http://github.com/qix-)
SPDX-FileCopyrightText: Perry Mitchell <perry@perrymitchell.net>
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
@ -37,6 +41,7 @@ SPDX-FileCopyrightText: John Hiesey
SPDX-FileCopyrightText: James Halliday
SPDX-FileCopyrightText: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)
SPDX-FileCopyrightText: Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
SPDX-FileCopyrightText: Heather Arthur <fayearthur@gmail.com>
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
@ -44,6 +49,7 @@ SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Dylan Piercey <pierceydylan@gmail.com>
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: DY <dfcreative@gmail.com>
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Ben Drucker
SPDX-FileCopyrightText: Arnout Kazemier
@ -127,12 +133,27 @@ This file is generated from multiple sources. Included packages:
- charenc
- version: 0.0.2
- license: BSD-3-Clause
- color-convert
- version: 2.0.1
- license: MIT
- color-name
- version: 1.1.4
- license: MIT
- color-string
- version: 1.9.1
- license: MIT
- color
- version: 4.2.3
- license: MIT
- crypt
- version: 0.0.2
- license: BSD-3-Clause
- css-loader
- version: 7.1.2
- license: MIT
- debounce
- version: 2.2.0
- license: MIT
- define-data-property
- version: 1.1.4
- license: MIT
@ -277,6 +298,12 @@ This file is generated from multiple sources. Included packages:
- side-channel
- version: 1.0.6
- license: MIT
- is-arrayish
- version: 0.3.2
- license: MIT
- simple-swizzle
- version: 0.2.2
- license: MIT
- readable-stream
- version: 3.6.2
- license: MIT
@ -319,6 +346,9 @@ This file is generated from multiple sources. Included packages:
- util
- version: 0.12.5
- license: MIT
- vue-color
- version: 2.8.1
- license: MIT
- vue-loader
- version: 15.11.1
- license: MIT

1
dist/226-226.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/226-226.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
226-226.js.license

2
dist/8114-8114.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
8114-8114.js.license

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

View file

@ -243,6 +243,9 @@ This file is generated from multiple sources. Included packages:
- call-bind
- version: 1.0.7
- license: MIT
- camelcase
- version: 8.0.0
- license: MIT
- cancelable-promise
- version: 4.3.1
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -18,6 +18,7 @@ SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)
SPDX-FileCopyrightText: Thorsten Lünborg
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Sindre Sorhus
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Raynos <raynos2@gmail.com>
@ -167,6 +168,9 @@ This file is generated from multiple sources. Included packages:
- call-bind
- version: 1.0.7
- license: MIT
- camelcase
- version: 8.0.0
- license: MIT
- cancelable-promise
- version: 4.3.1
- license: MIT

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,7 @@ SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Sindre Sorhus
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Raynos <raynos2@gmail.com>
@ -145,6 +146,9 @@ This file is generated from multiple sources. Included packages:
- call-bind
- version: 1.0.7
- license: MIT
- camelcase
- version: 8.0.0
- license: MIT
- charenc
- version: 0.0.2
- license: BSD-3-Clause

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,7 @@ SPDX-FileCopyrightText: jden <jason@denizac.org>
SPDX-FileCopyrightText: inherits developers
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: defunctzombie
SPDX-FileCopyrightText: color developers
SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
@ -18,6 +19,7 @@ SPDX-FileCopyrightText: Sindre Sorhus
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Raynos <raynos2@gmail.com>
SPDX-FileCopyrightText: Qix (http://github.com/qix-)
SPDX-FileCopyrightText: Perry Mitchell <perry@perrymitchell.net>
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
@ -38,6 +40,7 @@ SPDX-FileCopyrightText: John Hiesey
SPDX-FileCopyrightText: James Halliday
SPDX-FileCopyrightText: Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)
SPDX-FileCopyrightText: Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
SPDX-FileCopyrightText: Heather Arthur <fayearthur@gmail.com>
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
@ -45,6 +48,7 @@ SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Dylan Piercey <pierceydylan@gmail.com>
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: DY <dfcreative@gmail.com>
SPDX-FileCopyrightText: Christoph Wurst <christoph@winzerhof-wurst.at>
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Ben Drucker
@ -148,6 +152,18 @@ This file is generated from multiple sources. Included packages:
- charenc
- version: 0.0.2
- license: BSD-3-Clause
- color-convert
- version: 2.0.1
- license: MIT
- color-name
- version: 1.1.4
- license: MIT
- color-string
- version: 1.9.1
- license: MIT
- color
- version: 4.2.3
- license: MIT
- crypt
- version: 0.0.2
- license: BSD-3-Clause
@ -298,6 +314,12 @@ This file is generated from multiple sources. Included packages:
- side-channel
- version: 1.0.6
- license: MIT
- is-arrayish
- version: 0.3.2
- license: MIT
- simple-swizzle
- version: 0.2.2
- license: MIT
- readable-stream
- version: 3.6.2
- license: MIT

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

File diff suppressed because one or more lines are too long

View file

@ -1416,7 +1416,6 @@ return array(
'OC\\Core\\Migrations\\Version30000Date20240906095113' => $baseDir . '/core/Migrations/Version30000Date20240906095113.php',
'OC\\Core\\Migrations\\Version31000Date20240101084401' => $baseDir . '/core/Migrations/Version31000Date20240101084401.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => $baseDir . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',

View file

@ -1457,7 +1457,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version30000Date20240906095113' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240906095113.php',
'OC\\Core\\Migrations\\Version31000Date20240101084401' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240101084401.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',

View file

@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'd481e4c575c189d6ddb128740892dd54a7c7ed48',
'reference' => 'ee76fe192de8656d216b4079a6c50dda3fc9cdb1',
'type' => 'library',
'install_path' => __DIR__ . '/../../../',
'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'd481e4c575c189d6ddb128740892dd54a7c7ed48',
'reference' => 'ee76fe192de8656d216b4079a6c50dda3fc9cdb1',
'type' => 'library',
'install_path' => __DIR__ . '/../../../',
'aliases' => array(),

View file

@ -17,40 +17,26 @@ class SystemTag implements ISystemTag {
private bool $userVisible,
private bool $userAssignable,
private ?string $etag = null,
private ?string $color = null,
) {
}
/**
* {@inheritdoc}
*/
public function getId(): string {
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getName(): string {
return $this->name;
}
/**
* {@inheritdoc}
*/
public function isUserVisible(): bool {
return $this->userVisible;
}
/**
* {@inheritdoc}
*/
public function isUserAssignable(): bool {
return $this->userAssignable;
}
/**
* {@inheritdoc}
*/
public function getAccessLevel(): int {
if (!$this->userVisible) {
return self::ACCESS_LEVEL_INVISIBLE;
@ -63,10 +49,11 @@ class SystemTag implements ISystemTag {
return self::ACCESS_LEVEL_PUBLIC;
}
/**
* {@inheritdoc}
*/
public function getETag(): ?string {
return $this->etag;
}
public function getColor(): ?string {
return $this->color;
}
}

View file

@ -45,9 +45,6 @@ class SystemTagManager implements ISystemTagManager {
->andWhere($query->expr()->eq('editable', $query->createParameter('editable')));
}
/**
* {@inheritdoc}
*/
public function getTagsByIds($tagIds, ?IUser $user = null): array {
if (!\is_array($tagIds)) {
$tagIds = [$tagIds];
@ -92,9 +89,6 @@ class SystemTagManager implements ISystemTagManager {
return $tags;
}
/**
* {@inheritdoc}
*/
public function getAllTags($visibilityFilter = null, $nameSearchPattern = null): array {
$tags = [];
@ -130,9 +124,6 @@ class SystemTagManager implements ISystemTagManager {
return $tags;
}
/**
* {@inheritdoc}
*/
public function getTag(string $tagName, bool $userVisible, bool $userAssignable): ISystemTag {
// Length of name column is 64
$truncatedTagName = substr($tagName, 0, 64);
@ -153,9 +144,6 @@ class SystemTagManager implements ISystemTagManager {
return $this->createSystemTagFromRow($row);
}
/**
* {@inheritdoc}
*/
public function createTag(string $tagName, bool $userVisible, bool $userAssignable): ISystemTag {
// Length of name column is 64
$truncatedTagName = substr($tagName, 0, 64);
@ -194,14 +182,12 @@ class SystemTagManager implements ISystemTagManager {
return $tag;
}
/**
* {@inheritdoc}
*/
public function updateTag(
string $tagId,
string $newName,
bool $userVisible,
bool $userAssignable,
?string $color,
): void {
try {
$tags = $this->getTagsByIds($tagId);
@ -218,7 +204,9 @@ class SystemTagManager implements ISystemTagManager {
$tagId,
$truncatedNewName,
$userVisible,
$userAssignable
$userAssignable,
$beforeUpdate->getETag(),
$color
);
$query = $this->connection->getQueryBuilder();
@ -226,11 +214,13 @@ class SystemTagManager implements ISystemTagManager {
->set('name', $query->createParameter('name'))
->set('visibility', $query->createParameter('visibility'))
->set('editable', $query->createParameter('editable'))
->set('color', $query->createParameter('color'))
->where($query->expr()->eq('id', $query->createParameter('tagid')))
->setParameter('name', $truncatedNewName)
->setParameter('visibility', $userVisible ? 1 : 0)
->setParameter('editable', $userAssignable ? 1 : 0)
->setParameter('tagid', $tagId);
->setParameter('tagid', $tagId)
->setParameter('color', $color);
try {
if ($query->execute() === 0) {
@ -251,9 +241,6 @@ class SystemTagManager implements ISystemTagManager {
));
}
/**
* {@inheritdoc}
*/
public function deleteTags($tagIds): void {
if (!\is_array($tagIds)) {
$tagIds = [$tagIds];
@ -303,9 +290,6 @@ class SystemTagManager implements ISystemTagManager {
}
}
/**
* {@inheritdoc}
*/
public function canUserAssignTag(ISystemTag $tag, ?IUser $user): bool {
if ($user === null) {
return false;
@ -335,9 +319,6 @@ class SystemTagManager implements ISystemTagManager {
return false;
}
/**
* {@inheritdoc}
*/
public function canUserSeeTag(ISystemTag $tag, ?IUser $user): bool {
// If no user, then we only show public tags
if (!$user && $tag->getAccessLevel() === ISystemTag::ACCESS_LEVEL_PUBLIC) {
@ -361,12 +342,9 @@ class SystemTagManager implements ISystemTagManager {
}
private function createSystemTagFromRow($row): SystemTag {
return new SystemTag((string)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable'], $row['etag']);
return new SystemTag((string)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable'], $row['etag'], $row['color']);
}
/**
* {@inheritdoc}
*/
public function setTagGroups(ISystemTag $tag, array $groupIds): void {
// delete relationships first
$this->connection->beginTransaction();
@ -398,9 +376,6 @@ class SystemTagManager implements ISystemTagManager {
}
}
/**
* {@inheritdoc}
*/
public function getTagGroups(ISystemTag $tag): array {
$groupIds = [];
$query = $this->connection->getQueryBuilder();
@ -418,4 +393,5 @@ class SystemTagManager implements ISystemTagManager {
return $groupIds;
}
}

View file

@ -89,4 +89,11 @@ interface ISystemTag {
* @since 31.0.0
*/
public function getETag(): ?string;
/**
* Returns the color of the tag
*
* @since 31.0.0
*/
public function getColor(): ?string;
}

View file

@ -81,14 +81,16 @@ interface ISystemTagManager {
* @param string $newName the new tag name
* @param bool $userVisible whether the tag is visible by users
* @param bool $userAssignable whether the tag is assignable by users
* @param string $color color
*
* @throws TagNotFoundException if tag with the given id does not exist
* @throws TagAlreadyExistsException if there is already another tag
* with the same attributes
*
* @since 9.0.0
* @since 31.0.0 `$color` parameter added
*/
public function updateTag(string $tagId, string $newName, bool $userVisible, bool $userAssignable);
public function updateTag(string $tagId, string $newName, bool $userVisible, bool $userAssignable, ?string $color);
/**
* Delete the given tags from the database and all their relationships.

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",

View file

@ -81,13 +81,14 @@ class EditTest extends TestCase {
$tagId,
$newTagName,
$newTagUserVisible,
$newTagUserAssignable
$newTagUserAssignable,
''
);
$this->output->expects($this->once())
->method('writeln')
->with(
'<info>Tag updated ("' . $newTagName . '", ' . $newTagUserVisible . ', ' . $newTagUserAssignable . ')</info>'
'<info>Tag updated ("' . $newTagName . '", ' . json_encode($newTagUserVisible) . ', ' . json_encode($newTagUserAssignable) . ', "")</info>'
);
$this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
@ -145,7 +146,8 @@ class EditTest extends TestCase {
$tagId,
$newTagName,
$newTagUserVisible,
$newTagUserAssignable
$newTagUserAssignable,
''
);
$this->output->expects($this->once())

View file

@ -72,7 +72,7 @@ class SystemTagManagerTest extends TestCase {
$query->delete(SystemTagManager::TAG_TABLE)->execute();
}
public function getAllTagsDataProvider() {
public static function getAllTagsDataProvider() {
return [
[
// no tags at all
@ -119,7 +119,7 @@ class SystemTagManagerTest extends TestCase {
}
}
public function getAllTagsFilteredDataProvider() {
public static function getAllTagsFilteredDataProvider() {
return [
[
[
@ -232,7 +232,7 @@ class SystemTagManagerTest extends TestCase {
}
}
public function oneTagMultipleFlagsProvider() {
public static function oneTagMultipleFlagsProvider() {
return [
['one', false, false],
['one', true, false],
@ -305,27 +305,27 @@ class SystemTagManagerTest extends TestCase {
$this->tagManager->getTagsByIds([$tag1->getId() . 'suffix']);
}
public function updateTagProvider() {
public static function updateTagProvider() {
return [
[
// update name
['one', true, true],
['two', true, true]
['one', true, true, '0082c9'],
['two', true, true, '0082c9']
],
[
// update one flag
['one', false, true],
['one', true, true]
['one', false, true, null],
['one', true, true, '0082c9']
],
[
// update all flags
['one', false, false],
['one', true, true]
['one', false, false, '0082c9'],
['one', true, true, null]
],
[
// update all
['one', false, false],
['two', true, true]
['one', false, false, '0082c9'],
['two', true, true, '0082c9']
],
];
}
@ -337,24 +337,29 @@ class SystemTagManagerTest extends TestCase {
$tag1 = $this->tagManager->createTag(
$tagCreate[0],
$tagCreate[1],
$tagCreate[2]
$tagCreate[2],
$tagCreate[3],
);
$this->tagManager->updateTag(
$tag1->getId(),
$tagUpdated[0],
$tagUpdated[1],
$tagUpdated[2]
$tagUpdated[2],
$tagUpdated[3],
);
$tag2 = $this->tagManager->getTag(
$tagUpdated[0],
$tagUpdated[1],
$tagUpdated[2]
$tagUpdated[2],
$tagUpdated[3],
);
$this->assertEquals($tag2->getId(), $tag1->getId());
$this->assertEquals($tag2->getName(), $tagUpdated[0]);
$this->assertEquals($tag2->isUserVisible(), $tagUpdated[1]);
$this->assertEquals($tag2->isUserAssignable(), $tagUpdated[2]);
$this->assertEquals($tag2->getColor(), $tagUpdated[3]);
}
/**
@ -366,12 +371,14 @@ class SystemTagManagerTest extends TestCase {
$this->tagManager->createTag(
$tagCreate[0],
$tagCreate[1],
$tagCreate[2]
$tagCreate[2],
$tagCreate[3],
);
$tag2 = $this->tagManager->createTag(
$tagUpdated[0],
$tagUpdated[1],
$tagUpdated[2]
$tagUpdated[2],
$tagUpdated[3],
);
// update to match the first tag
@ -379,7 +386,8 @@ class SystemTagManagerTest extends TestCase {
$tag2->getId(),
$tagCreate[0],
$tagCreate[1],
$tagCreate[2]
$tagCreate[2],
$tagCreate[3],
);
}
@ -422,7 +430,7 @@ class SystemTagManagerTest extends TestCase {
], $tagIdMapping);
}
public function visibilityCheckProvider() {
public static function visibilityCheckProvider() {
return [
[false, false, false, false],
[true, false, false, true],
@ -449,7 +457,7 @@ class SystemTagManagerTest extends TestCase {
$this->assertEquals($expectedResult, $this->tagManager->canUserSeeTag($tag1, $user));
}
public function assignabilityCheckProvider() {
public static function assignabilityCheckProvider() {
return [
// no groups
[false, false, false, false],