Merge pull request #41335 from nextcloud/enh/a11y/admin-collab-tags
enh(systemtags): Add accessible system tags formpull/41526/head
commit
50f8d6c129
@ -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(); |
||||
} |
||||
}); |
||||
|
@ -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') |
@ -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> |
@ -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')) |
||||
} |
||||
} |
@ -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> |
@ -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') |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
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
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
File diff suppressed because one or more lines are too long
@ -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/>. |
||||
* |
||||
*/ |
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
Loading…
Reference in new issue