Skip to content

Commit 9b5eac6

Browse files
Merge pull request #6856 from nextcloud/enh/6817-Suggestions-on-smart-picking
fix(files): create suggestions bar
2 parents 2badc7a + a3a58e0 commit 9b5eac6

File tree

6 files changed

+225
-8
lines changed

6 files changed

+225
-8
lines changed

src/components/Editor.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
</template>
5252
<ContentContainer v-show="contentLoaded"
5353
ref="contentWrapper" />
54+
<SuggestionsBar v-if="isRichEditor && contentLoaded" />
5455
</MainContainer>
5556
<Reader v-if="isResolvingConflict"
5657
:content="syncError.data.outsideChange"
@@ -125,6 +126,7 @@ import Translate from './Modal/Translate.vue'
125126
import CollisionResolveDialog from './CollisionResolveDialog.vue'
126127
import { generateRemoteUrl } from '@nextcloud/router'
127128
import { fetchNode } from '../services/WebdavClient.ts'
129+
import SuggestionsBar from './SuggestionsBar.vue'
128130
129131
export default {
130132
name: 'Editor',
@@ -141,6 +143,7 @@ export default {
141143
Status,
142144
Assistant,
143145
Translate,
146+
SuggestionsBar,
144147
},
145148
mixins: [
146149
isMobile,

src/components/Menu/ActionInsertLink.vue

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
<script>
6767
import { NcActions, NcActionButton, NcActionInput } from '@nextcloud/vue'
6868
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
69-
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'
7069
import { generateUrl } from '@nextcloud/router'
7170
import { loadState } from '@nextcloud/initial-state'
7271
@@ -76,6 +75,7 @@ import { Document, Loading, LinkOff, Web, Shape } from '../icons.js'
7675
import { BaseActionEntry } from './BaseActionEntry.js'
7776
import { useFileMixin } from '../Editor.provider.js'
7877
import { useMenuIDMixin } from './MenuBar.provider.js'
78+
import { buildFilePicker } from '../../helpers/filePicker.js'
7979
8080
export default {
8181
name: 'ActionInsertLink',
@@ -122,12 +122,7 @@ export default {
122122
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
123123
}
124124
125-
const filePicker = getFilePickerBuilder(t('text', 'Select file or folder to link to'))
126-
.startAt(this.startPath)
127-
.allowDirectories(true)
128-
.setMultiSelect(false)
129-
.setType(FilePickerType.Choose)
130-
.build()
125+
const filePicker = buildFilePicker(this.startPath)
131126
132127
filePicker.pick()
133128
.then((file) => {

src/components/SuggestionsBar.vue

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<div v-if="isEmptyContent" class="container-suggestions">
8+
<NcButton ref="linkFileOrFolder"
9+
type="secondary"
10+
size="normal"
11+
class="suggestions--button"
12+
@click="linkFile">
13+
<template #icon>
14+
<Document :size="20" />
15+
</template>
16+
<template v-if="!isMobile" #default>
17+
{{ t('text', 'Link to file or folder') }}
18+
</template>
19+
</NcButton>
20+
21+
<NcButton type="secondary"
22+
size="normal"
23+
class="suggestions--button"
24+
@click="$callChooseLocalAttachment">
25+
<template #icon>
26+
<Upload :size="20" />
27+
</template>
28+
<template v-if="!isMobile" #default>
29+
{{ t('text', 'Upload') }}
30+
</template>
31+
</NcButton>
32+
33+
<NcButton type="secondary"
34+
size="normal"
35+
class="suggestions--button"
36+
@click="insertTable">
37+
<template #icon>
38+
<TableIcon :size="20" />
39+
</template>
40+
<template v-if="!isMobile" #default>
41+
{{ t('text', 'Insert Table') }}
42+
</template>
43+
</NcButton>
44+
45+
<NcButton type="secondary"
46+
size="normal"
47+
class="suggestions--button"
48+
@click="linkPicker">
49+
<template #icon>
50+
<Shape :size="20" />
51+
</template>
52+
<template v-if="!isMobile" #default>
53+
{{ t('text', 'Smart Picker') }}
54+
</template>
55+
</NcButton>
56+
</div>
57+
</template>
58+
59+
<script>
60+
import { NcButton } from '@nextcloud/vue'
61+
import { Document, Shape, Upload, Table as TableIcon } from '../components/icons.js'
62+
import { useActionChooseLocalAttachmentMixin } from './Editor/MediaHandler.provider.js'
63+
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
64+
import { useEditorMixin, useFileMixin } from './Editor.provider.js'
65+
import { generateUrl } from '@nextcloud/router'
66+
import { buildFilePicker } from '../helpers/filePicker.js'
67+
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
68+
69+
export default {
70+
name: 'SuggestionsBar',
71+
components: {
72+
TableIcon,
73+
Document,
74+
NcButton,
75+
Shape,
76+
Upload,
77+
},
78+
mixins: [
79+
useActionChooseLocalAttachmentMixin,
80+
useEditorMixin,
81+
useFileMixin,
82+
],
83+
84+
setup() {
85+
const isMobile = useIsMobile()
86+
return {
87+
isMobile,
88+
}
89+
},
90+
91+
data: () => {
92+
return {
93+
startPath: null,
94+
isEmptyContent: false,
95+
}
96+
},
97+
98+
computed: {
99+
relativePath() {
100+
return this.$file?.relativePath ?? '/'
101+
},
102+
},
103+
104+
mounted() {
105+
this.$editor.on('update', this.onUpdate)
106+
this.onUpdate({ editor: this.$editor })
107+
},
108+
109+
beforeDestroy() {
110+
this.$editor.off('update', this.onUpdate)
111+
},
112+
113+
methods: {
114+
/**
115+
* Open smart picker dialog
116+
* Triggered by the "Smart Picker" button
117+
*/
118+
linkPicker() {
119+
getLinkWithPicker(null, true)
120+
.then(link => {
121+
const chain = this.$editor.chain()
122+
if (this.$editor.view.state?.selection.empty) {
123+
chain.focus().insertPreview(link).run()
124+
} else {
125+
chain.setLink({ href: link }).focus().run()
126+
}
127+
})
128+
.catch(error => {
129+
console.error('Smart picker promise rejected', error)
130+
})
131+
},
132+
133+
/**
134+
* Insert table
135+
* Triggered by the "Insert table" button
136+
*/
137+
insertTable() {
138+
this.$editor.chain().focus().insertTable()?.run()
139+
},
140+
141+
/**
142+
* Open dialog and ask user which file to link to
143+
* Triggered by the "link to file or folder" button
144+
*/
145+
linkFile() {
146+
if (this.startPath === null) {
147+
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
148+
}
149+
150+
const filePicker = buildFilePicker(this.startPath)
151+
152+
filePicker.pick()
153+
.then((file) => {
154+
const client = OC.Files.getClient()
155+
client.getFileInfo(file).then((_status, fileInfo) => {
156+
const url = new URL(generateUrl(`/f/${fileInfo.id}`), window.origin)
157+
this.setLink(url.href, fileInfo.name)
158+
this.startPath = fileInfo.path + (fileInfo.type === 'dir' ? `/${fileInfo.name}/` : '')
159+
})
160+
})
161+
.catch(() => {
162+
// do not close menu but keep focus
163+
this.$refs.linkFileOrFolder.$el.focus()
164+
})
165+
},
166+
167+
/**
168+
* Save user entered URL as a link markup
169+
* Triggered when the user submits the ActionInput
170+
*
171+
* @param {string} url href attribute of the link
172+
* @param {string} text Text part of the link
173+
*/
174+
setLink(url, text) {
175+
this.$editor.chain().insertOrSetLink(text, { href: url }).focus().run()
176+
},
177+
178+
onUpdate({ editor }) {
179+
/**
180+
* Empty document has an empty document and an empty paragraph (open and close blocks)
181+
*/
182+
const EMPTY_DOCUMENT_SIZE = 4
183+
this.isEmptyContent = editor.state.doc.nodeSize <= EMPTY_DOCUMENT_SIZE
184+
},
185+
},
186+
}
187+
</script>
188+
189+
<style scoped lang="scss">
190+
191+
.container-suggestions {
192+
display: flex;
193+
flex-grow: 1;
194+
margin-left: max(0px, (100% - var(--text-editor-max-width)) / 2);
195+
}
196+
197+
.suggestions--button {
198+
margin: 5px;
199+
}
200+
</style>

src/css/print.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,13 @@
110110
border: none!important;
111111
}
112112
}
113+
.container-suggestions {
114+
display: none;
115+
}
113116
}
114117
}
115118

119+
116120
.menubar-placeholder, .text-editor--readonly-bar {
117121
display: none;
118122
}

src/css/prosemirror.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ div.ProseMirror {
1515
white-space: pre-wrap;
1616
-webkit-font-variant-ligatures: none;
1717
font-variant-ligatures: none;
18-
padding: 4px 8px 200px 14px;
18+
padding: 4px 8px 50px 14px;
1919
line-height: 150%;
2020
font-size: var(--default-font-size);
2121
outline: none;

src/helpers/filePicker.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'
7+
8+
export const buildFilePicker = (startPath) => {
9+
return getFilePickerBuilder(t('text', 'Select file or folder to link to'))
10+
.startAt(startPath)
11+
.allowDirectories(true)
12+
.setMultiSelect(false)
13+
.setType(FilePickerType.Choose)
14+
.build()
15+
}

0 commit comments

Comments
 (0)