Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module.exports = {
firstDay: true,
'cypress/globals': true,
},
parserOptions: {
parser: '@typescript-eslint/parser',
},
plugins: [
'cypress',
],
Expand Down
38 changes: 29 additions & 9 deletions apps/files/src/views/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
},

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -533,4 +546,11 @@ export default {
}
}
}

.sidebar__description {
display: flex;
flex-direction: column;
width: 100%;
gap: 8px 0;
}
</style>
235 changes: 235 additions & 0 deletions apps/systemtags/src/components/SystemTags.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<!--
- @copyright 2023 Christopher Ng <[email protected]>
-
- @author Christopher Ng <[email protected]>
-
- @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>
Loading