mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 00:32:29 -04:00
Merge pull request #41335 from nextcloud/enh/a11y/admin-collab-tags
enh(systemtags): Add accessible system tags form
This commit is contained in:
commit
50f8d6c129
29 changed files with 780 additions and 341 deletions
|
|
@ -1,29 +0,0 @@
|
|||
.systemtag-input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.systemtag-input--name {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.systemtag-input--name,
|
||||
.systemtag-input--level {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.systemtag-input--actions {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#systemtags .select2-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
#systemtags .select2-container .select2-choice {
|
||||
height: auto;
|
||||
}
|
||||
#systemtag_name {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
|
||||
* @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function() {
|
||||
if (!OCA.SystemTags) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.SystemTags = {};
|
||||
}
|
||||
|
||||
OCA.SystemTags.Admin = {
|
||||
|
||||
collection: null,
|
||||
|
||||
init: function() {
|
||||
var self = this;
|
||||
|
||||
this.collection = OC.SystemTags.collection;
|
||||
this.collection.fetch({
|
||||
success: function() {
|
||||
$('#systemtag').select2(_.extend(self.select2));
|
||||
$('#systemtag').parent().children('.select2-container').attr('aria-expanded', 'false')
|
||||
}
|
||||
});
|
||||
|
||||
var self = this;
|
||||
$('#systemtag_name').on('keyup', function(e) {
|
||||
if (e.which === 13) {
|
||||
_.bind(self._onClickSubmit, self)();
|
||||
}
|
||||
});
|
||||
$('#systemtag_submit').on('click', _.bind(this._onClickSubmit, this));
|
||||
$('#systemtag_delete').on('click', _.bind(this._onClickDelete, this));
|
||||
$('#systemtag_reset').on('click', _.bind(this._onClickReset, this));
|
||||
$('#systemtag').select2(_.extend(self.select2)).on('select2-open', () => {
|
||||
$('.select2-container').attr('aria-expanded', 'true')
|
||||
});
|
||||
$('#systemtag').select2(_.extend(self.select2)).on('select2-close', () => {
|
||||
$('.select2-container').attr('aria-expanded', 'false')
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Selecting a systemtag in select2
|
||||
*
|
||||
* @param {OC.SystemTags.SystemTagModel} tag
|
||||
*/
|
||||
onSelectTag: function (tag) {
|
||||
var level = 0;
|
||||
if (tag.get('userVisible')) {
|
||||
level += 2;
|
||||
if (tag.get('userAssignable')) {
|
||||
level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
$('#systemtag_name').val(tag.get('name'));
|
||||
$('#systemtag_level').val(level);
|
||||
|
||||
this._prepareForm(tag.get('id'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clicking the "Create"/"Update" button
|
||||
*/
|
||||
_onClickSubmit: function () {
|
||||
var level = parseInt($('#systemtag_level').val(), 10),
|
||||
tagId = $('#systemtags').attr('data-systemtag-id');
|
||||
var data = {
|
||||
name: $('#systemtag_name').val(),
|
||||
userVisible: level === 2 || level === 3,
|
||||
userAssignable: level === 3
|
||||
};
|
||||
|
||||
if (!data.name) {
|
||||
OCP.Toast.error(t('systemtags_manager', 'Tag name is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tagId) {
|
||||
var model = this.collection.get(tagId);
|
||||
model.save(data);
|
||||
} else {
|
||||
this.collection.create(data);
|
||||
}
|
||||
|
||||
this._onClickReset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clicking the "Delete" button
|
||||
*/
|
||||
_onClickDelete: function () {
|
||||
var tagId = $('#systemtags').attr('data-systemtag-id');
|
||||
var model = this.collection.get(tagId);
|
||||
model.destroy();
|
||||
|
||||
this._onClickReset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clicking the "Reset" button
|
||||
*/
|
||||
_onClickReset: function () {
|
||||
$('#systemtag_name').val('');
|
||||
$('#systemtag_level').val(3);
|
||||
this._prepareForm(0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the form for create/update
|
||||
*
|
||||
* @param {number} tagId
|
||||
*/
|
||||
_prepareForm: function (tagId) {
|
||||
if (tagId > 0) {
|
||||
$('#systemtags').attr('data-systemtag-id', tagId);
|
||||
$('#systemtag_delete').removeClass('hidden');
|
||||
$('#systemtag_submit span').text(t('systemtags_manager', 'Update'));
|
||||
$('#systemtag_create').addClass('hidden');
|
||||
} else {
|
||||
$('#systemtag').select2('val', '');
|
||||
$('#systemtags').attr('data-systemtag-id', '');
|
||||
$('#systemtag_delete').addClass('hidden');
|
||||
$('#systemtag_submit span').text(t('systemtags_manager', 'Create'));
|
||||
$('#systemtag_create').removeClass('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Select2 options for the SystemTag dropdown
|
||||
*/
|
||||
select2: {
|
||||
allowClear: false,
|
||||
multiple: false,
|
||||
placeholder: t('systemtags_manager', 'Select tag …'),
|
||||
query: _.debounce(function(query) {
|
||||
query.callback({
|
||||
results: OCA.SystemTags.Admin.collection.filterByName(query.term)
|
||||
});
|
||||
}, 100, true),
|
||||
id: function(element) {
|
||||
return element;
|
||||
},
|
||||
initSelection: function(element, callback) {
|
||||
var selection = ($(element).val() || []).split('|').sort();
|
||||
callback(selection);
|
||||
},
|
||||
formatResult: function (tag) {
|
||||
return OC.SystemTags.getDescriptiveTag(tag);
|
||||
},
|
||||
formatSelection: function (tag) {
|
||||
OCA.SystemTags.Admin.onSelectTag(tag);
|
||||
return OC.SystemTags.getDescriptiveTag(tag);
|
||||
},
|
||||
escapeMarkup: function(m) {
|
||||
return m;
|
||||
},
|
||||
sortResults: function(results) {
|
||||
results.sort(function(a, b) {
|
||||
return OC.Util.naturalSortCompare(a.get('name'), b.get('name'));
|
||||
});
|
||||
return results;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.TESTING) {
|
||||
OCA.SystemTags.Admin.init();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ class Admin implements ISettings {
|
|||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm() {
|
||||
\OCP\Util::addScript('systemtags', 'admin');
|
||||
return new TemplateResponse('systemtags', 'admin', [], '');
|
||||
}
|
||||
|
||||
|
|
|
|||
32
apps/systemtags/src/admin.ts
Normal file
32
apps/systemtags/src/admin.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
|
||||
import SystemTagsSection from './views/SystemTagsSection.vue'
|
||||
|
||||
// @ts-expect-error __webpack_nonce__ is injected by webpack
|
||||
__webpack_nonce__ = btoa(getRequestToken())
|
||||
|
||||
const SystemTagsSectionView = Vue.extend(SystemTagsSection)
|
||||
new SystemTagsSectionView().$mount('#vue-admin-systemtags')
|
||||
326
apps/systemtags/src/components/SystemTagForm.vue
Normal file
326
apps/systemtags/src/components/SystemTagForm.vue
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
<!--
|
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<form class="system-tag-form"
|
||||
:disabled="loading"
|
||||
aria-labelledby="system-tag-form-heading"
|
||||
@submit.prevent="handleSubmit"
|
||||
@reset="reset">
|
||||
<h3 id="system-tag-form-heading">
|
||||
{{ t('systemtags', 'Create or edit tags') }}
|
||||
</h3>
|
||||
|
||||
<div class="system-tag-form__group">
|
||||
<label for="system-tags-input">{{ t('systemtags', 'Search for a tag to edit') }}</label>
|
||||
<NcSelectTags v-model="selectedTag"
|
||||
input-id="system-tags-input"
|
||||
:placeholder="t('systemtags', 'Collaborative tags …')"
|
||||
:fetch-tags="false"
|
||||
:options="tags"
|
||||
:multiple="false"
|
||||
passthru>
|
||||
<template #no-options>
|
||||
{{ t('systemtags', 'No tags to select') }}
|
||||
</template>
|
||||
</NcSelectTags>
|
||||
</div>
|
||||
|
||||
<div class="system-tag-form__group">
|
||||
<label for="system-tag-name">{{ t('systemtags', 'Tag name') }}</label>
|
||||
<NcTextField id="system-tag-name"
|
||||
ref="tagNameInput"
|
||||
:value.sync="tagName"
|
||||
:error="Boolean(errorMessage)"
|
||||
:helper-text="errorMessage"
|
||||
label-outside />
|
||||
</div>
|
||||
|
||||
<div class="system-tag-form__group">
|
||||
<label for="system-tag-level">{{ t('systemtags', 'Tag level') }}</label>
|
||||
<NcSelect v-model="tagLevel"
|
||||
input-id="system-tag-level"
|
||||
:options="tagLevelOptions"
|
||||
:reduce="level => level.id"
|
||||
:clearable="false"
|
||||
:disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div class="system-tag-form__row">
|
||||
<NcButton v-if="isCreating"
|
||||
native-type="submit"
|
||||
:disabled="isCreateDisabled || loading">
|
||||
{{ t('systemtags', 'Create') }}
|
||||
</NcButton>
|
||||
<template v-else>
|
||||
<NcButton native-type="submit"
|
||||
:disabled="isUpdateDisabled || loading">
|
||||
{{ t('systemtags', 'Update') }}
|
||||
</NcButton>
|
||||
<NcButton :disabled="loading"
|
||||
@click="handleDelete">
|
||||
{{ t('systemtags', 'Delete') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
<NcButton native-type="reset"
|
||||
:disabled="isResetDisabled || loading">
|
||||
{{ t('systemtags', 'Reset') }}
|
||||
</NcButton>
|
||||
<NcLoadingIcon v-if="loading"
|
||||
:name="t('systemtags', 'Loading …')"
|
||||
:size="32" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/* eslint-disable */
|
||||
import Vue, { type PropType } from 'vue'
|
||||
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
||||
import NcSelectTags from '@nextcloud/vue/dist/Components/NcSelectTags.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { showSuccess } from '@nextcloud/dialogs'
|
||||
|
||||
import { defaultBaseTag } from '../utils.js'
|
||||
import { createTag, deleteTag, updateTag } from '../services/api.js'
|
||||
|
||||
import type { Tag, TagWithId } from '../types.js'
|
||||
|
||||
enum TagLevel {
|
||||
Public = 'Public',
|
||||
Restricted = 'Restricted',
|
||||
Invisible = 'Invisible',
|
||||
}
|
||||
|
||||
interface TagLevelOption {
|
||||
id: TagLevel
|
||||
label: string
|
||||
}
|
||||
|
||||
const tagLevelOptions: TagLevelOption[] = [
|
||||
{
|
||||
id: TagLevel.Public,
|
||||
label: t('systemtags', 'Public'),
|
||||
},
|
||||
{
|
||||
id: TagLevel.Restricted,
|
||||
label: t('systemtags', 'Restricted'),
|
||||
},
|
||||
{
|
||||
id: TagLevel.Invisible,
|
||||
label: t('systemtags', 'Invisible'),
|
||||
},
|
||||
]
|
||||
|
||||
const getTagLevel = (userVisible: boolean, userAssignable: boolean): TagLevel => {
|
||||
const matchLevel: Record<string, TagLevel> = {
|
||||
[[true, true].join(',')]: TagLevel.Public,
|
||||
[[true, false].join(',')]: TagLevel.Restricted,
|
||||
[[false, false].join(',')]: TagLevel.Invisible,
|
||||
}
|
||||
return matchLevel[[userVisible, userAssignable].join(',')]
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SystemTagForm',
|
||||
|
||||
components: {
|
||||
NcButton,
|
||||
NcLoadingIcon,
|
||||
NcSelect,
|
||||
NcSelectTags,
|
||||
NcTextField,
|
||||
},
|
||||
|
||||
props: {
|
||||
tags: {
|
||||
type: Array as PropType<TagWithId[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tagLevelOptions,
|
||||
selectedTag: null as null | TagWithId,
|
||||
errorMessage: '',
|
||||
tagName: '',
|
||||
tagLevel: TagLevel.Public,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
selectedTag(tag: null | TagWithId) {
|
||||
this.tagName = tag ? tag.displayName : ''
|
||||
this.tagLevel = tag ? getTagLevel(tag.userVisible, tag.userAssignable) : TagLevel.Public
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCreating(): boolean {
|
||||
return this.selectedTag === null
|
||||
},
|
||||
|
||||
isCreateDisabled(): boolean {
|
||||
return this.tagName === ''
|
||||
},
|
||||
|
||||
isUpdateDisabled(): boolean {
|
||||
return (
|
||||
this.tagName === ''
|
||||
|| (
|
||||
this.selectedTag?.displayName === this.tagName
|
||||
&& getTagLevel(this.selectedTag?.userVisible, this.selectedTag?.userAssignable) === this.tagLevel
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
isResetDisabled(): boolean {
|
||||
if (this.isCreating) {
|
||||
return this.tagName === '' && this.tagLevel === TagLevel.Public
|
||||
}
|
||||
return this.selectedTag === null
|
||||
},
|
||||
|
||||
userVisible(): boolean {
|
||||
const matchLevel: Record<TagLevel, boolean> = {
|
||||
[TagLevel.Public]: true,
|
||||
[TagLevel.Restricted]: true,
|
||||
[TagLevel.Invisible]: false,
|
||||
}
|
||||
return matchLevel[this.tagLevel]
|
||||
},
|
||||
|
||||
userAssignable(): boolean {
|
||||
const matchLevel: Record<TagLevel, boolean> = {
|
||||
[TagLevel.Public]: true,
|
||||
[TagLevel.Restricted]: false,
|
||||
[TagLevel.Invisible]: false,
|
||||
}
|
||||
return matchLevel[this.tagLevel]
|
||||
},
|
||||
|
||||
tagProperties(): Omit<Tag, 'id' | 'canAssign'> {
|
||||
return {
|
||||
displayName: this.tagName,
|
||||
userVisible: this.userVisible,
|
||||
userAssignable: this.userAssignable,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
async handleSubmit() {
|
||||
if (this.isCreating) {
|
||||
await this.create()
|
||||
return
|
||||
}
|
||||
await this.update()
|
||||
},
|
||||
|
||||
async create() {
|
||||
const tag: Tag = { ...defaultBaseTag, ...this.tagProperties }
|
||||
this.loading = true
|
||||
try {
|
||||
const id = await createTag(tag)
|
||||
const createdTag: TagWithId = { ...tag, id }
|
||||
this.$emit('tag:created', createdTag)
|
||||
showSuccess(t('systemtags', 'Created tag'))
|
||||
this.reset()
|
||||
} catch (error) {
|
||||
this.errorMessage = t('systemtags', 'Failed to create tag')
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async update() {
|
||||
if (this.selectedTag === null) {
|
||||
return
|
||||
}
|
||||
const tag: TagWithId = { ...this.selectedTag, ...this.tagProperties }
|
||||
this.loading = true
|
||||
try {
|
||||
await updateTag(tag)
|
||||
this.selectedTag = tag
|
||||
this.$emit('tag:updated', tag)
|
||||
showSuccess(t('systemtags', 'Updated tag'))
|
||||
this.$refs.tagNameInput?.focus()
|
||||
} catch (error) {
|
||||
this.errorMessage = t('systemtags', 'Failed to update tag')
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async handleDelete() {
|
||||
if (this.selectedTag === null) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
await deleteTag(this.selectedTag)
|
||||
this.$emit('tag:deleted', this.selectedTag)
|
||||
showSuccess(t('systemtags', 'Deleted tag'))
|
||||
this.reset()
|
||||
} catch (error) {
|
||||
this.errorMessage = t('systemtags', 'Failed to delete tag')
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.selectedTag = null
|
||||
this.errorMessage = ''
|
||||
this.tagName = ''
|
||||
this.tagLevel = TagLevel.Public
|
||||
this.$refs.tagNameInput?.focus()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-tag-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
gap: 8px 0;
|
||||
|
||||
&__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__row {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 0 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -59,22 +59,16 @@ import NcSelectTags from '@nextcloud/vue/dist/Components/NcSelectTags.js'
|
|||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import { defaultBaseTag } from '../utils.js'
|
||||
import { fetchLastUsedTagIds, fetchTags } from '../services/api.js'
|
||||
import {
|
||||
createTag,
|
||||
deleteTag,
|
||||
fetchLastUsedTagIds,
|
||||
fetchSelectedTags,
|
||||
fetchTags,
|
||||
selectTag,
|
||||
} from '../services/api.js'
|
||||
createTagForFile,
|
||||
deleteTagForFile,
|
||||
fetchTagsForFile,
|
||||
setTagForFile,
|
||||
} from '../services/files.js'
|
||||
|
||||
import type { BaseTag, Tag, TagWithId } from '../types.js'
|
||||
|
||||
const defaultBaseTag: BaseTag = {
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true,
|
||||
}
|
||||
import type { Tag, TagWithId } from '../types.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SystemTags',
|
||||
|
|
@ -133,7 +127,7 @@ export default Vue.extend({
|
|||
async handler() {
|
||||
this.loadingTags = true
|
||||
try {
|
||||
this.selectedTags = await fetchSelectedTags(this.fileId)
|
||||
this.selectedTags = await fetchTagsForFile(this.fileId)
|
||||
this.$emit('has-tags', this.selectedTags.length > 0)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to load selected tags'))
|
||||
|
|
@ -175,14 +169,15 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async handleSelect(tags: Tag[]) {
|
||||
const selectedTag = tags[tags.length - 1]
|
||||
if (!selectedTag.id) {
|
||||
const lastTag = tags[tags.length - 1]
|
||||
if (!lastTag.id) {
|
||||
// Ignore created tags handled by `handleCreate()`
|
||||
return
|
||||
}
|
||||
const selectedTag = lastTag as TagWithId
|
||||
this.loading = true
|
||||
try {
|
||||
await selectTag(this.fileId, selectedTag)
|
||||
await setTagForFile(selectedTag, this.fileId)
|
||||
const sortToFront = (a: TagWithId, b: TagWithId) => {
|
||||
if (a.id === selectedTag.id) {
|
||||
return -1
|
||||
|
|
@ -201,7 +196,7 @@ export default Vue.extend({
|
|||
async handleCreate(tag: Tag) {
|
||||
this.loading = true
|
||||
try {
|
||||
const id = await createTag(this.fileId, tag)
|
||||
const id = await createTagForFile(tag, this.fileId)
|
||||
const createdTag = { ...tag, id }
|
||||
this.sortedTags.unshift(createdTag)
|
||||
this.selectedTags.push(createdTag)
|
||||
|
|
@ -211,10 +206,10 @@ export default Vue.extend({
|
|||
this.loading = false
|
||||
},
|
||||
|
||||
async handleDeselect(tag: Tag) {
|
||||
async handleDeselect(tag: TagWithId) {
|
||||
this.loading = true
|
||||
try {
|
||||
await deleteTag(this.fileId, tag)
|
||||
await deleteTagForFile(tag, this.fileId)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to delete tag'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
import type { ServerTag, Tag, TagWithId } from '../types.js'
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ import { davClient } from './davClient.js'
|
|||
import { formatTag, parseIdFromLocation, parseTags } from '../utils'
|
||||
import { logger } from '../logger.js'
|
||||
|
||||
const fetchTagsBody = `<?xml version="1.0"?>
|
||||
export const fetchTagsPayload = `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:id />
|
||||
|
|
@ -45,7 +46,7 @@ export const fetchTags = async (): Promise<TagWithId[]> => {
|
|||
const path = '/systemtags'
|
||||
try {
|
||||
const { data: tags } = await davClient.getDirectoryContents(path, {
|
||||
data: fetchTagsBody,
|
||||
data: fetchTagsPayload,
|
||||
details: true,
|
||||
glob: '/systemtags/*', // Filter out first empty tag
|
||||
}) as ResponseDataDetailed<Required<FileStat>[]>
|
||||
|
|
@ -67,39 +68,10 @@ export const fetchLastUsedTagIds = async (): Promise<number[]> => {
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchSelectedTags = async (fileId: number): Promise<TagWithId[]> => {
|
||||
const path = '/systemtags-relations/files/' + fileId
|
||||
try {
|
||||
const { data: tags } = await davClient.getDirectoryContents(path, {
|
||||
data: fetchTagsBody,
|
||||
details: true,
|
||||
glob: '/systemtags-relations/files/*/*', // Filter out first empty tag
|
||||
}) as ResponseDataDetailed<Required<FileStat>[]>
|
||||
return parseTags(tags)
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to load selected tags'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to load selected tags'))
|
||||
}
|
||||
}
|
||||
|
||||
export const selectTag = async (fileId: number, tag: Tag | ServerTag): Promise<void> => {
|
||||
const path = '/systemtags-relations/files/' + fileId + '/' + tag.id
|
||||
const tagToPut = formatTag(tag)
|
||||
try {
|
||||
await davClient.customRequest(path, {
|
||||
method: 'PUT',
|
||||
data: tagToPut,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to select tag'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to select tag'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return created tag id
|
||||
*/
|
||||
export const createTag = async (fileId: number, tag: Tag): Promise<number> => {
|
||||
export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
|
||||
const path = '/systemtags'
|
||||
const tagToPost = formatTag(tag)
|
||||
try {
|
||||
|
|
@ -109,12 +81,7 @@ export const createTag = async (fileId: number, tag: Tag): Promise<number> => {
|
|||
})
|
||||
const contentLocation = headers.get('content-location')
|
||||
if (contentLocation) {
|
||||
const tagToPut = {
|
||||
...tagToPost,
|
||||
id: parseIdFromLocation(contentLocation),
|
||||
}
|
||||
await selectTag(fileId, tagToPut)
|
||||
return tagToPut.id
|
||||
return parseIdFromLocation(contentLocation)
|
||||
}
|
||||
logger.error(t('systemtags', 'Missing "Content-Location" header'))
|
||||
throw new Error(t('systemtags', 'Missing "Content-Location" header'))
|
||||
|
|
@ -124,8 +91,32 @@ export const createTag = async (fileId: number, tag: Tag): Promise<number> => {
|
|||
}
|
||||
}
|
||||
|
||||
export const deleteTag = async (fileId: number, tag: Tag): Promise<void> => {
|
||||
const path = '/systemtags-relations/files/' + fileId + '/' + tag.id
|
||||
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: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>
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</d:propertyupdate>`
|
||||
|
||||
try {
|
||||
await davClient.customRequest(path, {
|
||||
method: 'PROPPATCH',
|
||||
data,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to update tag'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to update tag'))
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteTag = async (tag: TagWithId): Promise<void> => {
|
||||
const path = '/systemtags/' + tag.id
|
||||
try {
|
||||
await davClient.deleteFile(path)
|
||||
} catch (error) {
|
||||
|
|
|
|||
82
apps/systemtags/src/services/files.ts
Normal file
82
apps/systemtags/src/services/files.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
import type { ServerTagWithId, Tag, TagWithId } from '../types.js'
|
||||
|
||||
import { davClient } from './davClient.js'
|
||||
import { createTag, fetchTagsPayload } from './api.js'
|
||||
import { formatTag, parseTags } from '../utils.js'
|
||||
import { logger } from '../logger.js'
|
||||
|
||||
export const fetchTagsForFile = async (fileId: number): Promise<TagWithId[]> => {
|
||||
const path = '/systemtags-relations/files/' + fileId
|
||||
try {
|
||||
const { data: tags } = await davClient.getDirectoryContents(path, {
|
||||
data: fetchTagsPayload,
|
||||
details: true,
|
||||
glob: '/systemtags-relations/files/*/*', // Filter out first empty tag
|
||||
}) as ResponseDataDetailed<Required<FileStat>[]>
|
||||
return parseTags(tags)
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to load tags for file'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to load tags for file'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return created tag id
|
||||
*/
|
||||
export const createTagForFile = async (tag: Tag, fileId: number): Promise<number> => {
|
||||
const tagToCreate = formatTag(tag)
|
||||
const tagId = await createTag(tagToCreate)
|
||||
const tagToSet: ServerTagWithId = {
|
||||
...tagToCreate,
|
||||
id: tagId,
|
||||
}
|
||||
await setTagForFile(tagToSet, fileId)
|
||||
return tagToSet.id
|
||||
}
|
||||
|
||||
export const setTagForFile = async (tag: TagWithId | ServerTagWithId, fileId: number): Promise<void> => {
|
||||
const path = '/systemtags-relations/files/' + fileId + '/' + tag.id
|
||||
const tagToPut = formatTag(tag)
|
||||
try {
|
||||
await davClient.customRequest(path, {
|
||||
method: 'PUT',
|
||||
data: tagToPut,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to set tag for file'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to set tag for file'))
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteTagForFile = async (tag: TagWithId, fileId: number): Promise<void> => {
|
||||
const path = '/systemtags-relations/files/' + fileId + '/' + tag.id
|
||||
try {
|
||||
await davClient.deleteFile(path)
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to delete tag for file'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to delete tag for file'))
|
||||
}
|
||||
}
|
||||
|
|
@ -36,3 +36,5 @@ export type TagWithId = Required<Tag>
|
|||
export type ServerTag = BaseTag & {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type ServerTagWithId = Required<ServerTag>
|
||||
|
|
|
|||
|
|
@ -24,12 +24,18 @@ import camelCase from 'camelcase'
|
|||
|
||||
import type { DAVResultResponseProps } from 'webdav'
|
||||
|
||||
import type { ServerTag, Tag, TagWithId } from './types.js'
|
||||
import type { BaseTag, ServerTag, Tag, TagWithId } from './types.js'
|
||||
|
||||
export const defaultBaseTag: BaseTag = {
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true,
|
||||
}
|
||||
|
||||
export const parseTags = (tags: { props: DAVResultResponseProps }[]): TagWithId[] => {
|
||||
return tags.map(({ props }) => Object.fromEntries(
|
||||
Object.entries(props)
|
||||
.map(([key, value]) => [camelCase(key), value]),
|
||||
.map(([key, value]) => [camelCase(key), camelCase(key) === 'displayName' ? String(value) : value]),
|
||||
)) as TagWithId[]
|
||||
}
|
||||
|
||||
|
|
|
|||
99
apps/systemtags/src/views/SystemTagsSection.vue
Normal file
99
apps/systemtags/src/views/SystemTagsSection.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<!--
|
||||
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @author Christopher Ng <chrng8@gmail.com>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection :name="t('systemtags', 'Collaborative tags')"
|
||||
:description="t('systemtags', 'Collaborative tags are available for all users. Restricted tags are visible to users but cannot be assigned by them. Invisible tags are for internal use, since users cannot see or assign them.')">
|
||||
<NcLoadingIcon v-if="loadingTags"
|
||||
:name="t('systemtags', 'Loading collaborative tags …')"
|
||||
:size="32" />
|
||||
|
||||
<SystemTagForm v-else
|
||||
:tags="tags"
|
||||
@tag:created="handleCreate"
|
||||
@tag:updated="handleUpdate"
|
||||
@tag:deleted="handleDelete" />
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/* eslint-disable */
|
||||
import Vue from 'vue'
|
||||
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import SystemTagForm from '../components/SystemTagForm.vue'
|
||||
|
||||
import { fetchTags } from '../services/api.js'
|
||||
|
||||
import type { TagWithId } from '../types.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SystemTagsSection',
|
||||
|
||||
components: {
|
||||
NcLoadingIcon,
|
||||
NcSettingsSection,
|
||||
SystemTagForm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loadingTags: false,
|
||||
tags: [] as TagWithId[],
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.loadingTags = true
|
||||
try {
|
||||
this.tags = await fetchTags()
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to load tags'))
|
||||
}
|
||||
this.loadingTags = false
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
handleCreate(tag: TagWithId) {
|
||||
this.tags.unshift(tag)
|
||||
},
|
||||
|
||||
handleUpdate(tag: TagWithId) {
|
||||
const tagIndex = this.tags.findIndex(currTag => currTag.id === tag.id)
|
||||
this.tags.splice(tagIndex, 1)
|
||||
this.tags.unshift(tag)
|
||||
},
|
||||
|
||||
handleDelete(tag: TagWithId) {
|
||||
const tagIndex = this.tags.findIndex(currTag => currTag.id === tag.id)
|
||||
this.tags.splice(tagIndex, 1)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -18,43 +18,6 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
script('core', 'systemtags');
|
||||
|
||||
script('systemtags', 'admin');
|
||||
style('systemtags', 'settings');
|
||||
|
||||
/** @var \OCP\IL10N $l */
|
||||
?>
|
||||
|
||||
<form id="systemtags" class="section" data-systemtag-id="">
|
||||
<h2><?php p($l->t('Collaborative tags')); ?></h2>
|
||||
<p class="settings-hint"><?php p($l->t('Collaborative tags are available for all users. Restricted tags are visible to users but cannot be assigned by them. Invisible tags are for internal use, since users cannot see or assign them.')); ?></p>
|
||||
|
||||
<input type="hidden" name="systemtag" id="systemtag" placeholder="<?php p($l->t('Select tag …')); ?>" />
|
||||
|
||||
<h3 id="systemtag_create"><?php p($l->t('Create a new tag')); ?></h3>
|
||||
|
||||
<div class="systemtag-input">
|
||||
<div class="systemtag-input--name">
|
||||
<label for="systemtag_name"><?php p($l->t('Tag name')); ?></label>
|
||||
<input type="text" id="systemtag_name" name="systemtag_name" placeholder="<?php p($l->t('Name')); ?>">
|
||||
</div>
|
||||
|
||||
<div class="systemtag-input--level">
|
||||
<label for="systemtag_level"><?php p($l->t('Tag level')); ?></label>
|
||||
<select id="systemtag_level">
|
||||
<option value="3"><?php p($l->t('Public')); ?></option>
|
||||
<option value="2"><?php p($l->t('Restricted')); ?></option>
|
||||
<option value="0"><?php p($l->t('Invisible')); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="systemtag-input--actions">
|
||||
<a id="systemtag_delete" class="hidden button systemtag-input--actions-button"><span><?php p($l->t('Delete')); ?></span></a>
|
||||
<a id="systemtag_reset" class="button systemtag-input--actions-button"><span><?php p($l->t('Reset')); ?></span></a>
|
||||
<a id="systemtag_submit" class="button systemtag-input--actions-button"><span><?php p($l->t('Create')); ?></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div id="vue-admin-systemtags"></div>
|
||||
|
|
|
|||
138
cypress/e2e/settings/systemtags.cy.ts
Normal file
138
cypress/e2e/settings/systemtags.cy.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/cypress'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
const tagName = 'foo'
|
||||
const updatedTagName = 'bar'
|
||||
|
||||
describe('Create system tags', () => {
|
||||
before(() => {
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin')
|
||||
})
|
||||
|
||||
it('Can create a tag', () => {
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', '')
|
||||
cy.get('input#system-tag-name').type(tagName)
|
||||
cy.get('input#system-tag-name').should('have.value', tagName)
|
||||
// submit the form
|
||||
cy.get('input#system-tag-name').type('{enter}')
|
||||
|
||||
// see that the created tag is in the list
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', tagName).should('exist')
|
||||
// ensure only one tag exists
|
||||
cy.get('li').should('have.length', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Update system tags', { testIsolation: false }, () => {
|
||||
before(() => {
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin')
|
||||
})
|
||||
|
||||
it('select the tag', () => {
|
||||
// select the tag to edit
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', tagName).should('exist').click()
|
||||
})
|
||||
})
|
||||
// see that the tag name matches the selected tag
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', tagName)
|
||||
// see that the tag level matches the selected tag
|
||||
cy.get('input#system-tag-level').click()
|
||||
cy.get('input#system-tag-level').siblings('.vs__selected').contains('Public').should('exist')
|
||||
})
|
||||
|
||||
it('update the tag name and level', () => {
|
||||
cy.get('input#system-tag-name').clear()
|
||||
cy.get('input#system-tag-name').type(updatedTagName)
|
||||
cy.get('input#system-tag-name').should('have.value', updatedTagName)
|
||||
// select the new tag level
|
||||
cy.get('input#system-tag-level').focus()
|
||||
cy.get('input#system-tag-level').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', 'Invisible').should('exist').click()
|
||||
})
|
||||
})
|
||||
// submit the form
|
||||
cy.get('input#system-tag-name').type('{enter}')
|
||||
})
|
||||
|
||||
it('see the tag was successfully updated', () => {
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', `${updatedTagName} (invisible)`).should('exist')
|
||||
// ensure only one tag exists
|
||||
cy.get('li').should('have.length', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete system tags', { testIsolation: false }, () => {
|
||||
before(() => {
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin')
|
||||
})
|
||||
|
||||
it('select the tag', () => {
|
||||
// select the tag to edit
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', `${updatedTagName} (invisible)`).should('exist').click()
|
||||
})
|
||||
})
|
||||
// see that the tag name matches the selected tag
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', updatedTagName)
|
||||
// see that the tag level matches the selected tag
|
||||
cy.get('input#system-tag-level').focus()
|
||||
cy.get('input#system-tag-level').siblings('.vs__selected').contains('Invisible').should('exist')
|
||||
})
|
||||
|
||||
it('can delete the tag', () => {
|
||||
cy.get('.system-tag-form__row').within(() => {
|
||||
cy.contains('button', 'Delete').should('be.enabled').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('see that the deleted tag is not present', () => {
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then(id => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', updatedTagName).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-sidebar.js
vendored
4
dist/files-sidebar.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-sidebar.js.map
vendored
2
dist/files-sidebar.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-personal-settings.js
vendored
4
dist/files_sharing-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-personal-settings.js.map
vendored
2
dist/files_sharing-personal-settings.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-vue-settings-admin-security.js
vendored
4
dist/settings-vue-settings-admin-security.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
dist/systemtags-admin.js
vendored
Normal file
3
dist/systemtags-admin.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
dist/systemtags-admin.js.LICENSE.txt
vendored
Normal file
21
dist/systemtags-admin.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @author Christopher Ng <chrng8@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
1
dist/systemtags-admin.js.map
vendored
Normal file
1
dist/systemtags-admin.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/systemtags-init.js
vendored
4
dist/systemtags-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/systemtags-init.js.map
vendored
2
dist/systemtags-init.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -103,6 +103,7 @@ module.exports = {
|
|||
},
|
||||
systemtags: {
|
||||
init: path.join(__dirname, 'apps/systemtags/src', 'init.ts'),
|
||||
admin: path.join(__dirname, 'apps/systemtags/src', 'admin.ts'),
|
||||
},
|
||||
theming: {
|
||||
'personal-theming': path.join(__dirname, 'apps/theming/src', 'personal-settings.js'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue