Skip to content

Commit

Permalink
feat(systemtags): add colors in bulk tagging action
Browse files Browse the repository at this point in the history
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Nov 15, 2024
1 parent e433c88 commit d7df8e8
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 40 deletions.
156 changes: 128 additions & 28 deletions apps/systemtags/src/components/SystemTagPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,54 @@
</div>

<!-- Tags list -->
<div class="systemtags-picker__tags"
<ul class="systemtags-picker__tags"
data-cy-systemtags-picker-tags>
<NcCheckboxRadioSwitch v-for="tag in filteredTags"
<li v-for="tag in filteredTags"
:key="tag.id"
:label="tag.displayName"
:checked="isChecked(tag)"
:indeterminate="isIndeterminate(tag)"
:disabled="!tag.canAssign"
:data-cy-systemtags-picker-tag="tag.id"
class="systemtags-picker__tag"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>
<NcButton v-if="canCreateTag"
:disabled="status === Status.CREATING_TAG"
alignment="start"
class="systemtags-picker__tag-create"
native-type="submit"
type="tertiary"
data-cy-systemtags-picker-button-create
@click="onNewTag">
{{ input.trim() }}<br>
<span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
<template #icon>
<PlusIcon />
</template>
</NcButton>
</div>
:style="tagListStyle(tag)"
class="systemtags-picker__tag">
<NcCheckboxRadioSwitch :checked="isChecked(tag)"
:disabled="!tag.canAssign"
:indeterminate="isIndeterminate(tag)"
:label="tag.displayName"
class="systemtags-picker__tag-checkbox"
@update:checked="onCheckUpdate(tag, $event)">
{{ formatTagName(tag) }}
</NcCheckboxRadioSwitch>

<!-- Color picker -->
<NcColorPicker :data-cy-systemtags-picker-tag-color="tag.id"
:value="`#${tag.color || primaryColor}`"
class="systemtags-picker__tag-color"
@update:value="onColorChange(tag, $event)">
<NcButton :aria-label="t('systemtags', 'Change tag color')" type="tertiary">
<template #icon>
<CircleIcon :size="24" />
<PencilIcon />
</template>
</NcButton>
</NcColorPicker>
</li>

<!-- Create new tag -->
<li>
<NcButton v-if="canCreateTag"
:disabled="status === Status.CREATING_TAG"
alignment="start"
class="systemtags-picker__tag-create"
native-type="submit"
type="tertiary"
data-cy-systemtags-picker-button-create
@click="onNewTag">
{{ input.trim() }}<br>
<span class="systemtags-picker__tag-create-subline">{{ t('systemtags', 'Create new tag') }}</span>
<template #icon>
<PlusIcon />
</template>
</NcButton>
</li>
</ul>

<!-- Note -->
<div class="systemtags-picker__note">
Expand Down Expand Up @@ -110,19 +130,30 @@ import escapeHTML from 'escape-html'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcChip from '@nextcloud/vue/dist/Components/NcChip.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import TagIcon from 'vue-material-design-icons/Tag.vue'
import CheckIcon from 'vue-material-design-icons/CheckCircle.vue'
import CircleIcon from 'vue-material-design-icons/Circle.vue'
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
import PlusIcon from 'vue-material-design-icons/Plus.vue'
import TagIcon from 'vue-material-design-icons/Tag.vue'

import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects, updateTag, updateTagColor } from '../services/api'
import { getNodeSystemTags, setNodeSystemTags } from '../utils'
import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api'
import { elementColor, invertTextColor, isDarkModeEnabled } from '../utils/colorUtils'
import logger from '../services/logger'

const primaryColor = getComputedStyle(document.body)
.getPropertyValue('--color-primary-element')
.replace('#', '') || '0069c3'
const mainBackgroundColor = getComputedStyle(document.body)
.getPropertyValue('--color-main-background')
.replace('#', '') || (isDarkModeEnabled() ? '000000' : 'ffffff')

type TagListCount = {
string: number
}
Expand All @@ -139,15 +170,18 @@ export default defineComponent({

components: {
CheckIcon,
CircleIcon,
NcButton,
NcCheckboxRadioSwitch,
// eslint-disable-next-line vue/no-unused-components
NcChip,
NcColorPicker,
NcDialog,
NcEmptyContent,
NcLoadingIcon,
NcNoteCard,
NcTextField,
PencilIcon,
PlusIcon,
TagIcon,
},
Expand All @@ -162,6 +196,7 @@ export default defineComponent({
setup() {
return {
emit,
primaryColor,
Status,
t,
}
Expand Down Expand Up @@ -329,7 +364,14 @@ export default defineComponent({
// Format & sanitize a tag chip for v-html tag rendering
formatTagChip(tag: TagWithId): string {
const chip = this.$refs.chip as NcChip
const chipHtml = chip.$el.outerHTML
const chipCloneEl = chip.$el.cloneNode(true) as HTMLElement
if (tag.color) {
const style = this.tagListStyle(tag)
Object.entries(style).forEach(([key, value]) => {
chipCloneEl.style.setProperty(key, value)
})
}
const chipHtml = chipCloneEl.outerHTML
return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName)))
},

Expand All @@ -345,6 +387,11 @@ export default defineComponent({
return tag.displayName
},

onColorChange(tag: TagWithId, color: string) {
tag.color = color.replace('#', '')
updateTag(tag)
},

isChecked(tag: TagWithId): boolean {
return tag.displayName in this.tagList
&& this.tagList[tag.displayName] === this.nodes.length
Expand Down Expand Up @@ -480,6 +527,17 @@ export default defineComponent({
showInfo(t('systemtags', 'File tags modification canceled'))
this.$emit('close', null)
},

tagListStyle(tag: TagWithId): Record<string, string> {
const primaryElement = elementColor(`#${tag.color || primaryColor}`, `#${mainBackgroundColor}`)
const textColor = invertTextColor(primaryElement) ? '#000000' : '#ffffff'
return {
'--color-primary': primaryElement,
'--color-primary-text': textColor,
'--color-primary-element': primaryElement,
'--color-primary-element-text': textColor,
}
},
},
})
</script>
Expand All @@ -506,6 +564,48 @@ export default defineComponent({
gap: var(--default-grid-baseline);
display: flex;
flex-direction: column;

li {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;

// Make switch full width
:deep(.checkbox-radio-switch) {
width: 100%;

.checkbox-content {
// adjust width
max-width: none;
// recalculate padding
box-sizing: border-box;
min-height: calc(var(--default-grid-baseline) * 2 + var(--default-clickable-area));
}
}
}

.systemtags-picker__tag-color button {
margin-inline-start: calc(var(--default-grid-baseline) * 2);
color: var(--color-primary-element);

span.pencil-icon {
display: none;
color: var(--color-main-text);
}

&:focus,
&:hover,
&[aria-expanded='true'] {
.pencil-icon {
display: block;
}
.circle-icon {
display: none;
}
}
}

.systemtags-picker__tag-create {
:deep(span) {
text-align: start;
Expand Down
11 changes: 3 additions & 8 deletions apps/systemtags/src/files_actions/bulkSystemTagsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { Permission, type Node } from '@nextcloud/files'
import { type Node } from '@nextcloud/files'

import { defineAsyncComponent } from 'vue'
import { FileAction } from '@nextcloud/files'
Expand Down Expand Up @@ -38,13 +38,8 @@ export const action = new FileAction({
return false
}

// Disabled for non dav resources
if (nodes.some((node) => !node.isDavRessource)) {
return false
}

// We need to have the update permission on all nodes
return !nodes.some((node) => (node.permissions & Permission.UPDATE) === 0)
// If the user is not logged in, the action is not available
return true
},

async exec(node: Node) {
Expand Down
6 changes: 4 additions & 2 deletions apps/systemtags/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { formatTag, parseIdFromLocation, parseTags } from '../utils'
import { logger } from '../logger.js'

export const fetchTagsPayload = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:id />
<oc:display-name />
<oc:user-visible />
<oc:user-assignable />
<oc:can-assign />
<d:getetag />
<nc:color />
</d:prop>
</d:propfind>`

Expand Down Expand Up @@ -98,12 +99,13 @@ export const createTag = async (tag: Tag | ServerTag): Promise<number> => {
export const updateTag = async (tag: TagWithId): Promise<void> => {
const path = '/systemtags/' + tag.id
const data = `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
<oc:display-name>${tag.displayName}</oc:display-name>
<oc:user-visible>${tag.userVisible}</oc:user-visible>
<oc:user-assignable>${tag.userAssignable}</oc:user-assignable>
<nc:color>${tag.color}</nc:color>
</d:prop>
</d:set>
</d:propertyupdate>`
Expand Down
2 changes: 2 additions & 0 deletions apps/systemtags/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface BaseTag {
userVisible: boolean
userAssignable: boolean
readonly canAssign: boolean // Computed server-side
etag?: string
color?: string
}

export type Tag = BaseTag & {
Expand Down
Loading

0 comments on commit d7df8e8

Please sign in to comment.