Merge pull request #4399 from nextcloud/enh/text-editor
This commit is contained in:
12
.github/workflows/cypress.yml
vendored
12
.github/workflows/cypress.yml
vendored
@@ -33,6 +33,11 @@ jobs:
|
|||||||
- name: Set up npm7
|
- name: Set up npm7
|
||||||
run: npm i -g npm@7
|
run: npm i -g npm@7
|
||||||
|
|
||||||
|
- name: Register text Git reference
|
||||||
|
run: |
|
||||||
|
text_app_ref="$(if [ "${{ matrix.server-versions }}" = "master" ]; then echo -n "main"; else echo -n "${{ matrix.server-versions }}"; fi)"
|
||||||
|
echo "text_app_ref=$text_app_ref" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout server
|
- name: Checkout server
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
@@ -51,6 +56,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: apps/${{ env.APP_NAME }}
|
path: apps/${{ env.APP_NAME }}
|
||||||
|
|
||||||
|
- name: Checkout text
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: nextcloud/text
|
||||||
|
ref: ${{ env.text_app_ref }}
|
||||||
|
path: apps/text
|
||||||
|
|
||||||
- name: Set up php ${{ matrix.php-versions }}
|
- name: Set up php ${{ matrix.php-versions }}
|
||||||
uses: shivammathur/setup-php@2.24.0
|
uses: shivammathur/setup-php@2.24.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ import { sampleBoard } from '../utils/sampleBoard'
|
|||||||
const user = randUser()
|
const user = randUser()
|
||||||
const boardData = sampleBoard()
|
const boardData = sampleBoard()
|
||||||
|
|
||||||
|
const auth = {
|
||||||
|
user: user.userId,
|
||||||
|
password: user.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
const useModal = (useModal) => {
|
||||||
|
return cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${Cypress.env('baseUrl')}/ocs/v2.php/apps/deck/api/v1.0/config/cardDetailsInModal?format=json`,
|
||||||
|
auth,
|
||||||
|
body: { value: useModal },
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe('Card', function() {
|
describe('Card', function() {
|
||||||
let boardId
|
let boardId
|
||||||
before(function() {
|
before(function() {
|
||||||
@@ -19,22 +35,10 @@ describe('Card', function() {
|
|||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Can show card details modal', function() {
|
|
||||||
cy.getNavigationEntry(boardData.title)
|
|
||||||
.first().click({ force: true })
|
|
||||||
|
|
||||||
cy.get('.board .stack').eq(0).within(() => {
|
|
||||||
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('.modal__card').should('be.visible')
|
|
||||||
cy.get('.app-sidebar-header__maintitle').contains('Hello world')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can add a card', function() {
|
it('Can add a card', function() {
|
||||||
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
const newCardTitle = 'Write some cypress tests'
|
const newCardTitle = 'Write some cypress tests'
|
||||||
|
|
||||||
cy.getNavigationEntry(boardData.title)
|
cy.getNavigationEntry(boardData.title)
|
||||||
@@ -54,4 +58,72 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Modal', () => {
|
||||||
|
beforeEach(function() {
|
||||||
|
cy.login(user)
|
||||||
|
useModal(true).then(() => {
|
||||||
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can show card details modal', function() {
|
||||||
|
cy.getNavigationEntry(boardData.title)
|
||||||
|
.first().click({ force: true })
|
||||||
|
|
||||||
|
cy.get('.board .stack').eq(0).within(() => {
|
||||||
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.modal__card').should('be.visible')
|
||||||
|
cy.get('.app-sidebar-header__maintitle').contains('Hello world')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Attachment from files app', () => {
|
||||||
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
|
cy.get('.modal__card').should('be.visible')
|
||||||
|
cy.get('.app-sidebar-tabs__tab [data-id="attachments"]').click()
|
||||||
|
cy.get('button.icon-upload').should('be.visible')
|
||||||
|
cy.get('button.icon-folder').should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('.oc-dialog #picker-filestable tr[data-entryname="welcome.txt"] td.filename').should('be.visible')
|
||||||
|
.click()
|
||||||
|
cy.get('.oc-dialog button.primary').click()
|
||||||
|
cy.get('.attachment-list .basename').contains('welcome.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Shows the modal with the editor', () => {
|
||||||
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
|
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
|
||||||
|
cy.get('.modal__card').should('be.visible')
|
||||||
|
cy.get('.app-sidebar-header__maintitle').contains('Hello world')
|
||||||
|
cy.get('.modal__card .ProseMirror h1').contains('Hello world').should('be.visible')
|
||||||
|
cy.get('.modal__card .ProseMirror h1')
|
||||||
|
.click()
|
||||||
|
.type(' writing more text{enter}- List item{enter}with entries{enter}{enter}Paragraph')
|
||||||
|
cy.wait('@save', { timeout: 7000 })
|
||||||
|
|
||||||
|
cy.reload()
|
||||||
|
cy.get('.modal__card').should('be.visible')
|
||||||
|
cy.get('.modal__card .ProseMirror h1').contains('Hello world writing more text').should('be.visible')
|
||||||
|
cy.get('.modal__card .ProseMirror li').eq(0).contains('List item').should('be.visible')
|
||||||
|
cy.get('.modal__card .ProseMirror li').eq(1).contains('with entries').should('be.visible')
|
||||||
|
cy.get('.modal__card .ProseMirror p').contains('Paragraph').should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sidebar', () => {
|
||||||
|
beforeEach(function() {
|
||||||
|
cy.login(user)
|
||||||
|
useModal(false).then(() => {
|
||||||
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Show the sidebar', () => {
|
||||||
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
|
cy.get('#app-sidebar-vue')
|
||||||
|
.find('.ProseMirror h1').contains('Hello world writing more text').should('be.visible')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ Cypress.Commands.add('createExampleBoard', ({ user, board }) => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `${Cypress.env('baseUrl')}/index.php/apps/deck/api/v1.0/boards/${boardData.id}/stacks/${stackData.id}/cards`,
|
url: `${Cypress.env('baseUrl')}/index.php/apps/deck/api/v1.0/boards/${boardData.id}/stacks/${stackData.id}/cards`,
|
||||||
auth,
|
auth,
|
||||||
body: { title: card.title },
|
body: { title: card.title, description: card.description ?? '' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const sampleBoard = (title = 'MyTestBoard') => {
|
|||||||
cards: [
|
cards: [
|
||||||
{
|
{
|
||||||
title: 'Hello world',
|
title: 'Hello world',
|
||||||
|
description: '# Hello world',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use OCA\Deck\AppInfo\Application;
|
|||||||
use OCA\Deck\Service\ConfigService;
|
use OCA\Deck\Service\ConfigService;
|
||||||
use OCA\Deck\Service\PermissionService;
|
use OCA\Deck\Service\PermissionService;
|
||||||
use OCA\Files\Event\LoadSidebar;
|
use OCA\Files\Event\LoadSidebar;
|
||||||
|
use OCA\Text\Event\LoadEditor;
|
||||||
use OCA\Viewer\Event\LoadViewer;
|
use OCA\Viewer\Event\LoadViewer;
|
||||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||||
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as CollaborationResourcesEvent;
|
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as CollaborationResourcesEvent;
|
||||||
@@ -90,6 +91,9 @@ class PageController extends Controller {
|
|||||||
|
|
||||||
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
|
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
|
||||||
$this->eventDispatcher->dispatchTyped(new CollaborationResourcesEvent());
|
$this->eventDispatcher->dispatchTyped(new CollaborationResourcesEvent());
|
||||||
|
if (class_exists(LoadEditor::class)) {
|
||||||
|
$this->eventDispatcher->dispatchTyped(new LoadEditor());
|
||||||
|
}
|
||||||
if (class_exists(LoadViewer::class)) {
|
if (class_exists(LoadViewer::class)) {
|
||||||
$this->eventDispatcher->dispatchTyped(new LoadViewer());
|
$this->eventDispatcher->dispatchTyped(new LoadViewer());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<NcModal v-if="cardDetailsInModal && $route.params.cardId"
|
<NcModal v-if="cardDetailsInModal && $route.params.cardId"
|
||||||
:clear-view-delay="0"
|
:clear-view-delay="0"
|
||||||
:title="t('deck', 'Card details')"
|
:close-button-contained="true"
|
||||||
size="large"
|
size="large"
|
||||||
@close="hideModal()">
|
@close="hideModal()">
|
||||||
<div class="modal__content modal__card">
|
<div class="modal__content modal__card">
|
||||||
|
|||||||
@@ -26,11 +26,12 @@
|
|||||||
{{ t('deck', 'Description') }}
|
{{ t('deck', 'Description') }}
|
||||||
<span v-if="descriptionLastEdit && !descriptionSaving">{{ t('deck', '(Unsaved)') }}</span>
|
<span v-if="descriptionLastEdit && !descriptionSaving">{{ t('deck', '(Unsaved)') }}</span>
|
||||||
<span v-if="descriptionSaving">{{ t('deck', '(Saving…)') }}</span>
|
<span v-if="descriptionSaving">{{ t('deck', '(Saving…)') }}</span>
|
||||||
<a v-tooltip="t('deck', 'Formatting help')"
|
<a v-if="!textAppAvailable"
|
||||||
|
v-tooltip="t('deck', 'Formatting help')"
|
||||||
href="https://deck.readthedocs.io/en/latest/Markdown/"
|
href="https://deck.readthedocs.io/en/latest/Markdown/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="icon icon-info" />
|
class="icon icon-info" />
|
||||||
<NcActions v-if="canEdit">
|
<NcActions v-if="!textAppAvailable && canEdit">
|
||||||
<NcActionButton v-if="!descriptionEditing" icon="icon-rename" @click="showEditor()">
|
<NcActionButton v-if="!descriptionEditing" icon="icon-rename" @click="showEditor()">
|
||||||
{{ t('deck', 'Edit description') }}
|
{{ t('deck', 'Edit description') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
@@ -48,6 +49,10 @@
|
|||||||
</NcActions>
|
</NcActions>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
<div v-if="textAppAvailable" class="description__text">
|
||||||
|
<div ref="editor" />
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
<div v-if="!descriptionEditing && hasDescription"
|
<div v-if="!descriptionEditing && hasDescription"
|
||||||
id="description-preview"
|
id="description-preview"
|
||||||
@click="clickedPreview"
|
@click="clickedPreview"
|
||||||
@@ -63,6 +68,7 @@
|
|||||||
@initialized="addKeyListeners"
|
@initialized="addKeyListeners"
|
||||||
@update:modelValue="updateDescription"
|
@update:modelValue="updateDescription"
|
||||||
@blur="saveDescription" />
|
@blur="saveDescription" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<NcModal v-if="modalShow" :title="t('deck', 'Choose attachment')" @close="modalShow=false">
|
<NcModal v-if="modalShow" :title="t('deck', 'Choose attachment')" @close="modalShow=false">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
@@ -116,6 +122,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
textAppAvailable: !!window.OCA?.Text?.createEditor,
|
||||||
|
editor: null,
|
||||||
keyExitState: 0,
|
keyExitState: 0,
|
||||||
description: '',
|
description: '',
|
||||||
markdownIt: null,
|
markdownIt: null,
|
||||||
@@ -175,10 +183,31 @@ export default {
|
|||||||
return this.card?.description?.trim?.() !== ''
|
return this.card?.description?.trim?.() !== ''
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setupEditor()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this?.editor?.destroy()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async setupEditor() {
|
||||||
|
this?.editor?.destroy()
|
||||||
|
this.editor = await window.OCA.Text.createEditor({
|
||||||
|
el: this.$refs.editor,
|
||||||
|
content: this.card.description,
|
||||||
|
readOnly: !this.canEdit,
|
||||||
|
onUpdate: ({ markdown }) => {
|
||||||
|
this.description = markdown
|
||||||
|
this.updateDescription()
|
||||||
|
},
|
||||||
|
onFileInsert: () => {
|
||||||
|
this.showAttachmentModal()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
addKeyListeners() {
|
addKeyListeners() {
|
||||||
this.$refs.markdownEditor.easymde.codemirror.on('keydown', (a, b) => {
|
this.$refs.markdownEditor.easymde.codemirror.on('keydown', (a, b) => {
|
||||||
|
|
||||||
if (this.keyExitState === 0 && (b.key === 'Meta' || b.key === 'Alt')) {
|
if (this.keyExitState === 0 && (b.key === 'Meta' || b.key === 'Alt')) {
|
||||||
this.keyExitState = 1
|
this.keyExitState = 1
|
||||||
}
|
}
|
||||||
@@ -213,17 +242,23 @@ export default {
|
|||||||
this.modalShow = true
|
this.modalShow = true
|
||||||
},
|
},
|
||||||
addAttachment(attachment) {
|
addAttachment(attachment) {
|
||||||
|
const asImage = (attachment.type === 'file' && attachment.extendedData.hasPreview) || attachment.extendedData.mimetype.includes('image')
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.insertAtCursor(
|
||||||
|
asImage
|
||||||
|
? `<a href="${this.attachmentPreview(attachment)}"><img src="${this.attachmentPreview(attachment)}" alt="${attachment.data}" /></a>`
|
||||||
|
: `<a href="${this.attachmentPreview(attachment)}">${attachment.data}</a>`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
const attachmentString = (asImage ? '!' : '') + '[📎 ' + attachment.data + '](' + this.attachmentPreview(attachment) + ')'
|
||||||
const descString = this.$refs.markdownEditor.easymde.value()
|
const descString = this.$refs.markdownEditor.easymde.value()
|
||||||
let embed = ''
|
|
||||||
if ((attachment.type === 'file' && attachment.extendedData.hasPreview) || attachment.extendedData.mimetype.includes('image')) {
|
|
||||||
embed = '!'
|
|
||||||
}
|
|
||||||
const attachmentString = embed + '[📎 ' + attachment.data + '](' + this.attachmentPreview(attachment) + ')'
|
|
||||||
const newContent = descString + '\n' + attachmentString
|
const newContent = descString + '\n' + attachmentString
|
||||||
this.$refs.markdownEditor.easymde.value(newContent)
|
this.$refs.markdownEditor.easymde.value(newContent)
|
||||||
this.description = newContent
|
this.description = newContent
|
||||||
this.modalShow = false
|
}
|
||||||
this.updateDescription()
|
this.updateDescription()
|
||||||
|
this.modalShow = false
|
||||||
},
|
},
|
||||||
clickedPreview(e) {
|
clickedPreview(e) {
|
||||||
if (e.target.getAttribute('type') === 'checkbox') {
|
if (e.target.getAttribute('type') === 'checkbox') {
|
||||||
@@ -379,4 +414,12 @@ h5 {
|
|||||||
.vue-easymde .cm-s-easymde .cm-formatting.cm-image {
|
.vue-easymde .cm-s-easymde .cm-formatting.cm-image {
|
||||||
color: var(--color-text-maxcontrast);
|
color: var(--color-text-maxcontrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-sidebar__tab .description__text .text-menubar {
|
||||||
|
top: -10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__card .description__text .text-menubar {
|
||||||
|
top: 142px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user