Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

9356 9362 tag form improvements #9525

Merged
merged 12 commits into from
Aug 3, 2023
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
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