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

fix(files): create suggestions bar #6856

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
</template>
<ContentContainer v-show="contentLoaded"
ref="contentWrapper" />
<SuggestionsBar v-if="isRichEditor && isEmptyContent && contentLoaded" />
</MainContainer>
<Reader v-if="isResolvingConflict"
:content="syncError.data.outsideChange"
Expand Down Expand Up @@ -126,6 +127,7 @@
import Translate from './Modal/Translate.vue'
import { generateRemoteUrl } from '@nextcloud/router'
import { fetchNode } from '../services/WebdavClient.ts'
import SuggestionsBar from './SuggestionsBar.vue'

Check warning on line 130 in src/components/Editor.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor.vue#L130

Added line #L130 was not covered by tests

export default {
name: 'Editor',
Expand All @@ -141,6 +143,7 @@
Status,
Assistant,
Translate,
SuggestionsBar,

Check warning on line 146 in src/components/Editor.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor.vue#L146

Added line #L146 was not covered by tests
},
mixins: [
isMobile,
Expand Down Expand Up @@ -271,6 +274,7 @@
contentWrapper: null,
translateModal: false,
translateContent: '',
isEmptyContent: true,

Check warning on line 277 in src/components/Editor.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor.vue#L277

Added line #L277 was not covered by tests
}
},
computed: {
Expand Down Expand Up @@ -612,6 +616,7 @@
this.emit('update:content', {
markdown: proseMirrorMarkdown,
})
this.isEmptyContent = editor.state.doc.nodeSize <= 4

Check warning on line 619 in src/components/Editor.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Editor.vue#L619

Added line #L619 was not covered by tests
Copy link
Member

@juliusknorr juliusknorr Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the background for that fixed number of 4? I'd assume we can also check the text content length directly? Or the resulting markdown length?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text content length is checked by the count of nodeSize from editor.state.doc, checked with Max

Copy link
Member

@juliusknorr juliusknorr Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text content length is checked by the count of nodeSize from editor.state.doc

Yes, that is what the code does. I would still be curious about the reasoning, this is not obvious. I assume there is one ...

},

onSync({ steps, document }) {
Expand Down
46 changes: 4 additions & 42 deletions src/components/Menu/ActionInsertLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
<script>
import { NcActions, NcActionButton, NcActionInput } from '@nextcloud/vue'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'

Expand All @@ -76,6 +75,7 @@
import { BaseActionEntry } from './BaseActionEntry.js'
import { useFileMixin } from '../Editor.provider.js'
import { useMenuIDMixin } from './MenuBar.provider.js'
import { buildFilePicker } from '../../helpers/filePicker.js'

export default {
name: 'ActionInsertLink',
Expand Down Expand Up @@ -122,12 +122,7 @@
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
}

const filePicker = getFilePickerBuilder(t('text', 'Select file or folder to link to'))
.startAt(this.startPath)
.allowDirectories(true)
.setMultiSelect(false)
.setType(FilePickerType.Choose)
.build()
const filePicker = buildFilePicker(this.startPath)

Check warning on line 125 in src/components/Menu/ActionInsertLink.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Menu/ActionInsertLink.vue#L125

Added line #L125 was not covered by tests

filePicker.pick()
.then((file) => {
Expand Down Expand Up @@ -173,42 +168,9 @@
* @param {string} text Text part of the link
*/
setLink(url, text) {
// Heuristics for determining if we need a https:// prefix.
const noPrefixes = [
/^[a-zA-Z]+:/, // url with protocol ("mailTo:email@domain.tld")
/^\//, // absolute path
/\?fileId=/, // relative link with fileId
/^\.\.?\//, // relative link starting with ./ or ../
/^[^.]*[/$]/, // no dots before first '/' - not a domain name
/^#/, // url fragment
]
if (url && !noPrefixes.find(regex => url.match(regex))) {
url = 'https://' + url
}

// Avoid issues when parsing urls later on in markdown that might be entered in an invalid format (e.g. "mailto: example@example.com")
const href = url.replaceAll(' ', '%20')
const chain = this.$editor.chain()
// Check if any text is selected, if not insert the link using the given text property
if (this.$editor.view.state?.selection.empty) {
chain.insertContent({
type: 'paragraph',
content: [{
type: 'text',
marks: [{
type: 'link',
attrs: {
href,
},
}],
text,
}],
})
} else {
chain.setLink({ href })
}
chain.focus().run()
this.$editor.chain().setOrInsertLink(url, text).focus().run()

Check warning on line 171 in src/components/Menu/ActionInsertLink.vue

View check run for this annotation

Codecov / codecov/patch

src/components/Menu/ActionInsertLink.vue#L171

Added line #L171 was not covered by tests
},

/**
* Remove link markup at current position
* Triggered by the "remove link" button
Expand Down
156 changes: 156 additions & 0 deletions src/components/SuggestionsBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<template>
<div class="container-suggestions">
<NcButton ref="linkFileOrFolder"
type="tertiary"
size="normal"
@click="linkFile">
<template #icon>
<Document :size="20" />
</template>
{{ t('text', 'Link to file or folder') }}
</NcButton>

<NcButton type="tertiary"
size="normal"
@click="$callChooseLocalAttachment">
<template #icon>
<Document :size="20" />
</template>
{{ t('text', 'Upload') }}
</NcButton>

<NcButton type="tertiary"
size="normal"
@click="insertTable">
<template #icon>
<Table :size="20" />
</template>
{{ t('text', 'Insert Table') }}
</NcButton>

<NcButton type="tertiary"
size="normal"
@click="linkPicker">
<template #icon>
<Shape :size="20" />
</template>
{{ t('text', 'Smart Picker') }}
</NcButton>
</div>
</template>

<script>
import { NcButton } from '@nextcloud/vue'
import { Document, Table, Shape } from './icons.js'
import { useActionChooseLocalAttachmentMixin } from './Editor/MediaHandler.provider.js'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'
import { useEditorMixin, useFileMixin } from './Editor.provider.js'
import { generateUrl } from '@nextcloud/router'
import { buildFilePicker } from '../helpers/filePicker.js'

Check warning on line 49 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L43-L49

Added lines #L43 - L49 were not covered by tests

export default {
name: 'SuggestionsBar',
components: {
Table,

Check failure on line 54 in src/components/SuggestionsBar.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Name "Table" is reserved in HTML
Document,
NcButton,
Shape,
},
mixins: [
useActionChooseLocalAttachmentMixin,
useEditorMixin,
useFileMixin,
],

Check warning on line 63 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L51-L63

Added lines #L51 - L63 were not covered by tests

data: () => {
return {
startPath: null,
}
},

Check warning on line 69 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L65-L69

Added lines #L65 - L69 were not covered by tests

computed: {
relativePath() {
return this.$file?.relativePath ?? '/'
},
},

Check warning on line 75 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L71-L75

Added lines #L71 - L75 were not covered by tests

methods: {
/**
* Open smart picker dialog
* Triggered by the "Smart Picker" button
*/
linkPicker() {
getLinkWithPicker(null, true)
.then(link => {
const chain = this.$editor.chain()
if (this.$editor.view.state?.selection.empty) {
chain.focus().insertPreview(link).run()
} else {
chain.setLink({ href: link }).focus().run()
}
})
.catch(error => {
console.error('Smart picker promise rejected', error)
})
},

Check warning on line 95 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L77-L95

Added lines #L77 - L95 were not covered by tests

/**
* Insert table
* Triggered by the "Insert table" button
*/
insertTable() {
this.$editor.chain().focus().insertTable()?.run()
},

Check warning on line 103 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L97-L103

Added lines #L97 - L103 were not covered by tests

/**
* Open dialog and ask user which file to link to
* Triggered by the "link to file or folder" button
*/
linkFile() {
if (this.startPath === null) {
this.startPath = this.relativePath.split('/').slice(0, -1).join('/')
}

Check warning on line 112 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L105-L112

Added lines #L105 - L112 were not covered by tests

const filePicker = buildFilePicker(this.startPath)

Check warning on line 114 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L114

Added line #L114 was not covered by tests

filePicker.pick()
.then((file) => {
const client = OC.Files.getClient()
client.getFileInfo(file).then((_status, fileInfo) => {
const url = new URL(generateUrl(`/f/${fileInfo.id}`), window.origin)
this.setLink(url.href, fileInfo.name)
this.startPath = fileInfo.path + (fileInfo.type === 'dir' ? `/${fileInfo.name}/` : '')
})
})
.catch(() => {
// do not close menu but keep focus
this.$refs.linkFileOrFolder.$el.focus()
})
},

Check warning on line 129 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L116-L129

Added lines #L116 - L129 were not covered by tests

/**
* Save user entered URL as a link markup
* Triggered when the user submits the ActionInput
*
* @param {string} url href attribute of the link
* @param {string} text Text part of the link
*/
setLink(url, text) {
this.$editor.chain().setOrInsertLink(url, text).focus().run()
},
},

Check warning on line 141 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L131-L141

Added lines #L131 - L141 were not covered by tests

}

Check warning on line 143 in src/components/SuggestionsBar.vue

View check run for this annotation

Codecov / codecov/patch

src/components/SuggestionsBar.vue#L143

Added line #L143 was not covered by tests

</script>

<style scoped lang="scss">

.container-suggestions {
display: flex;
justify-content: center;
align-items: flex-end;
align-content: flex-end;
position: sticky;
}
</style>
15 changes: 15 additions & 0 deletions src/helpers/filePicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'

export const buildFilePicker = (startPath) => {
return getFilePickerBuilder(t('text', 'Select file or folder to link to'))
.startAt(startPath)
.allowDirectories(true)
.setMultiSelect(false)
.setType(FilePickerType.Choose)
.build()
}

Check warning on line 15 in src/helpers/filePicker.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/filePicker.js#L9-L15

Added lines #L9 - L15 were not covered by tests
45 changes: 45 additions & 0 deletions src/marks/Link.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import TipTapLink from '@tiptap/extension-link'
import { domHref, parseHref } from './../helpers/links.js'
import { linkClicking } from '../plugins/links.js'
import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js'

Check failure on line 10 in src/marks/Link.js

View workflow job for this annotation

GitHub Actions / NPM lint

'getLinkWithPicker' is defined but never used

const PROTOCOLS_TO_LINK_TO = ['http:', 'https:', 'mailto:', 'tel:']

Expand Down Expand Up @@ -88,6 +89,50 @@
]
},

addCommands() {
return {
/**
* Check if any text is selected, if not insert the link using the given text property
*
* @param {string} url href attribute of the link
* @param {string} text Text part of the link
*/
setOrInsertLink: (url, text) => ({ state, chain }) => {
// Heuristics for determining if we need a https:// prefix.
const noPrefixes = [
/^[a-zA-Z]+:/, // url with protocol ("mailTo:email@domain.tld")
/^\//, // absolute path
/\?fileId=/, // relative link with fileId
/^\.\.?\//, // relative link starting with ./ or ../
/^[^.]*[/$]/, // no dots before first '/' - not a domain name
/^#/, // url fragment
]
if (url && !noPrefixes.find(regex => url.match(regex))) {
url = 'https://' + url
}
// Avoid issues when parsing urls later on in markdown that might be entered in an invalid format (e.g. "mailto: example@example.com")
const href = url.replaceAll(' ', '%20')
if (state.selection.empty) {
return chain().insertContent({
type: 'paragraph',
content: [{
type: 'text',
marks: [{
type: 'link',
attrs: {
href,
},
}],
text,
}],
}).run()
} else {
return chain().setLink({ href }).run()
}

Check warning on line 131 in src/marks/Link.js

View check run for this annotation

Codecov / codecov/patch

src/marks/Link.js#L101-L131

Added lines #L101 - L131 were not covered by tests
},
}
},

addProseMirrorPlugins() {
const plugins = this.parent()
// remove upstream link click handle plugin
Expand Down
Loading