mirror of
https://github.com/nextcloud/server.git
synced 2026-06-14 19:20:35 -04:00
Merge pull request #37065 from nextcloud/enh/a11y-system-tags
Add accessible system tags select
This commit is contained in:
commit
09cb9c78e7
32 changed files with 631 additions and 569 deletions
|
|
@ -10,6 +10,9 @@ module.exports = {
|
|||
firstDay: true,
|
||||
'cypress/globals': true,
|
||||
},
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
plugins: [
|
||||
'cypress',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -36,10 +36,16 @@
|
|||
@closed="handleClosed">
|
||||
<!-- TODO: create a standard to allow multiple elements here? -->
|
||||
<template v-if="fileInfo" #description>
|
||||
<LegacyView v-for="view in views"
|
||||
:key="view.cid"
|
||||
:component="view"
|
||||
:file-info="fileInfo" />
|
||||
<div class="sidebar__description">
|
||||
<SystemTags v-if="isSystemTagsEnabled"
|
||||
v-show="showTags"
|
||||
:file-id="fileInfo.id"
|
||||
@has-tags="value => showTags = value" />
|
||||
<LegacyView v-for="view in views"
|
||||
:key="view.cid"
|
||||
:component="view"
|
||||
:file-info="fileInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions menu -->
|
||||
|
|
@ -96,22 +102,25 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
|||
import FileInfo from '../services/FileInfo.js'
|
||||
import SidebarTab from '../components/SidebarTab.vue'
|
||||
import LegacyView from '../components/LegacyView.vue'
|
||||
import SystemTags from '../../../systemtags/src/components/SystemTags.vue'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
|
||||
components: {
|
||||
LegacyView,
|
||||
NcActionButton,
|
||||
NcAppSidebar,
|
||||
NcEmptyContent,
|
||||
LegacyView,
|
||||
SidebarTab,
|
||||
SystemTags,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// reactive state
|
||||
Sidebar: OCA.Files.Sidebar.state,
|
||||
showTags: false,
|
||||
error: null,
|
||||
loading: true,
|
||||
fileInfo: null,
|
||||
|
|
@ -410,9 +419,7 @@ export default {
|
|||
* Toggle the tags selector
|
||||
*/
|
||||
toggleTags() {
|
||||
if (OCA.SystemTags && OCA.SystemTags.View) {
|
||||
OCA.SystemTags.View.toggle()
|
||||
}
|
||||
this.showTags = !this.showTags
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -505,7 +512,7 @@ export default {
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-sidebar {
|
||||
&--has-preview::v-deep {
|
||||
&--has-preview:deep {
|
||||
.app-sidebar-header__figure {
|
||||
background-size: cover;
|
||||
}
|
||||
|
|
@ -525,6 +532,12 @@ export default {
|
|||
height: 100% !important;
|
||||
}
|
||||
|
||||
:deep {
|
||||
.app-sidebar-header__description {
|
||||
margin: 0 16px 4px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
::v-deep svg {
|
||||
width: 20px;
|
||||
|
|
@ -533,4 +546,11 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
235
apps/systemtags/src/components/SystemTags.vue
Normal file
235
apps/systemtags/src/components/SystemTags.vue
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<!--
|
||||
- @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>
|
||||
<div class="system-tags">
|
||||
<label for="system-tags-input">{{ t('systemtags', 'Search or create collaborative tags') }}</label>
|
||||
<NcSelectTags class="system-tags__select"
|
||||
input-id="system-tags-input"
|
||||
:placeholder="t('systemtags', 'Collaborative tags …')"
|
||||
:options="sortedTags"
|
||||
:value="selectedTags"
|
||||
:create-option="createOption"
|
||||
:taggable="true"
|
||||
:passthru="true"
|
||||
:fetch-tags="false"
|
||||
:loading="loading"
|
||||
@input="handleInput"
|
||||
@option:selected="handleSelect"
|
||||
@option:created="handleCreate"
|
||||
@option:deselected="handleDeselect">
|
||||
<template #no-options>
|
||||
{{ t('systemtags', 'No tags to select, type to create a new tag') }}
|
||||
</template>
|
||||
</NcSelectTags>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// FIXME Vue TypeScript ESLint errors
|
||||
/* eslint-disable */
|
||||
import Vue from 'vue'
|
||||
import NcSelectTags from '@nextcloud/vue/dist/Components/NcSelectTags.js'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import {
|
||||
createTag,
|
||||
deleteTag,
|
||||
fetchLastUsedTagIds,
|
||||
fetchSelectedTags,
|
||||
fetchTags,
|
||||
selectTag,
|
||||
} from '../services/api.js'
|
||||
|
||||
import type { BaseTag, Tag, TagWithId } from '../types.js'
|
||||
|
||||
const defaultBaseTag: BaseTag = {
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true,
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SystemTags',
|
||||
|
||||
components: {
|
||||
NcSelectTags,
|
||||
},
|
||||
|
||||
props: {
|
||||
fileId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
sortedTags: [] as TagWithId[],
|
||||
selectedTags: [] as TagWithId[],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
try {
|
||||
const tags = await fetchTags()
|
||||
const lastUsedOrder = await fetchLastUsedTagIds()
|
||||
|
||||
const lastUsedTags: TagWithId[] = []
|
||||
const remainingTags: TagWithId[] = []
|
||||
|
||||
for (const tag of tags) {
|
||||
if (lastUsedOrder.includes(tag.id)) {
|
||||
lastUsedTags.push(tag)
|
||||
continue
|
||||
}
|
||||
remainingTags.push(tag)
|
||||
}
|
||||
|
||||
const sortByLastUsed = (a: TagWithId, b: TagWithId) => {
|
||||
return lastUsedOrder.indexOf(a.id) - lastUsedOrder.indexOf(b.id)
|
||||
}
|
||||
lastUsedTags.sort(sortByLastUsed)
|
||||
|
||||
this.sortedTags = [...lastUsedTags, ...remainingTags]
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to load tags'))
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
fileId: {
|
||||
immediate: true,
|
||||
async handler() {
|
||||
try {
|
||||
this.selectedTags = await fetchSelectedTags(this.fileId)
|
||||
this.$emit('has-tags', this.selectedTags.length > 0)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to load selected tags'))
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
|
||||
createOption(newDisplayName: string): Tag {
|
||||
for (const tag of this.sortedTags) {
|
||||
const { id, displayName, ...baseTag } = tag
|
||||
if (
|
||||
displayName === newDisplayName
|
||||
&& Object.entries(baseTag)
|
||||
.every(([key, value]) => defaultBaseTag[key] === value)
|
||||
) {
|
||||
// Return existing tag to prevent vue-select from thinking the tags are different and showing duplicate options
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return {
|
||||
...defaultBaseTag,
|
||||
displayName: newDisplayName,
|
||||
}
|
||||
},
|
||||
|
||||
handleInput(selectedTags: Tag[]) {
|
||||
/**
|
||||
* Filter out tags with no id to prevent duplicate selected options
|
||||
*
|
||||
* Created tags are added programmatically by `handleCreate()` with
|
||||
* their respective ids returned from the server
|
||||
*/
|
||||
this.selectedTags = selectedTags.filter(selectedTag => Boolean(selectedTag.id)) as TagWithId[]
|
||||
},
|
||||
|
||||
async handleSelect(tags: Tag[]) {
|
||||
const selectedTag = tags[tags.length - 1]
|
||||
if (!selectedTag.id) {
|
||||
// Ignore created tags handled by `handleCreate()`
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
await selectTag(this.fileId, selectedTag)
|
||||
const sortToFront = (a: TagWithId, b: TagWithId) => {
|
||||
if (a.id === selectedTag.id) {
|
||||
return -1
|
||||
} else if (b.id === selectedTag.id) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
this.sortedTags.sort(sortToFront)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to select tag'))
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async handleCreate(tag: Tag) {
|
||||
this.loading = true
|
||||
try {
|
||||
const id = await createTag(this.fileId, tag)
|
||||
const createdTag = { ...tag, id }
|
||||
this.sortedTags.unshift(createdTag)
|
||||
this.selectedTags.push(createdTag)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to create tag'))
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async handleDeselect(tag: Tag) {
|
||||
this.loading = true
|
||||
try {
|
||||
await deleteTag(this.fileId, tag)
|
||||
} catch (error) {
|
||||
showError(t('systemtags', 'Failed to delete tag'))
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label[for="system-tags-input"] {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
:deep {
|
||||
.vs__deselect {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Vincent Petry <vincent@nextcloud.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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function() {
|
||||
OCA.SystemTags = _.extend({}, OCA.SystemTags)
|
||||
if (!OCA.SystemTags) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.SystemTags = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.SystemTags.FilesPlugin = {
|
||||
ignoreLists: [
|
||||
'trashbin',
|
||||
'files.public',
|
||||
],
|
||||
|
||||
attach(fileList) {
|
||||
if (this.ignoreLists.indexOf(fileList.id) >= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// only create and attach once
|
||||
// FIXME: this should likely be done on a different code path now
|
||||
// for the sidebar to only have it registered once
|
||||
if (!OCA.SystemTags.View) {
|
||||
const systemTagsInfoView = new OCA.SystemTags.SystemTagsInfoView()
|
||||
fileList.registerDetailView(systemTagsInfoView)
|
||||
OCA.SystemTags.View = systemTagsInfoView
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.SystemTags.FilesPlugin)
|
||||
28
apps/systemtags/src/logger.ts
Normal file
28
apps/systemtags/src/logger.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* @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 { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
export const logger = getLoggerBuilder()
|
||||
.setApp('systemtags')
|
||||
.detectUser()
|
||||
.build()
|
||||
137
apps/systemtags/src/services/api.ts
Normal file
137
apps/systemtags/src/services/api.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* @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 axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import { davClient } from './davClient.js'
|
||||
import { formatTag, parseIdFromLocation, parseTags } from '../utils.js'
|
||||
import { logger } from '../logger.js'
|
||||
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import type { ServerTag, Tag, TagWithId } from '../types.js'
|
||||
|
||||
const fetchTagsBody = `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:id />
|
||||
<oc:display-name />
|
||||
<oc:user-visible />
|
||||
<oc:user-assignable />
|
||||
<oc:can-assign />
|
||||
</d:prop>
|
||||
</d:propfind>`
|
||||
|
||||
export const fetchTags = async (): Promise<TagWithId[]> => {
|
||||
const path = '/systemtags'
|
||||
try {
|
||||
const { data: tags } = await davClient.getDirectoryContents(path, {
|
||||
data: fetchTagsBody,
|
||||
details: true,
|
||||
glob: '/systemtags/*', // Filter out first empty tag
|
||||
}) as ResponseDataDetailed<Required<FileStat>[]>
|
||||
return parseTags(tags)
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to load tags'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to load tags'))
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchLastUsedTagIds = async (): Promise<number[]> => {
|
||||
const url = generateUrl('/apps/systemtags/lastused')
|
||||
try {
|
||||
const { data: lastUsedTagIds } = await axios.get<string[]>(url)
|
||||
return lastUsedTagIds.map(Number)
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to load last used tags'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to load last used tags'))
|
||||
}
|
||||
}
|
||||
|
||||
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> => {
|
||||
const path = '/systemtags'
|
||||
const tagToPost = formatTag(tag)
|
||||
try {
|
||||
const { headers } = await davClient.customRequest(path, {
|
||||
method: 'POST',
|
||||
data: tagToPost,
|
||||
})
|
||||
const contentLocation = headers.get('content-location')
|
||||
if (contentLocation) {
|
||||
const tagToPut = {
|
||||
...tagToPost,
|
||||
id: parseIdFromLocation(contentLocation),
|
||||
}
|
||||
await selectTag(fileId, tagToPut)
|
||||
return tagToPut.id
|
||||
}
|
||||
logger.error(t('systemtags', 'Missing "Content-Location" header'))
|
||||
throw new Error(t('systemtags', 'Missing "Content-Location" header'))
|
||||
} catch (error) {
|
||||
logger.error(t('systemtags', 'Failed to create tag'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to create tag'))
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteTag = async (fileId: number, tag: Tag): 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'), { error })
|
||||
throw new Error(t('systemtags', 'Failed to delete tag'))
|
||||
}
|
||||
}
|
||||
33
apps/systemtags/src/services/davClient.ts
Normal file
33
apps/systemtags/src/services/davClient.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* @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 { createClient } from 'webdav'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
|
||||
const rootUrl = generateRemoteUrl('dav')
|
||||
|
||||
export const davClient = createClient(rootUrl, {
|
||||
headers: {
|
||||
requesttoken: getRequestToken() ?? '',
|
||||
},
|
||||
})
|
||||
|
|
@ -23,8 +23,6 @@
|
|||
|
||||
import './app.js'
|
||||
import './systemtagsfilelist.js'
|
||||
import './filesplugin.js'
|
||||
import './systemtagsinfoview.js'
|
||||
import './css/systemtagsfilelist.scss'
|
||||
|
||||
window.OCA.SystemTags = OCA.SystemTags
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Vincent Petry <vincent@nextcloud.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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
(function(OCA) {
|
||||
|
||||
/**
|
||||
* @param {any} model -
|
||||
*/
|
||||
function modelToSelection(model) {
|
||||
const data = model.toJSON()
|
||||
if (!OC.isUserAdmin() && !data.canAssign) {
|
||||
data.locked = true
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* @class OCA.SystemTags.SystemTagsInfoView
|
||||
* @classdesc
|
||||
*
|
||||
* Displays a file's system tags
|
||||
*
|
||||
*/
|
||||
const SystemTagsInfoView = OCA.Files.DetailFileInfoView.extend(
|
||||
/** @lends OCA.SystemTags.SystemTagsInfoView.prototype */ {
|
||||
|
||||
_rendered: false,
|
||||
|
||||
className: 'systemTagsInfoView',
|
||||
name: 'systemTags',
|
||||
|
||||
/* required by the new files sidebar to check if the view is unique */
|
||||
id: 'systemTagsInfoView',
|
||||
|
||||
/**
|
||||
* @type {OC.SystemTags.SystemTagsInputField}
|
||||
*/
|
||||
_inputView: null,
|
||||
|
||||
initialize(options) {
|
||||
const self = this
|
||||
options = options || {}
|
||||
|
||||
this._inputView = new OC.SystemTags.SystemTagsInputField({
|
||||
multiple: true,
|
||||
allowActions: true,
|
||||
allowCreate: true,
|
||||
isAdmin: OC.isUserAdmin(),
|
||||
initSelection(element, callback) {
|
||||
callback(self.selectedTagsCollection.map(modelToSelection))
|
||||
},
|
||||
})
|
||||
|
||||
this.selectedTagsCollection = new OC.SystemTags.SystemTagsMappingCollection([], { objectType: 'files' })
|
||||
|
||||
this._inputView.collection.on('change:name', this._onTagRenamedGlobally, this)
|
||||
this._inputView.collection.on('remove', this._onTagDeletedGlobally, this)
|
||||
|
||||
this._inputView.on('select', this._onSelectTag, this)
|
||||
this._inputView.on('deselect', this._onDeselectTag, this)
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler whenever a tag was selected
|
||||
*
|
||||
* @param {object} tag the tag to create
|
||||
*/
|
||||
_onSelectTag(tag) {
|
||||
// create a mapping entry for this tag
|
||||
this.selectedTagsCollection.create(tag.toJSON())
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler whenever a tag gets deselected.
|
||||
* Removes the selected tag from the mapping collection.
|
||||
*
|
||||
* @param {string} tagId tag id
|
||||
*/
|
||||
_onDeselectTag(tagId) {
|
||||
this.selectedTagsCollection.get(tagId).destroy()
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler whenever a tag was renamed globally.
|
||||
*
|
||||
* This will automatically adjust the tag mapping collection to
|
||||
* container the new name.
|
||||
*
|
||||
* @param {OC.Backbone.Model} changedTag tag model that has changed
|
||||
*/
|
||||
_onTagRenamedGlobally(changedTag) {
|
||||
// also rename it in the selection, if applicable
|
||||
const selectedTagMapping = this.selectedTagsCollection.get(changedTag.id)
|
||||
if (selectedTagMapping) {
|
||||
selectedTagMapping.set(changedTag.toJSON())
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler whenever a tag was deleted globally.
|
||||
*
|
||||
* This will automatically adjust the tag mapping collection to
|
||||
* container the new name.
|
||||
*
|
||||
* @param {OC.Backbone.Model} tagId tag model that has changed
|
||||
*/
|
||||
_onTagDeletedGlobally(tagId) {
|
||||
// also rename it in the selection, if applicable
|
||||
this.selectedTagsCollection.remove(tagId)
|
||||
},
|
||||
|
||||
setFileInfo(fileInfo) {
|
||||
const self = this
|
||||
if (!this._rendered) {
|
||||
this.render()
|
||||
}
|
||||
|
||||
if (fileInfo) {
|
||||
this.selectedTagsCollection.setObjectId(fileInfo.id)
|
||||
this.selectedTagsCollection.fetch({
|
||||
success(collection) {
|
||||
collection.fetched = true
|
||||
|
||||
const appliedTags = collection.map(modelToSelection)
|
||||
self._inputView.setData(appliedTags)
|
||||
if (appliedTags.length > 0) {
|
||||
self.show()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.hide()
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders this details view
|
||||
*/
|
||||
render() {
|
||||
this.$el.append(this._inputView.$el)
|
||||
this._inputView.render()
|
||||
},
|
||||
|
||||
isVisible() {
|
||||
return !this.$el.hasClass('hidden')
|
||||
},
|
||||
|
||||
show() {
|
||||
this.$el.removeClass('hidden')
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.$el.addClass('hidden')
|
||||
},
|
||||
|
||||
toggle() {
|
||||
this.$el.toggleClass('hidden')
|
||||
},
|
||||
|
||||
openDropdown() {
|
||||
this.$el.find('.systemTagsInputField').select2('open')
|
||||
},
|
||||
|
||||
remove() {
|
||||
this._inputView.remove()
|
||||
},
|
||||
})
|
||||
|
||||
OCA.SystemTags.SystemTagsInfoView = SystemTagsInfoView
|
||||
|
||||
})(OCA)
|
||||
38
apps/systemtags/src/types.ts
Normal file
38
apps/systemtags/src/types.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export interface BaseTag {
|
||||
id?: number
|
||||
userVisible: boolean
|
||||
userAssignable: boolean
|
||||
readonly canAssign: boolean // Computed server-side
|
||||
}
|
||||
|
||||
export type Tag = BaseTag & {
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export type TagWithId = Required<Tag>
|
||||
|
||||
export type ServerTag = BaseTag & {
|
||||
name: string
|
||||
}
|
||||
66
apps/systemtags/src/utils.ts
Normal file
66
apps/systemtags/src/utils.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @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 camelCase from 'camelcase'
|
||||
|
||||
import type { FileStat } from 'webdav'
|
||||
|
||||
import type { ServerTag, Tag, TagWithId } from './types.js'
|
||||
|
||||
export const parseTags = (tags: Required<FileStat>[]): TagWithId[] => {
|
||||
return tags.map(({ props }) => Object.fromEntries(
|
||||
Object.entries(props)
|
||||
.map(([key, value]) => [camelCase(key), value])
|
||||
)) as TagWithId[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse id from `Content-Location` header
|
||||
*/
|
||||
export const parseIdFromLocation = (url: string): number => {
|
||||
const queryPos = url.indexOf('?')
|
||||
if (queryPos > 0) {
|
||||
url = url.substring(0, queryPos)
|
||||
}
|
||||
|
||||
const parts = url.split('/')
|
||||
let result
|
||||
do {
|
||||
result = parts[parts.length - 1]
|
||||
parts.pop()
|
||||
// note: first result can be empty when there is a trailing slash,
|
||||
// so we take the part before that
|
||||
} while (!result && parts.length > 0)
|
||||
|
||||
return Number(result)
|
||||
}
|
||||
|
||||
export const formatTag = (initialTag: Tag | ServerTag): ServerTag => {
|
||||
const tag: any = { ...initialTag }
|
||||
if (tag.name && !tag.displayName) {
|
||||
return tag
|
||||
}
|
||||
tag.name = tag.displayName
|
||||
delete tag.displayName
|
||||
|
||||
return tag
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* @copyright 2016 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Vincent Petry <vincent@nextcloud.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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('OCA.SystemTags.SystemTagsInfoView tests', function() {
|
||||
var isAdminStub;
|
||||
var view;
|
||||
var clock;
|
||||
|
||||
beforeEach(function() {
|
||||
clock = sinon.useFakeTimers();
|
||||
view = new OCA.SystemTags.SystemTagsInfoView();
|
||||
$('#testArea').append(view.$el);
|
||||
isAdminStub = sinon.stub(OC, 'isUserAdmin').returns(true);
|
||||
});
|
||||
afterEach(function() {
|
||||
isAdminStub.restore();
|
||||
clock.restore();
|
||||
view.remove();
|
||||
view = undefined;
|
||||
});
|
||||
describe('rendering', function() {
|
||||
it('renders input field view', function() {
|
||||
view.render();
|
||||
expect(view.$el.find('input[name=tags]').length).toEqual(1);
|
||||
});
|
||||
it('fetches selected tags then renders when setting file info', function() {
|
||||
var fetchStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'fetch');
|
||||
var setDataStub = sinon.stub(OC.SystemTags.SystemTagsInputField.prototype, 'setData');
|
||||
|
||||
expect(view.$el.hasClass('hidden')).toEqual(false);
|
||||
|
||||
view.setFileInfo({id: '123'});
|
||||
expect(view.$el.find('input[name=tags]').length).toEqual(1);
|
||||
|
||||
expect(fetchStub.calledOnce).toEqual(true);
|
||||
expect(view.selectedTagsCollection.url())
|
||||
.toEqual(OC.linkToRemote('dav') + '/systemtags-relations/files/123');
|
||||
|
||||
view.selectedTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '3', name: 'test3'}
|
||||
]);
|
||||
|
||||
fetchStub.yieldTo('success', view.selectedTagsCollection);
|
||||
expect(setDataStub.calledOnce).toEqual(true);
|
||||
expect(setDataStub.getCall(0).args[0]).toEqual([{
|
||||
id: '1', name: 'test1', userVisible: true, userAssignable: true, canAssign: true
|
||||
}, {
|
||||
id: '3', name: 'test3', userVisible: true, userAssignable: true, canAssign: true
|
||||
}]);
|
||||
|
||||
expect(view.$el.hasClass('hidden')).toEqual(false);
|
||||
|
||||
fetchStub.restore();
|
||||
setDataStub.restore();
|
||||
});
|
||||
it('overrides initSelection to use the local collection', function() {
|
||||
var inputViewSpy = sinon.spy(OC.SystemTags, 'SystemTagsInputField');
|
||||
var element = $('<input type="hidden" val="1,3"/>');
|
||||
view.remove();
|
||||
view = new OCA.SystemTags.SystemTagsInfoView();
|
||||
view.selectedTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '3', name: 'test3', userVisible: false, userAssignable: false, canAssign: false}
|
||||
]);
|
||||
|
||||
var callback = sinon.stub();
|
||||
inputViewSpy.getCall(0).args[0].initSelection(element, callback);
|
||||
|
||||
expect(callback.calledOnce).toEqual(true);
|
||||
expect(callback.getCall(0).args[0]).toEqual([{
|
||||
id: '1', name: 'test1', userVisible: true, userAssignable: true, canAssign: true
|
||||
}, {
|
||||
id: '3', name: 'test3', userVisible: false, userAssignable: false, canAssign: false
|
||||
}]);
|
||||
|
||||
inputViewSpy.restore();
|
||||
});
|
||||
it('sets locked flag on non-assignable tags when user is not an admin', function() {
|
||||
isAdminStub.returns(false);
|
||||
|
||||
var inputViewSpy = sinon.spy(OC.SystemTags, 'SystemTagsInputField');
|
||||
var element = $('<input type="hidden" val="1,3"/>');
|
||||
view.remove();
|
||||
view = new OCA.SystemTags.SystemTagsInfoView();
|
||||
view.selectedTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '3', name: 'test3', userAssignable: false, canAssign: false}
|
||||
]);
|
||||
|
||||
var callback = sinon.stub();
|
||||
inputViewSpy.getCall(0).args[0].initSelection(element, callback);
|
||||
|
||||
expect(callback.calledOnce).toEqual(true);
|
||||
expect(callback.getCall(0).args[0]).toEqual([{
|
||||
id: '1', name: 'test1', userVisible: true, userAssignable: true, canAssign: true
|
||||
}, {
|
||||
id: '3', name: 'test3', userVisible: true, userAssignable: false, canAssign: false, locked: true
|
||||
}]);
|
||||
|
||||
inputViewSpy.restore();
|
||||
});
|
||||
it('does not set locked flag on non-assignable tags when canAssign overrides it with true', function() {
|
||||
isAdminStub.returns(false);
|
||||
|
||||
var inputViewSpy = sinon.spy(OC.SystemTags, 'SystemTagsInputField');
|
||||
var element = $('<input type="hidden" val="1,4"/>');
|
||||
view.remove();
|
||||
view = new OCA.SystemTags.SystemTagsInfoView();
|
||||
view.selectedTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '4', name: 'test4', userAssignable: false, canAssign: true}
|
||||
]);
|
||||
|
||||
var callback = sinon.stub();
|
||||
inputViewSpy.getCall(0).args[0].initSelection(element, callback);
|
||||
|
||||
expect(callback.calledOnce).toEqual(true);
|
||||
expect(callback.getCall(0).args[0]).toEqual([{
|
||||
id: '1', name: 'test1', userVisible: true, userAssignable: true, canAssign: true
|
||||
}, {
|
||||
id: '4', name: 'test4', userVisible: true, userAssignable: false, canAssign: true
|
||||
}]);
|
||||
|
||||
inputViewSpy.restore();
|
||||
});
|
||||
});
|
||||
describe('events', function() {
|
||||
var allTagsCollection;
|
||||
beforeEach(function() {
|
||||
allTagsCollection = view._inputView.collection;
|
||||
|
||||
allTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '2', name: 'test2'},
|
||||
{id: '3', name: 'test3'}
|
||||
]);
|
||||
|
||||
view.selectedTagsCollection.add([
|
||||
{id: '1', name: 'test1'},
|
||||
{id: '3', name: 'test3'}
|
||||
]);
|
||||
view.render();
|
||||
});
|
||||
|
||||
it('renames model in selection collection on rename', function() {
|
||||
allTagsCollection.get('3').set('name', 'test3_renamed');
|
||||
|
||||
expect(view.selectedTagsCollection.get('3').get('name')).toEqual('test3_renamed');
|
||||
});
|
||||
|
||||
it('adds tag to selection collection when selected by input', function() {
|
||||
var createStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'create');
|
||||
view._inputView.trigger('select', allTagsCollection.get('2'));
|
||||
|
||||
expect(createStub.calledOnce).toEqual(true);
|
||||
expect(createStub.getCall(0).args[0]).toEqual({
|
||||
id: '2',
|
||||
name: 'test2',
|
||||
userVisible: true,
|
||||
userAssignable: true,
|
||||
canAssign: true
|
||||
});
|
||||
|
||||
createStub.restore();
|
||||
});
|
||||
it('removes tag from selection collection when deselected by input', function() {
|
||||
var destroyStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'destroy');
|
||||
view._inputView.trigger('deselect', '3');
|
||||
|
||||
expect(destroyStub.calledOnce).toEqual(true);
|
||||
expect(destroyStub.calledOn(view.selectedTagsCollection.get('3'))).toEqual(true);
|
||||
|
||||
destroyStub.restore();
|
||||
});
|
||||
|
||||
it('removes tag from selection whenever the tag was deleted globally', function() {
|
||||
expect(view.selectedTagsCollection.get('3')).not.toBeFalsy();
|
||||
|
||||
allTagsCollection.remove('3');
|
||||
|
||||
expect(view.selectedTagsCollection.get('3')).toBeFalsy();
|
||||
|
||||
});
|
||||
});
|
||||
describe('visibility', function() {
|
||||
it('reports visibility based on the "hidden" class name', function() {
|
||||
view.$el.addClass('hidden');
|
||||
|
||||
expect(view.isVisible()).toBeFalsy();
|
||||
|
||||
view.$el.removeClass('hidden');
|
||||
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
});
|
||||
it('is visible after rendering', function() {
|
||||
view.render();
|
||||
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
});
|
||||
it('shows and hides the element', function() {
|
||||
view.show();
|
||||
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
|
||||
view.hide();
|
||||
|
||||
expect(view.isVisible()).toBeFalsy();
|
||||
|
||||
view.show();
|
||||
|
||||
expect(view.isVisible()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('select2', function() {
|
||||
var select2Stub;
|
||||
|
||||
beforeEach(function() {
|
||||
select2Stub = sinon.stub($.fn, 'select2');
|
||||
});
|
||||
afterEach(function() {
|
||||
select2Stub.restore();
|
||||
});
|
||||
it('opens dropdown', function() {
|
||||
view.openDropdown();
|
||||
|
||||
expect(select2Stub.calledOnce).toBeTruthy();
|
||||
expect(select2Stub.withArgs('open')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +1 @@
|
|||
.systemtags-select2-dropdown .select2-result-label .checkmark{visibility:hidden;margin-left:-5px;margin-right:5px;padding:4px}.systemtags-select2-dropdown .select2-result-label .new-item .systemtags-actions{display:none}.systemtags-select2-dropdown .select2-selected .select2-result-label .checkmark{visibility:visible}.systemtags-select2-dropdown .select2-result-label .icon{display:inline-block;opacity:.5}.systemtags-select2-dropdown .select2-result-label .icon.rename{padding:4px}.systemtags-select2-dropdown .systemtags-actions{position:absolute;right:5px}.systemtags-select2-dropdown .systemtags-rename-form{display:inline-block;width:calc(100% - 20px);top:-6px;position:relative}.systemtags-select2-dropdown .systemtags-rename-form input{display:inline-block;height:30px;width:calc(100% - 40px)}.systemtags-select2-dropdown .label{width:85%;display:inline-block;overflow:hidden;text-overflow:ellipsis}.systemtags-select2-dropdown .label.hidden{display:none}.systemtags-select2-dropdown span{line-height:25px}.systemtags-select2-dropdown .systemtags-item{display:inline-block;height:25px;width:100%}.systemtags-select2-dropdown .select2-result-label{height:25px}.systemTagsInfoView,.systemtags-select2-container{width:100%}.systemTagsInfoView .select2-choices,.systemtags-select2-container .select2-choices{flex-wrap:nowrap !important;max-height:44px}.systemTagsInfoView .select2-choices .select2-search-choice.select2-locked .label,.systemtags-select2-container .select2-choices .select2-search-choice.select2-locked .label{opacity:.5}#select2-drop.systemtags-select2-dropdown .select2-results li.select2-result{padding:5px}/*# sourceMappingURL=systemtags.css.map */
|
||||
.systemtags-select2-dropdown .select2-result-label .checkmark{visibility:hidden;margin-left:-5px;margin-right:5px;padding:4px}.systemtags-select2-dropdown .select2-result-label .new-item .systemtags-actions{display:none}.systemtags-select2-dropdown .select2-selected .select2-result-label .checkmark{visibility:visible}.systemtags-select2-dropdown .select2-result-label .icon{display:inline-block;opacity:.5}.systemtags-select2-dropdown .select2-result-label .icon.rename{padding:4px}.systemtags-select2-dropdown .systemtags-actions{position:absolute;right:5px}.systemtags-select2-dropdown .systemtags-rename-form{display:inline-block;width:calc(100% - 20px);top:-6px;position:relative}.systemtags-select2-dropdown .systemtags-rename-form input{display:inline-block;height:30px;width:calc(100% - 40px)}.systemtags-select2-dropdown .label{width:85%;display:inline-block;overflow:hidden;text-overflow:ellipsis}.systemtags-select2-dropdown .label.hidden{display:none}.systemtags-select2-dropdown span{line-height:25px}.systemtags-select2-dropdown .systemtags-item{display:inline-block;height:25px;width:100%}.systemtags-select2-dropdown .select2-result-label{height:25px}.systemtags-select2-container{width:100%}.systemtags-select2-container .select2-choices{flex-wrap:nowrap !important;max-height:44px}.systemtags-select2-container .select2-choices .select2-search-choice.select2-locked .label{opacity:.5}#select2-drop.systemtags-select2-dropdown .select2-results li.select2-result{padding:5px}/*# sourceMappingURL=systemtags.css.map */
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["systemtags.scss"],"names":[],"mappings":"AAcE,8DACC,kBACA,iBACA,iBACA,YAED,iFACC,aAGF,gFACC,mBAED,yDACC,qBACA,WACA,gEACC,YAGF,iDACC,kBACA,UAED,qDACC,qBACA,wBACA,SACA,kBACA,2DACC,qBACA,YACA,wBAGF,oCACC,UACA,qBACA,gBACA,uBACA,2CACC,aAGF,kCACC,iBAED,8CACC,qBACA,YACA,WAED,mDACC,YAIF,kDAEC,WAEA,oFACC,4BACA,gBAGD,8KACC,WAIF,6EACC","file":"systemtags.css"}
|
||||
{"version":3,"sourceRoot":"","sources":["systemtags.scss"],"names":[],"mappings":"AAcE,8DACC,kBACA,iBACA,iBACA,YAED,iFACC,aAGF,gFACC,mBAED,yDACC,qBACA,WACA,gEACC,YAGF,iDACC,kBACA,UAED,qDACC,qBACA,wBACA,SACA,kBACA,2DACC,qBACA,YACA,wBAGF,oCACC,UACA,qBACA,gBACA,uBACA,2CACC,aAGF,kCACC,iBAED,8CACC,qBACA,YACA,WAED,mDACC,YAIF,8BACC,WAEA,+CACC,4BACA,gBAGD,4FACC,WAIF,6EACC","file":"systemtags.css"}
|
||||
|
|
@ -69,7 +69,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.systemTagsInfoView,
|
||||
.systemtags-select2-container {
|
||||
width: 100%;
|
||||
|
||||
|
|
|
|||
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
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/core-systemtags.js
vendored
4
dist/core-systemtags.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-systemtags.js.map
vendored
2
dist/core-systemtags.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.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
22
dist/files-sidebar.js.LICENSE.txt
vendored
22
dist/files-sidebar.js.LICENSE.txt
vendored
|
|
@ -1,3 +1,25 @@
|
|||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
|
|
|
|||
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_trashbin-main.js
vendored
4
dist/files_trashbin-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_trashbin-main.js.map
vendored
2
dist/files_trashbin-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/systemtags-systemtags.js
vendored
4
dist/systemtags-systemtags.js
vendored
File diff suppressed because one or more lines are too long
26
dist/systemtags-systemtags.js.LICENSE.txt
vendored
26
dist/systemtags-systemtags.js.LICENSE.txt
vendored
|
|
@ -21,32 +21,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015
|
||||
*
|
||||
* @author Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Vincent Petry <vincent@nextcloud.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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
|
|
|
|||
2
dist/systemtags-systemtags.js.map
vendored
2
dist/systemtags-systemtags.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -89,7 +89,16 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'babel-loader',
|
||||
use: [
|
||||
'babel-loader',
|
||||
{
|
||||
// Fix TypeScript syntax errors in Vue
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: BabelLoaderExcludeNodeModulesExcept([]),
|
||||
},
|
||||
{
|
||||
|
|
@ -210,6 +219,13 @@ module.exports = {
|
|||
vue$: path.resolve('./node_modules/vue'),
|
||||
},
|
||||
extensions: ['*', '.ts', '.js', '.vue'],
|
||||
extensionAlias: {
|
||||
/**
|
||||
* Resolve TypeScript files when using fully-specified esm import paths
|
||||
* https://github.com/webpack/webpack/issues/13252
|
||||
*/
|
||||
'.js': ['.js', '.ts'],
|
||||
},
|
||||
symlinks: true,
|
||||
fallback: {
|
||||
fs: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue