]> source.dussan.org Git - nextcloud-server.git/commitdiff
fix(systemtags): enhance create tag in tag picker UX
authorskjnldsv <skjnldsv@protonmail.com>
Thu, 7 Nov 2024 10:47:09 +0000 (11:47 +0100)
committerskjnldsv <skjnldsv@protonmail.com>
Fri, 15 Nov 2024 10:09:56 +0000 (11:09 +0100)
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
apps/systemtags/src/components/SystemTagPicker.vue
apps/systemtags/src/services/api.ts
cypress/e2e/systemtags/files-bulk-action.cy.ts

index 07ce186d349d868e611757249f6741d60338f440..8ed26ce9cb3b06f203f830250c82badb0ea5a067 100644 (file)
 
                <template v-else>
                        <!-- Search or create input -->
-                       <form class="systemtags-picker__create" @submit.stop.prevent="onNewTag">
+                       <div class="systemtags-picker__input">
                                <NcTextField :value.sync="input"
                                        :label="t('systemtags', 'Search or create tag')"
                                        data-cy-systemtags-picker-input>
                                        <TagIcon :size="20" />
                                </NcTextField>
-                               <NcButton :disabled="status === Status.CREATING_TAG"
-                                       native-type="submit"
-                                       data-cy-systemtags-picker-input-submit>
-                                       {{ t('systemtags', 'Create tag') }}
-                               </NcButton>
-                       </form>
+                       </div>
 
                        <!-- Tags list -->
-                       <div v-if="filteredTags.length > 0"
-                               class="systemtags-picker__tags"
+                       <div class="systemtags-picker__tags"
                                data-cy-systemtags-picker-tags>
                                <NcCheckboxRadioSwitch v-for="tag in filteredTags"
                                        :key="tag.id"
                                        :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>
-                       <NcEmptyContent v-else :name="t('systemtags', 'No tags found')">
-                               <template #icon>
-                                       <TagIcon />
-                               </template>
-                       </NcEmptyContent>
 
                        <!-- Note -->
                        <div class="systemtags-picker__note">
@@ -113,6 +117,7 @@ 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 PlusIcon from 'vue-material-design-icons/Plus.vue'
 
 import { getNodeSystemTags, setNodeSystemTags } from '../utils'
 import { createTag, fetchTag, fetchTags, getTagObjects, setTagObjects } from '../services/api'
@@ -143,6 +148,7 @@ export default defineComponent({
                NcLoadingIcon,
                NcNoteCard,
                NcTextField,
+               PlusIcon,
                TagIcon,
        },
 
@@ -176,12 +182,17 @@ export default defineComponent({
        },
 
        computed: {
+               sortedTags(): TagWithId[] {
+                       return [...this.tags]
+                               .sort((a, b) => a.displayName.localeCompare(b.displayName, getLanguage(), { ignorePunctuation: true }))
+               },
+
                filteredTags(): TagWithId[] {
                        if (this.input.trim() === '') {
-                               return this.tags
+                               return this.sortedTags
                        }
 
-                       return this.tags
+                       return this.sortedTags
                                .filter(tag => tag.displayName.normalize().includes(this.input.normalize()))
                },
 
@@ -189,6 +200,11 @@ export default defineComponent({
                        return this.toAdd.length > 0 || this.toRemove.length > 0
                },
 
+               canCreateTag(): boolean {
+                       return this.input.trim() !== ''
+                               && !this.tags.some(tag => tag.displayName.trim().toLocaleLowerCase() === this.input.trim().toLocaleLowerCase())
+               },
+
                statusMessage(): string {
                        if (this.toAdd.length === 0 && this.toRemove.length === 0) {
                                // should not happen™
@@ -199,7 +215,7 @@ export default defineComponent({
                                return n(
                                        'systemtags',
                                        '{tag1} will be set and {tag2} will be removed from 1 file.',
-                                       '{tag1} and {tag2} will be set and removed from {count} files.',
+                                       '{tag1} will be set and {tag2} will be removed from {count} files.',
                                        this.nodes.length,
                                        {
                                                tag1: this.formatTagChip(this.toAdd[0]),
@@ -368,6 +384,15 @@ export default defineComponent({
 
                                // Check the newly created tag
                                this.onCheckUpdate(tag, true)
+
+                               // Scroll to the newly created tag
+                               await this.$nextTick()
+                               const newTagEl = this.$el.querySelector(`input[type="checkbox"][label="${tag.displayName}"]`)
+                               newTagEl?.scrollIntoView({
+                                       behavior: 'instant',
+                                       block: 'center',
+                                       inline: 'center',
+                               })
                        } catch (error) {
                                showError((error as Error)?.message || t('systemtags', 'Failed to create tag'))
                        } finally {
@@ -461,22 +486,33 @@ export default defineComponent({
 
 <style scoped lang="scss">
 // Common sticky properties
-.systemtags-picker__create,
+.systemtags-picker__input,
 .systemtags-picker__note {
        position: sticky;
        z-index: 9;
        background-color: var(--color-main-background);
 }
 
-.systemtags-picker__create {
+.systemtags-picker__input {
        display: flex;
        top: 0;
        gap: 8px;
        padding-block-end: 8px;
        align-items: flex-end;
+}
 
-       button {
-               flex-shrink: 0;
+.systemtags-picker__tags {
+       padding-block: 8px;
+       gap: var(--default-grid-baseline);
+       display: flex;
+       flex-direction: column;
+       .systemtags-picker__tag-create {
+               :deep(span) {
+                       text-align: start;
+               }
+               &-subline {
+                       font-weight: normal;
+               }
        }
 }
 
index 4f202e07522952b5ea6558e625220320dd21c5aa..3262ccd3a878fb52e0361f932220a48c6b56676e 100644 (file)
@@ -46,7 +46,7 @@ export const fetchTag = async (tagId: number): Promise<TagWithId> => {
        try {
                const { data: tag } = await davClient.stat(path, {
                        data: fetchTagsPayload,
-                       details: true
+                       details: true,
                }) as ResponseDataDetailed<Required<FileStat>>
                return parseTags([tag])[0]
        } catch (error) {
index bfc2280a9d871bc742d64e128c1226bd10b9e2c7..9614b445f9bb1cc3c66b1978224db2d0194a9c8d 100644 (file)
@@ -326,7 +326,10 @@ describe('Systemtags: Files bulk action', { testIsolation: false }, () => {
 
                        const newTag = randomBytes(8).toString('base64').slice(0, 6)
                        cy.get('[data-cy-systemtags-picker-input]').type(newTag)
-                       cy.get('[data-cy-systemtags-picker-input-submit]').click()
+
+                       cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 0)
+                       cy.get('[data-cy-systemtags-picker-button-create]').should('be.visible')
+                       cy.get('[data-cy-systemtags-picker-button-create]').click()
 
                        cy.wait('@createTag')
                        cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 6)