Skip to content

Commit

Permalink
9356 9362 tag form improvements (#9525)
Browse files Browse the repository at this point in the history
* tag styling complete

* auto save complete

* show only not selected tags as available

* WIP button contextual helper for tags

* fixed unit tests

* linting

* set options, type hinting

* fixed after refactoring

* added changelog, PR improvements

* linting

* fixed e2e tests

* fixed bug for chrome
  • Loading branch information
grimmoc authored and AlexAndBear committed Dec 13, 2023
1 parent f60adc2 commit 50e8e15
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 66 deletions.
10 changes: 10 additions & 0 deletions changelog/unreleased/enhancement-tags-form
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Enhancement: Tags form improved

We've improved the tags form in various ways, including visual appearance as well as usability. Auto save, remove tags on backspace, and contextual helper (and more, see issues)

https://github.com/owncloud/web/pull/9525
https://github.com/owncloud/web/issues/9363
https://github.com/owncloud/web/issues/9356
https://github.com/owncloud/web/issues/9360
https://github.com/owncloud/web/issues/9362
https://github.com/owncloud/web/issues/9416
24 changes: 23 additions & 1 deletion packages/design-system/src/components/OcSelect/OcSelect.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<div>
<label :for="id" class="oc-label" v-text="label" />
<oc-contextual-helper
v-if="contextualHelper?.isEnabled"
v-bind="contextualHelper?.data"
class="oc-pl-xs"
></oc-contextual-helper>
<vue-select
ref="select"
:disabled="disabled || readOnly"
Expand Down Expand Up @@ -81,9 +86,18 @@
import Fuse from 'fuse.js'
import uniqueId from '../../utils/uniqueId'
import VueSelect from 'vue-select'
import { defineComponent, ComponentPublicInstance, onMounted, ref, unref, VNodeRef } from 'vue'
import {
defineComponent,
ComponentPublicInstance,
onMounted,
ref,
unref,
VNodeRef,
PropType
} from 'vue'
import { useGettext } from 'vue3-gettext'
import 'vue-select/dist/vue-select.css'
import { ContextualHelper } from 'design-system/src/helpers'
/**
* Select component with a trigger and dropdown based on [Vue Select](https://vue-select.org/)
Expand Down Expand Up @@ -144,6 +158,14 @@ export default defineComponent({
type: String,
default: null
},
/**
* oc-contextual-helper can be injected here
*/
contextualHelper: {
type: Object as PropType<ContextualHelper>,
required: false,
default: null
},
/**
* Key to use as label when option is an object
* NOTE: this maps to the vue-select prop `label`
Expand Down
22 changes: 22 additions & 0 deletions packages/design-system/src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import { ConfigurationManager } from 'web-pkg'

export type IconFillType = 'fill' | 'line' | 'none'
export type IconType = {
name: string
color?: string
fillType?: IconFillType
}

export interface ContextualHelperDataListItem {
text: string
headline?: boolean
}
export interface ContextualHelperData {
title: string
text?: string
list?: ContextualHelperDataListItem[]
readMoreLink?: string
}

export interface ContextualHelperOptions {
configurationManager: ConfigurationManager
}

export interface ContextualHelper {
isEnabled: boolean
data: ContextualHelperData
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ exports[`Spaces view loading states should render spaces list after loading has
<label class="oc-page-size-label" for="oc-page-size-3">Items per page</label>
<div class="oc-page-size-select" input-id="oc-page-size-3" options="20,50,100,250">
<label class="oc-label" for="oc-select-4"></label>
<!--v-if-->
<div class="v-select vs--single vs--unsearchable oc-select oc-page-size-select" dir="auto" style="background: transparent;">
<div aria-expanded="false" aria-label="Search for option" aria-owns="vs2__listbox" class="vs__dropdown-toggle" id="vs2__combobox" role="combobox">
<div class="vs__selected-options">
Expand Down
116 changes: 82 additions & 34 deletions packages/web-app-files/src/components/SideBar/TagsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,63 @@
class="oc-mb-s"
:multiple="true"
:options="availableTags"
:contextual-helper="contextualHelper"
taggable
push-tags
:select-on-key-codes="[keycode('enter'), keycode(',')]"
:label="$gettext('Add or edit tags')"
:create-option="createOption"
:selectable="isOptionSelectable"
:fix-message-line="true"
:map-keydown="keydownMethods"
@update:model-value="save"
>
<template #selected-option="{ label }">
<span class="oc-flex oc-flex-center">
<avatar-image
class="oc-flex oc-align-self-center oc-mr-s"
:width="16.8"
:userid="label"
:user-name="label"
/>
<span>{{ label }}</span>
</span>
<template #selected-option-container="{ option, deselect }">
<oc-tag class="tags-control-tag oc-ml-xs" :rounded="true" size="small">
<oc-icon name="price-tag-3" size="small" />
<span class="oc-text-truncate">{{ option.label }}</span>
<span class="oc-flex oc-flex-middle oc-ml-s oc-mr-xs">
<oc-icon v-if="option.readonly" class="vs__deselect-lock" name="lock" size="small" />
<oc-button
v-else
appearance="raw"
:title="$gettext('Deselect %{label}', { label: option.label })"
:aria-label="$gettext('Deselect %{label}', { label: option.label })"
class="vs__deselect oc-mx-rm"
@mousedown.stop.prevent
@click="deselect(option)"
>
<oc-icon name="close" size="small" />
</oc-button>
</span>
</oc-tag>
</template>
<template #option="{ label, error }">
<div class="oc-flex">
<span v-if="showSelectNewLabel({ label })" class="oc-mr-s" v-text="$gettext('New')" />
<span class="oc-flex oc-flex-center">
<avatar-image
class="oc-flex oc-align-self-center oc-mr-s"
:width="16.8"
:userid="label"
:user-name="label"
/>
<span>{{ label }}</span>
<oc-tag class="tags-control-tag oc-ml-xs" :rounded="true" size="small">
<oc-icon name="price-tag-3" size="small" />
<span class="oc-text-truncate">{{ label }}</span>
</oc-tag>
</span>
</div>
<div v-if="error" class="oc-text-input-danger">{{ error }}</div>
</template>
</oc-select>
<compare-save-dialog
class="edit-compare-save-dialog oc-mb-l"
:original-object="{ tags: currentTags.map((t) => t.label) }"
:compare-object="{ tags: selectedTags.map((t) => t.label) }"
@revert="revertChanges"
@confirm="save"
></compare-save-dialog>
</div>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, inject, onMounted, ref, unref, VNodeRef, watch } from 'vue'
import CompareSaveDialog from 'web-pkg/src/components/SideBar/CompareSaveDialog.vue'
import { eventBus } from 'web-pkg/src/services/eventBus'
import { useTask } from 'vue-concurrency'
import { useClientService, useStore } from 'web-pkg/src/composables'
import { useClientService, useConfigurationManager, useStore } from 'web-pkg/src/composables'
import { Resource } from 'web-client'
import diff from 'lodash-es/difference'
import { useGettext } from 'vue3-gettext'
import keycode from 'keycode'
import { tagsHelper } from 'web-app-files/src/helpers/contextualHelpers'
import { ContextualHelper } from 'design-system/src/helpers'
const tagsMaxCount = 100
Expand All @@ -76,18 +77,17 @@ type TagOption = {
export default defineComponent({
name: 'TagsPanel',
components: {
CompareSaveDialog
},
setup() {
const store = useStore()
const clientService = useClientService()
const { $gettext } = useGettext()
const configurationManager = useConfigurationManager()
const injectedResource = inject<Resource>('resource')
const resource = computed<Resource>(() => unref(injectedResource))
const selectedTags = ref<TagOption[]>([])
const availableTags = ref<TagOption[]>([])
let allTags: string[] = []
const tagSelect: VNodeRef = ref(null)
const currentTags = computed<TagOption[]>(() => {
Expand All @@ -98,7 +98,12 @@ export default defineComponent({
const {
data: { value: tags = [] }
} = yield clientService.graphAuthenticated.tags.getTags()
availableTags.value = [...tags.map((t) => ({ label: t }))]
allTags = tags
const selectedLabels = new Set(unref(selectedTags).map((o) => o.label))
availableTags.value = tags
.filter((t) => selectedLabels.has(t) === false)
.map((t) => ({ label: t }))
})
const revertChanges = () => {
Expand All @@ -121,8 +126,18 @@ export default defineComponent({
return !unref(tagSelect).$refs.select.optionExists(option)
}
const save = async () => {
const save = async (e: TagOption[] | string[]) => {
try {
selectedTags.value = e.map((x) => (typeof x === 'object' ? x : { label: x }))
const allAvailableTags = new Set([...allTags, ...unref(availableTags).map((t) => t.label)])
availableTags.value = diff(
Array.from(allAvailableTags),
unref(selectedTags).map((o) => o.label)
).map((label) => ({
label
}))
const { id, tags, fileId } = unref(resource)
const selectedTagLabels = unref(selectedTags).map((t) => t.label)
const tagsToAdd = diff(selectedTagLabels, tags)
Expand All @@ -149,6 +164,9 @@ export default defineComponent({
})
eventBus.publish('sidebar.entity.saved')
if (unref(tagSelect) !== null) {
unref(tagSelect).$refs.search.focus()
}
} catch (e) {
console.error(e)
store.dispatch('showErrorMessage', {
Expand All @@ -161,6 +179,7 @@ export default defineComponent({
watch(resource, () => {
if (unref(resource)?.tags) {
revertChanges()
loadAvailableTagsTask.perform()
}
})
Expand All @@ -171,6 +190,29 @@ export default defineComponent({
loadAvailableTagsTask.perform()
})
const keydownMethods = (map, vm) => {
const objectMapping = {
...map
}
objectMapping[keycode('backspace')] = async (e) => {
if (e.target.value || selectedTags.value.length === 0) {
return
}
e.preventDefault()
availableTags.value.push(selectedTags.value.pop())
await save(unref(selectedTags))
}
return objectMapping
}
const contextualHelper = {
isEnabled: configurationManager.options.contextHelpers,
data: tagsHelper({ configurationManager: configurationManager })
} as ContextualHelper
return {
loadAvailableTagsTask,
availableTags,
Expand All @@ -184,7 +226,9 @@ export default defineComponent({
isOptionSelectable,
showSelectNewLabel,
save,
keycode
keycode,
keydownMethods,
contextualHelper
}
}
})
Expand All @@ -196,4 +240,8 @@ export default defineComponent({
border-radius: 5px;
}
}
.tags-control-tag {
max-width: 12rem;
height: var(--oc-space-large);
}
</style>
32 changes: 16 additions & 16 deletions packages/web-app-files/src/helpers/contextualHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
import { ConfigurationManager } from 'web-pkg'
import { omit } from 'lodash-es'
import { ContextualHelperData, ContextualHelperOptions } from 'design-system/src/helpers'

// just a dummy function to trick gettext tools
function $gettext(msg) {
return msg
}

export interface ContextualHelperDataListItem {
text: string
headline?: boolean
}
export interface ContextualHelperData {
title: string
text?: string
list?: ContextualHelperDataListItem[]
readMoreLink?: string
}

export interface ContextualHelperOptions {
configurationManager: ConfigurationManager
}

export const shareInviteCollaboratorHelp = (options: ContextualHelperOptions) =>
filterContextHelper(
{
Expand Down Expand Up @@ -154,3 +139,18 @@ const filterContextHelper = (
}
return data
}

export const tagsHelper = (options: ContextualHelperOptions) =>
filterContextHelper(
{
title: $gettext('Who can view tags?'),
list: [
{
text: $gettext(
'Everyone who can view the file can view its tags. Likewise, everyone who can edit the file can edit its tags.'
)
}
]
},
options
)
Loading

0 comments on commit 50e8e15

Please sign in to comment.