Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
5e45da27
Commit
5e45da27
authored
May 25, 2020
by
jerasmus
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to insert an image via SSE
Added the ability to insert an image via SSE
parent
ee4f1d10
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
260 additions
and
22 deletions
+260
-22
app/assets/javascripts/static_site_editor/components/edit_area.vue
...s/javascripts/static_site_editor/components/edit_area.vue
+1
-1
app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
...ts/vue_shared/components/rich_content_editor/constants.js
+1
-0
app/assets/javascripts/vue_shared/components/rich_content_editor/editor_service.js
...e_shared/components/rich_content_editor/editor_service.js
+7
-0
app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
...components/rich_content_editor/modals/add_image_modal.vue
+74
-0
app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
...ed/components/rich_content_editor/rich_content_editor.vue
+36
-16
changelogs/unreleased/216640-insert-image-modal.yml
changelogs/unreleased/216640-insert-image-modal.yml
+5
-0
locale/gitlab.pot
locale/gitlab.pot
+18
-0
spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
...red/components/rich_content_editor/editor_service_spec.js
+38
-4
spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
...onents/rich_content_editor/modals/add_image_modal_spec.js
+41
-0
spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
...omponents/rich_content_editor/rich_content_editor_spec.js
+39
-1
No files found.
app/assets/javascripts/static_site_editor/components/edit_area.vue
View file @
5e45da27
...
...
@@ -49,7 +49,7 @@ export default {
<
template
>
<div
class=
"d-flex flex-grow-1 flex-column h-100"
>
<edit-header
class=
"py-2"
:title=
"title"
/>
<rich-content-editor
v-model=
"editableContent"
class=
"mb-9"
/>
<rich-content-editor
v-model=
"editableContent"
class=
"mb-9
h-100
"
/>
<publish-toolbar
class=
"gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:return-url=
"returnUrl"
...
...
app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
View file @
5e45da27
...
...
@@ -24,6 +24,7 @@ const TOOLBAR_ITEM_CONFIGS = [
{
isDivider
:
true
},
{
icon
:
'
dash
'
,
command
:
'
HR
'
,
tooltip
:
__
(
'
Add a line
'
)
},
{
icon
:
'
table
'
,
event
:
'
openPopupAddTable
'
,
classes
:
'
tui-table
'
,
tooltip
:
__
(
'
Add a table
'
)
},
{
icon
:
'
doc-image
'
,
event
:
CUSTOM_EVENTS
.
openAddImageModal
,
tooltip
:
__
(
'
Insert an image
'
)
},
{
isDivider
:
true
},
{
icon
:
'
code
'
,
command
:
'
Code
'
,
tooltip
:
__
(
'
Insert inline code
'
)
},
];
...
...
app/assets/javascripts/vue_shared/components/rich_content_editor/editor_service.js
View file @
5e45da27
...
...
@@ -34,3 +34,10 @@ export const addCustomEventListener = (editorInstance, event, handler) => {
editorInstance
.
eventManager
.
addEventType
(
event
);
editorInstance
.
eventManager
.
listen
(
event
,
handler
);
};
export
const
removeCustomEventListener
=
(
editorInstance
,
event
,
handler
)
=>
editorInstance
.
eventManager
.
removeEventHandler
(
event
,
handler
);
export
const
addImage
=
({
editor
},
image
)
=>
editor
.
exec
(
'
AddImage
'
,
image
);
export
const
getMarkdown
=
editorInstance
=>
editorInstance
.
invoke
(
'
getMarkdown
'
);
app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
0 → 100644
View file @
5e45da27
<
script
>
import
{
isSafeURL
}
from
'
~/lib/utils/url_utility
'
;
import
{
GlModal
,
GlFormGroup
,
GlFormInput
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
components
:
{
GlModal
,
GlFormGroup
,
GlFormInput
,
},
data
()
{
return
{
error
:
null
,
imageUrl
:
null
,
altText
:
null
,
modalTitle
:
__
(
'
Image Details
'
),
okTitle
:
__
(
'
Insert
'
),
urlLabel
:
__
(
'
Image URL
'
),
descriptionLabel
:
__
(
'
Description
'
),
};
},
methods
:
{
show
()
{
this
.
error
=
null
;
this
.
imageUrl
=
null
;
this
.
altText
=
null
;
this
.
$refs
.
modal
.
show
();
},
onOk
(
event
)
{
if
(
!
this
.
isValid
())
{
event
.
preventDefault
();
return
;
}
const
{
imageUrl
,
altText
}
=
this
;
this
.
$emit
(
'
addImage
'
,
{
imageUrl
,
altText
:
altText
||
__
(
'
image
'
)
});
},
isValid
()
{
if
(
!
isSafeURL
(
this
.
imageUrl
))
{
this
.
error
=
__
(
'
Please provide a valid URL
'
);
this
.
$refs
.
urlInput
.
$el
.
focus
();
return
false
;
}
return
true
;
},
},
};
</
script
>
<
template
>
<gl-modal
ref=
"modal"
modal-id=
"add-image-modal"
:title=
"modalTitle"
:ok-title=
"okTitle"
@
ok=
"onOk"
>
<gl-form-group
:label=
"urlLabel"
label-for=
"url-input"
:state=
"!Boolean(error)"
:invalid-feedback=
"error"
>
<gl-form-input
id=
"url-input"
ref=
"urlInput"
v-model=
"imageUrl"
/>
</gl-form-group>
<gl-form-group
:label=
"descriptionLabel"
label-for=
"description-input"
>
<gl-form-input
id=
"description-input"
ref=
"descriptionInput"
v-model=
"altText"
/>
</gl-form-group>
</gl-modal>
</
template
>
app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
View file @
5e45da27
...
...
@@ -2,6 +2,7 @@
import
'
codemirror/lib/codemirror.css
'
;
import
'
@toast-ui/editor/dist/toastui-editor.css
'
;
import
AddImageModal
from
'
./modals/add_image_modal.vue
'
;
import
{
EDITOR_OPTIONS
,
EDITOR_TYPES
,
...
...
@@ -10,7 +11,12 @@ import {
CUSTOM_EVENTS
,
}
from
'
./constants
'
;
import
{
addCustomEventListener
}
from
'
./editor_service
'
;
import
{
addCustomEventListener
,
removeCustomEventListener
,
addImage
,
getMarkdown
,
}
from
'
./editor_service
'
;
export
default
{
components
:
{
...
...
@@ -18,6 +24,7 @@ export default {
import
(
/* webpackChunkName: 'toast_editor' */
'
@toast-ui/vue-editor
'
).
then
(
toast
=>
toast
.
Editor
,
),
AddImageModal
,
},
props
:
{
value
:
{
...
...
@@ -49,13 +56,20 @@ export default {
editorOptions
()
{
return
{
...
EDITOR_OPTIONS
,
...
this
.
options
};
},
editorInstance
()
{
return
this
.
$refs
.
editor
;
},
},
beforeDestroy
()
{
removeCustomEventListener
(
this
.
editorInstance
,
CUSTOM_EVENTS
.
openAddImageModal
,
this
.
onOpenAddImageModal
,
);
},
methods
:
{
onContentChanged
()
{
this
.
$emit
(
'
input
'
,
this
.
getMarkdown
());
},
getMarkdown
()
{
return
this
.
$refs
.
editor
.
invoke
(
'
getMarkdown
'
);
this
.
$emit
(
'
input
'
,
getMarkdown
(
this
.
editorInstance
));
},
onLoad
(
editorInstance
)
{
addCustomEventListener
(
...
...
@@ -65,20 +79,26 @@ export default {
);
},
onOpenAddImageModal
()
{
// TODO - add image modal (next MR)
this
.
$refs
.
addImageModal
.
show
();
},
onAddImage
(
image
)
{
addImage
(
this
.
editorInstance
,
image
);
},
},
};
</
script
>
<
template
>
<toast-editor
ref=
"editor"
:initial-value=
"value"
:options=
"editorOptions"
:preview-style=
"previewStyle"
:initial-edit-type=
"initialEditType"
:height=
"height"
@
change=
"onContentChanged"
@
load=
"onLoad"
/>
<div>
<toast-editor
ref=
"editor"
:initial-value=
"value"
:options=
"editorOptions"
:preview-style=
"previewStyle"
:initial-edit-type=
"initialEditType"
:height=
"height"
@
change=
"onContentChanged"
@
load=
"onLoad"
/>
<add-image-modal
ref=
"addImageModal"
@
addImage=
"onAddImage"
/>
</div>
</
template
>
changelogs/unreleased/216640-insert-image-modal.yml
0 → 100644
View file @
5e45da27
---
title
:
Add ability to insert an image via SSE
merge_request
:
33029
author
:
type
:
added
locale/gitlab.pot
View file @
5e45da27
...
...
@@ -11574,6 +11574,12 @@ msgstr ""
msgid "Ignored"
msgstr ""
msgid "Image Details"
msgstr ""
msgid "Image URL"
msgstr ""
msgid "Image: %{image}"
msgstr ""
...
...
@@ -11832,12 +11838,18 @@ msgstr ""
msgid "Input your repository URL"
msgstr ""
msgid "Insert"
msgstr ""
msgid "Insert a code block"
msgstr ""
msgid "Insert a quote"
msgstr ""
msgid "Insert an image"
msgstr ""
msgid "Insert code"
msgstr ""
...
...
@@ -15988,6 +16000,9 @@ msgstr ""
msgid "Please provide a name"
msgstr ""
msgid "Please provide a valid URL"
msgstr ""
msgid "Please provide a valid email address."
msgstr ""
...
...
@@ -26183,6 +26198,9 @@ msgstr ""
msgid "https://your-bitbucket-server"
msgstr ""
msgid "image"
msgstr ""
msgid "image diff"
msgstr ""
...
...
spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
View file @
5e45da27
import
{
generateToolbarItem
,
addCustomEventListener
,
removeCustomEventListener
,
addImage
,
getMarkdown
,
}
from
'
~/vue_shared/components/rich_content_editor/editor_service
'
;
describe
(
'
Editor Service
'
,
()
=>
{
const
mockInstance
=
{
eventManager
:
{
addEventType
:
jest
.
fn
(),
removeEventHandler
:
jest
.
fn
(),
listen
:
jest
.
fn
()
},
editor
:
{
exec
:
jest
.
fn
()
},
invoke
:
jest
.
fn
(),
};
const
event
=
'
someCustomEvent
'
;
const
handler
=
jest
.
fn
();
describe
(
'
generateToolbarItem
'
,
()
=>
{
const
config
=
{
icon
:
'
bold
'
,
...
...
@@ -11,6 +22,7 @@ describe('Editor Service', () => {
tooltip
:
'
Some Tooltip
'
,
event
:
'
some-event
'
,
};
const
generatedItem
=
generateToolbarItem
(
config
);
it
(
'
generates the correct command
'
,
()
=>
{
...
...
@@ -33,10 +45,6 @@ describe('Editor Service', () => {
});
describe
(
'
addCustomEventListener
'
,
()
=>
{
const
mockInstance
=
{
eventManager
:
{
addEventType
:
jest
.
fn
(),
listen
:
jest
.
fn
()
}
};
const
event
=
'
someCustomEvent
'
;
const
handler
=
jest
.
fn
();
it
(
'
registers an event type on the instance and adds an event handler
'
,
()
=>
{
addCustomEventListener
(
mockInstance
,
event
,
handler
);
...
...
@@ -44,4 +52,30 @@ describe('Editor Service', () => {
expect
(
mockInstance
.
eventManager
.
listen
).
toHaveBeenCalledWith
(
event
,
handler
);
});
});
describe
(
'
removeCustomEventListener
'
,
()
=>
{
it
(
'
removes an event handler from the instance
'
,
()
=>
{
removeCustomEventListener
(
mockInstance
,
event
,
handler
);
expect
(
mockInstance
.
eventManager
.
removeEventHandler
).
toHaveBeenCalledWith
(
event
,
handler
);
});
});
describe
(
'
addImage
'
,
()
=>
{
it
(
'
calls the exec method on the instance
'
,
()
=>
{
const
mockImage
=
{
imageUrl
:
'
some/url.png
'
,
description
:
'
some description
'
};
addImage
(
mockInstance
,
mockImage
);
expect
(
mockInstance
.
editor
.
exec
).
toHaveBeenCalledWith
(
'
AddImage
'
,
mockImage
);
});
});
describe
(
'
getMarkdown
'
,
()
=>
{
it
(
'
calls the invoke method on the instance
'
,
()
=>
{
getMarkdown
(
mockInstance
);
expect
(
mockInstance
.
invoke
).
toHaveBeenCalledWith
(
'
getMarkdown
'
);
});
});
});
spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js
0 → 100644
View file @
5e45da27
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlModal
}
from
'
@gitlab/ui
'
;
import
AddImageModal
from
'
~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
'
;
describe
(
'
Add Image Modal
'
,
()
=>
{
let
wrapper
;
const
findModal
=
()
=>
wrapper
.
find
(
GlModal
);
const
findUrlInput
=
()
=>
wrapper
.
find
({
ref
:
'
urlInput
'
});
const
findDescriptionInput
=
()
=>
wrapper
.
find
({
ref
:
'
descriptionInput
'
});
beforeEach
(()
=>
{
wrapper
=
shallowMount
(
AddImageModal
);
});
describe
(
'
when content is loaded
'
,
()
=>
{
it
(
'
renders a modal component
'
,
()
=>
{
expect
(
findModal
().
exists
()).
toBe
(
true
);
});
it
(
'
renders an input to add an image URL
'
,
()
=>
{
expect
(
findUrlInput
().
exists
()).
toBe
(
true
);
});
it
(
'
renders an input to add an image description
'
,
()
=>
{
expect
(
findDescriptionInput
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
add image
'
,
()
=>
{
it
(
'
emits an addImage event when a valid URL is specified
'
,
()
=>
{
const
preventDefault
=
jest
.
fn
();
const
mockImage
=
{
imageUrl
:
'
/some/valid/url.png
'
,
altText
:
'
some description
'
};
wrapper
.
setData
({
...
mockImage
});
findModal
().
vm
.
$emit
(
'
ok
'
,
{
preventDefault
});
expect
(
preventDefault
).
not
.
toHaveBeenCalled
();
expect
(
wrapper
.
emitted
(
'
addImage
'
)).
toEqual
([[
mockImage
]]);
});
});
});
spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
View file @
5e45da27
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
RichContentEditor
from
'
~/vue_shared/components/rich_content_editor/rich_content_editor.vue
'
;
import
AddImageModal
from
'
~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue
'
;
import
{
EDITOR_OPTIONS
,
EDITOR_TYPES
,
...
...
@@ -8,11 +9,17 @@ import {
CUSTOM_EVENTS
,
}
from
'
~/vue_shared/components/rich_content_editor/constants
'
;
import
{
addCustomEventListener
}
from
'
~/vue_shared/components/rich_content_editor/editor_service
'
;
import
{
addCustomEventListener
,
removeCustomEventListener
,
addImage
,
}
from
'
~/vue_shared/components/rich_content_editor/editor_service
'
;
jest
.
mock
(
'
~/vue_shared/components/rich_content_editor/editor_service
'
,
()
=>
({
...
jest
.
requireActual
(
'
~/vue_shared/components/rich_content_editor/editor_service
'
),
addCustomEventListener
:
jest
.
fn
(),
removeCustomEventListener
:
jest
.
fn
(),
addImage
:
jest
.
fn
(),
}));
describe
(
'
Rich Content Editor
'
,
()
=>
{
...
...
@@ -20,6 +27,7 @@ describe('Rich Content Editor', () => {
const
value
=
'
## Some Markdown
'
;
const
findEditor
=
()
=>
wrapper
.
find
({
ref
:
'
editor
'
});
const
findAddImageModal
=
()
=>
wrapper
.
find
(
AddImageModal
);
beforeEach
(()
=>
{
wrapper
=
shallowMount
(
RichContentEditor
,
{
...
...
@@ -77,4 +85,34 @@ describe('Rich Content Editor', () => {
);
});
});
describe
(
'
when editor is destroyed
'
,
()
=>
{
it
(
'
removes the CUSTOM_EVENTS.openAddImageModal custom event listener
'
,
()
=>
{
const
mockInstance
=
{
eventManager
:
{
removeEventHandler
:
jest
.
fn
()
}
};
wrapper
.
vm
.
$refs
.
editor
=
mockInstance
;
wrapper
.
vm
.
$destroy
();
expect
(
removeCustomEventListener
).
toHaveBeenCalledWith
(
mockInstance
,
CUSTOM_EVENTS
.
openAddImageModal
,
wrapper
.
vm
.
onOpenAddImageModal
,
);
});
});
describe
(
'
add image modal
'
,
()
=>
{
it
(
'
renders an addImageModal component
'
,
()
=>
{
expect
(
findAddImageModal
().
exists
()).
toBe
(
true
);
});
it
(
'
calls the onAddImage method when the addImage event is emitted
'
,
()
=>
{
const
mockImage
=
{
imageUrl
:
'
some/url.png
'
,
description
:
'
some description
'
};
const
mockInstance
=
{
exec
:
jest
.
fn
()
};
wrapper
.
vm
.
$refs
.
editor
=
mockInstance
;
findAddImageModal
().
vm
.
$emit
(
'
addImage
'
,
mockImage
);
expect
(
addImage
).
toHaveBeenCalledWith
(
mockInstance
,
mockImage
);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment