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
d2ddb757
Commit
d2ddb757
authored
Apr 28, 2021
by
Enrique Alcántara
Committed by
Paul Slaughter
Apr 28, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Upgrade TipTap to v2
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60006
parent
f1a40767
Changes
20
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
660 additions
and
292 deletions
+660
-292
app/assets/javascripts/content_editor/components/content_editor.vue
.../javascripts/content_editor/components/content_editor.vue
+8
-8
app/assets/javascripts/content_editor/components/toolbar_button.vue
.../javascripts/content_editor/components/toolbar_button.vue
+9
-8
app/assets/javascripts/content_editor/components/top_toolbar.vue
...ets/javascripts/content_editor/components/top_toolbar.vue
+17
-10
app/assets/javascripts/content_editor/extensions/code_block_highlight.js
...scripts/content_editor/extensions/code_block_highlight.js
+17
-32
app/assets/javascripts/content_editor/index.js
app/assets/javascripts/content_editor/index.js
+1
-1
app/assets/javascripts/content_editor/services/content_editor.js
...ets/javascripts/content_editor/services/content_editor.js
+25
-0
app/assets/javascripts/content_editor/services/create_content_editor.js
...ascripts/content_editor/services/create_content_editor.js
+68
-0
app/assets/javascripts/content_editor/services/create_editor.js
...sets/javascripts/content_editor/services/create_editor.js
+0
-73
app/assets/javascripts/content_editor/services/markdown_serializer.js
...avascripts/content_editor/services/markdown_serializer.js
+17
-7
app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
...s/javascripts/pages/shared/wikis/components/wiki_form.vue
+13
-11
config/webpack.config.js
config/webpack.config.js
+3
-3
package.json
package.json
+22
-1
spec/frontend/content_editor/components/content_editor_spec.js
...frontend/content_editor/components/content_editor_spec.js
+37
-16
spec/frontend/content_editor/components/toolbar_button_spec.js
...frontend/content_editor/components/toolbar_button_spec.js
+31
-20
spec/frontend/content_editor/components/top_toolbar_spec.js
spec/frontend/content_editor/components/top_toolbar_spec.js
+13
-12
spec/frontend/content_editor/markdown_processing_spec.js
spec/frontend/content_editor/markdown_processing_spec.js
+4
-3
spec/frontend/content_editor/services/create_content_editor_spec.js
...end/content_editor/services/create_content_editor_spec.js
+34
-0
spec/frontend/content_editor/services/create_editor_spec.js
spec/frontend/content_editor/services/create_editor_spec.js
+0
-49
spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
.../frontend/pages/shared/wikis/components/wiki_form_spec.js
+8
-2
yarn.lock
yarn.lock
+333
-36
No files found.
app/assets/javascripts/content_editor/components/content_editor.vue
View file @
d2ddb757
<
script
>
import
{
EditorContent
,
Editor
}
from
'
tiptap
'
;
import
{
EditorContent
as
TiptapEditorContent
}
from
'
@tiptap/vue-2
'
;
import
{
ContentEditor
}
from
'
../services/content_editor
'
;
import
TopToolbar
from
'
./top_toolbar.vue
'
;
export
default
{
components
:
{
EditorContent
,
Tiptap
EditorContent
,
TopToolbar
,
},
props
:
{
e
ditor
:
{
type
:
Object
,
contentE
ditor
:
{
type
:
ContentEditor
,
required
:
true
,
validator
:
(
editor
)
=>
editor
instanceof
Editor
,
},
},
};
</
script
>
<
template
>
<div
class=
"md md-area"
:class=
"
{ 'is-focused':
editor.f
ocused }">
<top-toolbar
class=
"gl-mb-4"
:
editor=
"e
ditor"
/>
<
editor-content
:editor=
"e
ditor"
/>
<div
class=
"md md-area"
:class=
"
{ 'is-focused':
contentEditor.tiptapEditor.isF
ocused }">
<top-toolbar
class=
"gl-mb-4"
:
content-editor=
"contentE
ditor"
/>
<
tiptap-editor-content
:editor=
"contentEditor.tiptapE
ditor"
/>
</div>
</
template
>
app/assets/javascripts/content_editor/components/toolbar_button.vue
View file @
d2ddb757
<
script
>
import
{
GlButton
,
GlTooltipDirective
as
GlTooltip
}
from
'
@gitlab/ui
'
;
import
{
Editor
as
TiptapEditor
}
from
'
@tiptap/vue-2
'
;
export
default
{
components
:
{
...
...
@@ -13,8 +14,8 @@ export default {
type
:
String
,
required
:
true
,
},
e
ditor
:
{
type
:
Object
,
tiptapE
ditor
:
{
type
:
TiptapEditor
,
required
:
true
,
},
contentType
:
{
...
...
@@ -25,23 +26,23 @@ export default {
type
:
String
,
required
:
true
,
},
e
xecute
Command
:
{
type
:
Boolean
,
e
ditor
Command
:
{
type
:
String
,
required
:
false
,
default
:
true
,
default
:
''
,
},
},
computed
:
{
isActive
()
{
return
this
.
editor
.
isActive
[
this
.
contentType
]()
&&
this
.
editor
.
f
ocused
;
return
this
.
tiptapEditor
.
isActive
(
this
.
contentType
)
&&
this
.
tiptapEditor
.
isF
ocused
;
},
},
methods
:
{
execute
()
{
const
{
contentType
}
=
this
;
if
(
this
.
e
xecute
Command
)
{
this
.
editor
.
commands
[
contentType
]
();
if
(
this
.
e
ditor
Command
)
{
this
.
tiptapEditor
.
chain
()[
this
.
editorCommand
]().
focus
().
run
();
}
this
.
$emit
(
'
click
'
,
{
contentType
});
...
...
app/assets/javascripts/content_editor/components/top_toolbar.vue
View file @
d2ddb757
<
script
>
import
{
ContentEditor
}
from
'
../services/content_editor
'
;
import
Divider
from
'
./divider.vue
'
;
import
ToolbarButton
from
'
./toolbar_button.vue
'
;
...
...
@@ -8,8 +9,8 @@ export default {
Divider
,
},
props
:
{
e
ditor
:
{
type
:
Object
,
contentE
ditor
:
{
type
:
ContentEditor
,
required
:
true
,
},
},
...
...
@@ -23,44 +24,50 @@ export default {
data-testid=
"bold"
content-type=
"bold"
icon-name=
"bold"
editor-command=
"toggleBold"
:label=
"__('Bold text')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
<toolbar-button
data-testid=
"italic"
content-type=
"italic"
icon-name=
"italic"
editor-command=
"toggleItalic"
:label=
"__('Italic text')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
<toolbar-button
data-testid=
"code"
content-type=
"code"
icon-name=
"code"
editor-command=
"toggleCode"
:label=
"__('Code')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
<divider
/>
<toolbar-button
data-testid=
"blockquote"
content-type=
"blockquote"
icon-name=
"quote"
editor-command=
"toggleBlockquote"
:label=
"__('Insert a quote')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
<toolbar-button
data-testid=
"bullet-list"
content-type=
"bullet
_l
ist"
content-type=
"bullet
L
ist"
icon-name=
"list-bulleted"
editor-command=
"toggleBulletList"
:label=
"__('Add a bullet list')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
<toolbar-button
data-testid=
"ordered-list"
content-type=
"ordered
_l
ist"
content-type=
"ordered
L
ist"
icon-name=
"list-numbered"
editor-command=
"toggleOrderedList"
:label=
"__('Add a numbered list')"
:
editor=
"e
ditor"
:
tiptap-editor=
"contentEditor.tiptapE
ditor"
/>
</div>
</
template
>
app/assets/javascripts/content_editor/extensions/code_block_highlight.js
View file @
d2ddb757
import
{
CodeBlock
Highlight
as
BaseCodeBlockHighlight
}
from
'
tiptap-extensions
'
;
import
{
CodeBlock
Lowlight
}
from
'
@tiptap/extension-code-block-lowlight
'
;
export
default
class
GlCodeBlockHighlight
extends
BaseCodeBlockHighlight
{
get
schema
()
{
const
baseSchema
=
super
.
schema
;
return
{
...
baseSchema
,
attrs
:
{
params
:
{
default
:
null
,
},
},
parseDOM
:
[
{
tag
:
'
pre
'
,
preserveWhitespace
:
'
full
'
,
getAttrs
:
(
node
)
=>
{
const
code
=
node
.
querySelector
(
'
code
'
);
if
(
!
code
)
{
return
null
;
}
const
extractLanguage
=
(
element
)
=>
element
.
firstElementChild
?.
getAttribute
(
'
lang
'
);
export
default
CodeBlockLowlight
.
extend
({
addAttributes
()
{
return
{
...
this
.
parent
(),
/* `params` is the name of the attribute that
prosemirror-markdown uses to extract the language
of a codeblock.
https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62
*/
params
:
code
.
getAttribute
(
'
lang
'
),
params
:
{
parseHTML
:
(
element
)
=>
{
return
{
params
:
extractLanguage
(
element
),
};
},
},
],
};
}
}
}
,
}
);
app/assets/javascripts/content_editor/index.js
View file @
d2ddb757
export
{
default
as
createEditor
}
from
'
./services/create
_editor
'
;
export
*
from
'
./services/create_content
_editor
'
;
export
{
default
as
ContentEditor
}
from
'
./components/content_editor.vue
'
;
app/assets/javascripts/content_editor/services/content_editor.js
0 → 100644
View file @
d2ddb757
/* eslint-disable no-underscore-dangle */
export
class
ContentEditor
{
constructor
({
tiptapEditor
,
serializer
})
{
this
.
_tiptapEditor
=
tiptapEditor
;
this
.
_serializer
=
serializer
;
}
get
tiptapEditor
()
{
return
this
.
_tiptapEditor
;
}
async
setSerializedContent
(
serializedContent
)
{
const
{
_tiptapEditor
:
editor
,
_serializer
:
serializer
}
=
this
;
editor
.
commands
.
setContent
(
await
serializer
.
deserialize
({
schema
:
editor
.
schema
,
content
:
serializedContent
}),
);
}
getSerializedContent
()
{
const
{
_tiptapEditor
:
editor
,
_serializer
:
serializer
}
=
this
;
return
serializer
.
serialize
({
schema
:
editor
.
schema
,
content
:
editor
.
getJSON
()
});
}
}
app/assets/javascripts/content_editor/services/create_content_editor.js
0 → 100644
View file @
d2ddb757
import
Blockquote
from
'
@tiptap/extension-blockquote
'
;
import
Bold
from
'
@tiptap/extension-bold
'
;
import
BulletList
from
'
@tiptap/extension-bullet-list
'
;
import
Code
from
'
@tiptap/extension-code
'
;
import
Document
from
'
@tiptap/extension-document
'
;
import
Dropcursor
from
'
@tiptap/extension-dropcursor
'
;
import
Gapcursor
from
'
@tiptap/extension-gapcursor
'
;
import
HardBreak
from
'
@tiptap/extension-hard-break
'
;
import
Heading
from
'
@tiptap/extension-heading
'
;
import
History
from
'
@tiptap/extension-history
'
;
import
HorizontalRule
from
'
@tiptap/extension-horizontal-rule
'
;
import
Image
from
'
@tiptap/extension-image
'
;
import
Italic
from
'
@tiptap/extension-italic
'
;
import
Link
from
'
@tiptap/extension-link
'
;
import
ListItem
from
'
@tiptap/extension-list-item
'
;
import
OrderedList
from
'
@tiptap/extension-ordered-list
'
;
import
Paragraph
from
'
@tiptap/extension-paragraph
'
;
import
Text
from
'
@tiptap/extension-text
'
;
import
{
Editor
}
from
'
@tiptap/vue-2
'
;
import
{
isFunction
}
from
'
lodash
'
;
import
{
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
}
from
'
../constants
'
;
import
CodeBlockHighlight
from
'
../extensions/code_block_highlight
'
;
import
{
ContentEditor
}
from
'
./content_editor
'
;
import
createMarkdownSerializer
from
'
./markdown_serializer
'
;
const
createTiptapEditor
=
({
extensions
=
[],
options
}
=
{})
=>
new
Editor
({
extensions
:
[
Dropcursor
,
Gapcursor
,
History
,
Document
,
Text
,
Paragraph
,
Bold
,
Italic
,
Code
,
Link
,
Heading
,
HardBreak
,
Blockquote
,
HorizontalRule
,
BulletList
,
OrderedList
,
ListItem
,
Image
.
configure
({
inline
:
true
}),
CodeBlockHighlight
,
...
extensions
,
],
editorProps
:
{
attributes
:
{
class
:
'
gl-outline-0!
'
,
},
},
...
options
,
});
export
const
createContentEditor
=
({
renderMarkdown
,
extensions
=
[],
tiptapOptions
}
=
{})
=>
{
if
(
!
isFunction
(
renderMarkdown
))
{
throw
new
Error
(
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
);
}
const
tiptapEditor
=
createTiptapEditor
({
extensions
,
options
:
tiptapOptions
});
const
serializer
=
createMarkdownSerializer
({
render
:
renderMarkdown
});
return
new
ContentEditor
({
tiptapEditor
,
serializer
});
};
app/assets/javascripts/content_editor/services/create_editor.js
deleted
100644 → 0
View file @
f1a40767
import
{
isFunction
,
isString
}
from
'
lodash
'
;
import
{
Editor
}
from
'
tiptap
'
;
import
{
Bold
,
Italic
,
Code
,
Link
,
Image
,
Heading
,
Blockquote
,
HorizontalRule
,
BulletList
,
OrderedList
,
ListItem
,
HardBreak
,
}
from
'
tiptap-extensions
'
;
import
{
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
}
from
'
../constants
'
;
import
CodeBlockHighlight
from
'
../extensions/code_block_highlight
'
;
import
createMarkdownSerializer
from
'
./markdown_serializer
'
;
const
createEditor
=
async
({
content
,
renderMarkdown
,
serializer
:
customSerializer
,
...
options
}
=
{})
=>
{
if
(
!
customSerializer
&&
!
isFunction
(
renderMarkdown
))
{
throw
new
Error
(
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
);
}
const
editor
=
new
Editor
({
extensions
:
[
new
Bold
(),
new
Italic
(),
new
Code
(),
new
Link
(),
new
Image
(),
new
Heading
({
levels
:
[
1
,
2
,
3
,
4
,
5
,
6
]
}),
new
Blockquote
(),
new
HorizontalRule
(),
new
BulletList
(),
new
ListItem
(),
new
OrderedList
(),
new
CodeBlockHighlight
(),
new
HardBreak
(),
],
editorProps
:
{
attributes
:
{
class
:
'
gl-outline-0!
'
,
},
},
...
options
,
});
const
serializer
=
customSerializer
||
createMarkdownSerializer
({
render
:
renderMarkdown
});
editor
.
setSerializedContent
=
async
(
serializedContent
)
=>
{
editor
.
setContent
(
await
serializer
.
deserialize
({
schema
:
editor
.
schema
,
content
:
serializedContent
}),
);
};
editor
.
getSerializedContent
=
()
=>
{
return
serializer
.
serialize
({
schema
:
editor
.
schema
,
content
:
editor
.
getJSON
()
});
};
if
(
isString
(
content
))
{
await
editor
.
setSerializedContent
(
content
);
}
return
editor
;
};
export
default
createEditor
;
app/assets/javascripts/content_editor/services/markdown_serializer.js
View file @
d2ddb757
...
...
@@ -54,14 +54,24 @@ const create = ({ render = () => null }) => {
*/
serialize
:
({
schema
,
content
})
=>
{
const
document
=
schema
.
nodeFromJSON
(
content
);
const
serializer
=
new
ProseMirrorMarkdownSerializer
(
defaultMarkdownSerializer
.
nodes
,
{
...
defaultMarkdownSerializer
.
marks
,
bold
:
{
// creates a bold alias for the strong mark converter
...
defaultMarkdownSerializer
.
marks
.
strong
,
const
{
nodes
,
marks
}
=
defaultMarkdownSerializer
;
const
serializer
=
new
ProseMirrorMarkdownSerializer
(
{
...
defaultMarkdownSerializer
.
nodes
,
horizontalRule
:
nodes
.
horizontal_rule
,
bulletList
:
nodes
.
bullet_list
,
listItem
:
nodes
.
list_item
,
orderedList
:
nodes
.
ordered_list
,
codeBlock
:
nodes
.
code_block
,
hardBreak
:
nodes
.
hard_break
,
},
{
...
defaultMarkdownSerializer
.
marks
,
bold
:
marks
.
strong
,
italic
:
{
open
:
'
_
'
,
close
:
'
_
'
,
mixable
:
true
,
expelEnclosingWhitespace
:
true
},
});
},
);
return
serializer
.
serialize
(
document
,
{
tightLists
:
true
,
...
...
app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
View file @
d2ddb757
...
...
@@ -39,7 +39,7 @@ export default {
isContentEditorLoading
:
true
,
useContentEditor
:
false
,
commitMessage
:
''
,
e
ditor
:
null
,
contentE
ditor
:
null
,
isDirty
:
false
,
};
},
...
...
@@ -102,7 +102,7 @@ export default {
handleFormSubmit
()
{
if
(
this
.
useContentEditor
)
{
this
.
content
=
this
.
e
ditor
.
getSerializedContent
();
this
.
content
=
this
.
contentE
ditor
.
getSerializedContent
();
}
this
.
isDirty
=
false
;
...
...
@@ -136,16 +136,18 @@ export default {
this
.
isContentEditorLoading
=
true
;
this
.
useContentEditor
=
true
;
const
createEditor
=
await
import
(
/* webpackChunkName: 'content_editor' */
'
~/content_editor/services/create_editor
'
const
{
createContentEditor
}
=
await
import
(
/* webpackChunkName: 'content_editor' */
'
~/content_editor/services/create_
content_
editor
'
);
this
.
e
ditor
=
this
.
e
ditor
||
(
await
createEditor
.
default
({
this
.
contentE
ditor
=
this
.
contentE
ditor
||
createContentEditor
({
renderMarkdown
:
(
markdown
)
=>
this
.
getContentHTML
(
markdown
),
tiptapOptions
:
{
onUpdate
:
()
=>
this
.
handleContentChange
(),
}));
await
this
.
editor
.
setSerializedContent
(
this
.
content
);
},
});
await
this
.
contentEditor
.
setSerializedContent
(
this
.
content
);
this
.
isContentEditorLoading
=
false
;
},
...
...
@@ -296,7 +298,7 @@ export default {
<div
v-if=
"isContentEditorActive"
>
<gl-loading-icon
v-if=
"isContentEditorLoading"
class=
"bordered-box gl-w-full gl-py-6"
/>
<content-editor
v-else
:
editor=
"e
ditor"
/>
<content-editor
v-else
:
content-editor=
"contentE
ditor"
/>
<input
id=
"wiki_content"
v-model.trim=
"content"
type=
"hidden"
name=
"wiki[content]"
/>
</div>
...
...
config/webpack.config.js
View file @
d2ddb757
...
...
@@ -307,11 +307,11 @@ module.exports = {
chunks
:
'
initial
'
,
minChunks
:
autoEntriesCount
*
0.9
,
}),
tiptap
:
{
prosemirror
:
{
priority
:
17
,
name
:
'
tiptap
'
,
name
:
'
prosemirror
'
,
chunks
:
'
all
'
,
test
:
/
[\\/]
node_modules
[\\/]
(
tiptap|prosemirror
)
-
?\w
*
[\\/]
/
,
test
:
/
[\\/]
node_modules
[\\/]
prosemirror.*
?
[\\/]
/
,
minChunks
:
2
,
reuseExistingChunk
:
true
,
},
...
...
package.json
View file @
d2ddb757
...
...
@@ -57,6 +57,28 @@
"
@rails/ujs
"
:
"
^6.0.3-4
"
,
"
@sentry/browser
"
:
"
^5.22.3
"
,
"
@sourcegraph/code-host-integration
"
:
"
0.0.52
"
,
"
@tiptap/core
"
:
"
^2.0.0-beta.38
"
,
"
@tiptap/extension-blockquote
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-bold
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-bullet-list
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-code
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-code-block-lowlight
"
:
"
^2.0.0-beta.9
"
,
"
@tiptap/extension-document
"
:
"
^2.0.0-beta.5
"
,
"
@tiptap/extension-dropcursor
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-gapcursor
"
:
"
^2.0.0-beta.10
"
,
"
@tiptap/extension-hard-break
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-heading
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-history
"
:
"
^2.0.0-beta.5
"
,
"
@tiptap/extension-horizontal-rule
"
:
"
^2.0.0-beta.7
"
,
"
@tiptap/extension-image
"
:
"
^2.0.0-beta.4
"
,
"
@tiptap/extension-italic
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-link
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-list-item
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-ordered-list
"
:
"
^2.0.0-beta.6
"
,
"
@tiptap/extension-paragraph
"
:
"
^2.0.0-beta.7
"
,
"
@tiptap/extension-strike
"
:
"
^2.0.0-beta.7
"
,
"
@tiptap/extension-text
"
:
"
^2.0.0-beta.5
"
,
"
@tiptap/vue-2
"
:
"
^2.0.0-beta.21
"
,
"
@toast-ui/editor
"
:
"
^2.5.2
"
,
"
@toast-ui/vue-editor
"
:
"
^2.5.2
"
,
"
apollo-cache-inmemory
"
:
"
^1.6.6
"
,
...
...
@@ -139,7 +161,6 @@
"
three-stl-loader
"
:
"
^1.0.4
"
,
"
timeago.js
"
:
"
^4.0.2
"
,
"
tiptap
"
:
"
^1.32.1
"
,
"
tiptap-commands
"
:
"
^1.17.1
"
,
"
tiptap-extensions
"
:
"
^1.35.1
"
,
"
url-loader
"
:
"
^4.1.1
"
,
"
uuid
"
:
"
8.1.0
"
,
...
...
spec/frontend/content_editor/components/content_editor_spec.js
View file @
d2ddb757
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
EditorContent
}
from
'
tiptap
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
EditorContent
}
from
'
@tiptap/vue-2
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
ContentEditor
from
'
~/content_editor/components/content_editor.vue
'
;
import
TopToolbar
from
'
~/content_editor/components/top_toolbar.vue
'
;
import
createEditor
from
'
~/content_editor/services/create
_editor
'
;
import
{
createContentEditor
}
from
'
~/content_editor/services/create_content
_editor
'
;
describe
(
'
ContentEditor
'
,
()
=>
{
let
wrapper
;
let
editor
;
const
createWrapper
=
async
(
_e
ditor
)
=>
{
wrapper
=
m
ount
(
ContentEditor
,
{
const
createWrapper
=
async
(
contentE
ditor
)
=>
{
wrapper
=
shallowM
ount
(
ContentEditor
,
{
propsData
:
{
editor
:
_e
ditor
,
contentE
ditor
,
},
});
};
beforeEach
(
async
()
=>
{
editor
=
await
createEditor
({
renderMarkdown
:
()
=>
'
sample text
'
});
createWrapper
(
editor
);
await
waitForPromises
();
beforeEach
(()
=>
{
editor
=
createContentEditor
({
renderMarkdown
:
()
=>
true
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders editor content component and attaches editor instance
'
,
async
()
=>
{
expect
(
wrapper
.
findComponent
(
EditorContent
).
props
().
editor
).
toBe
(
editor
);
it
(
'
renders editor content component and attaches editor instance
'
,
()
=>
{
createWrapper
(
editor
);
expect
(
wrapper
.
findComponent
(
EditorContent
).
props
().
editor
).
toBe
(
editor
.
tiptapEditor
);
});
it
(
'
renders top toolbar component and attaches editor instance
'
,
()
=>
{
createWrapper
(
editor
);
expect
(
wrapper
.
findComponent
(
TopToolbar
).
props
().
contentEditor
).
toBe
(
editor
);
});
it
(
'
renders top toolbar component and attaches editor instance
'
,
async
()
=>
{
expect
(
wrapper
.
findComponent
(
TopToolbar
).
props
().
editor
).
toBe
(
editor
);
it
.
each
`
isFocused | classes
${
true
}
|
${[
'
md
'
,
'
md-area
'
,
'
is-focused
'
]}
${
false
}
|
${[
'
md
'
,
'
md-area
'
]}
`
(
'
has $classes class selectors when tiptapEditor.isFocused = $isFocused
'
,
({
isFocused
,
classes
})
=>
{
editor
.
tiptapEditor
.
isFocused
=
isFocused
;
createWrapper
(
editor
);
expect
(
wrapper
.
classes
()).
toStrictEqual
(
classes
);
},
);
it
(
'
adds isFocused class when tiptapEditor is focused
'
,
()
=>
{
editor
.
tiptapEditor
.
isFocused
=
true
;
createWrapper
(
editor
);
expect
(
wrapper
.
classes
()).
toContain
(
'
is-focused
'
);
});
});
spec/frontend/content_editor/components/toolbar_button_spec.js
View file @
d2ddb757
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
{
Extension
}
from
'
@tiptap/core
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
ToolbarButton
from
'
~/content_editor/components/toolbar_button.vue
'
;
import
{
createContentEditor
}
from
'
~/content_editor/services/create_content_editor
'
;
describe
(
'
content_editor/components/toolbar_button
'
,
()
=>
{
let
wrapper
;
let
editor
;
let
tiptapEditor
;
let
toggleFooSpy
;
const
CONTENT_TYPE
=
'
bold
'
;
const
ICON_NAME
=
'
bold
'
;
const
LABEL
=
'
Bold
'
;
const
buildEditor
=
()
=>
{
editor
=
{
isActive
:
{
[
CONTENT_TYPE
]:
jest
.
fn
(),
},
commands
:
{
[
CONTENT_TYPE
]:
jest
.
fn
(),
}
,
toggleFooSpy
=
jest
.
fn
();
tiptapEditor
=
createContentEditor
(
{
extensions
:
[
Extension
.
create
({
addCommands
()
{
return
{
toggleFoo
:
()
=>
toggleFooSpy
,
};
},
}),
],
renderMarkdown
:
()
=>
true
,
}).
tiptapEditor
;
jest
.
spyOn
(
tiptapEditor
,
'
isActive
'
);
};
const
buildWrapper
=
(
propsData
=
{})
=>
{
...
...
@@ -26,7 +36,7 @@ describe('content_editor/components/toolbar_button', () => {
GlButton
,
},
propsData
:
{
e
ditor
,
tiptapE
ditor
,
contentType
:
CONTENT_TYPE
,
iconName
:
ICON_NAME
,
label
:
LABEL
,
...
...
@@ -52,33 +62,34 @@ describe('content_editor/components/toolbar_button', () => {
it
.
each
`
editorState | outcomeDescription | outcome
${{
isActive
:
true
,
f
ocused
:
true
}
} |
${
'
button is active
'
}
|
${
true
}
${{
isActive
:
false
,
f
ocused
:
true
}
} |
${
'
button is not active
'
}
|
${
false
}
${{
isActive
:
true
,
f
ocused
:
false
}
} |
${
'
button is not active
'
}
|
${
false
}
${{
isActive
:
true
,
isF
ocused
:
true
}
} |
${
'
button is active
'
}
|
${
true
}
${{
isActive
:
false
,
isF
ocused
:
true
}
} |
${
'
button is not active
'
}
|
${
false
}
${{
isActive
:
true
,
isF
ocused
:
false
}
} |
${
'
button is not active
'
}
|
${
false
}
`
(
'
$outcomeDescription when when editor state is $editorState
'
,
({
editorState
,
outcome
})
=>
{
editor
.
isActive
[
CONTENT_TYPE
]
.
mockReturnValueOnce
(
editorState
.
isActive
);
editor
.
focused
=
editorState
.
f
ocused
;
tiptapEditor
.
isActive
.
mockReturnValueOnce
(
editorState
.
isActive
);
tiptapEditor
.
isFocused
=
editorState
.
isF
ocused
;
buildWrapper
();
expect
(
findButton
().
classes
().
includes
(
'
active
'
)).
toBe
(
outcome
);
expect
(
tiptapEditor
.
isActive
).
toHaveBeenCalledWith
(
CONTENT_TYPE
);
});
describe
(
'
when button is clicked
'
,
()
=>
{
it
(
'
executes the content type command when executeCommand = true
'
,
async
()
=>
{
buildWrapper
({
e
xecuteCommand
:
true
});
buildWrapper
({
e
ditorCommand
:
'
toggleFoo
'
});
await
findButton
().
trigger
(
'
click
'
);
expect
(
editor
.
commands
[
CONTENT_TYPE
]
).
toHaveBeenCalled
();
expect
(
toggleFooSpy
).
toHaveBeenCalled
();
expect
(
wrapper
.
emitted
().
click
).
toHaveLength
(
1
);
});
it
(
'
does not executes the content type command when executeCommand = false
'
,
async
()
=>
{
buildWrapper
(
{
executeCommand
:
false
}
);
buildWrapper
();
await
findButton
().
trigger
(
'
click
'
);
expect
(
editor
.
commands
[
CONTENT_TYPE
]
).
not
.
toHaveBeenCalled
();
expect
(
toggleFooSpy
).
not
.
toHaveBeenCalled
();
expect
(
wrapper
.
emitted
().
click
).
toHaveLength
(
1
);
});
});
...
...
spec/frontend/content_editor/components/top_toolbar_spec.js
View file @
d2ddb757
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
TopToolbar
from
'
~/content_editor/components/top_toolbar.vue
'
;
import
{
createContentEditor
}
from
'
~/content_editor/services/create_content_editor
'
;
describe
(
'
content_editor/components/top_toolbar
'
,
()
=>
{
let
wrapper
;
let
e
ditor
;
let
contentE
ditor
;
const
buildEditor
=
()
=>
{
editor
=
{}
;
contentEditor
=
createContentEditor
({
renderMarkdown
:
()
=>
true
})
;
};
const
buildWrapper
=
()
=>
{
wrapper
=
extendedWrapper
(
shallowMount
(
TopToolbar
,
{
propsData
:
{
e
ditor
,
contentE
ditor
,
},
}),
);
...
...
@@ -29,18 +30,18 @@ describe('content_editor/components/top_toolbar', () => {
});
it
.
each
`
testId | button
${
'
bold
'
}
|
${{
contentType
:
'
bold
'
,
iconName
:
'
bold
'
,
label
:
'
Bold
'
}
}
${
'
italic
'
}
|
${{
contentType
:
'
italic
'
,
iconName
:
'
italic
'
,
label
:
'
Italic
'
}
}
${
'
code
'
}
|
${{
contentType
:
'
code
'
,
iconName
:
'
code
'
,
label
:
'
Code
'
}
}
${
'
blockquote
'
}
|
${{
contentType
:
'
blockquote
'
,
iconName
:
'
quote
'
,
label
:
'
Insert a quote
'
}
}
${
'
bullet-list
'
}
|
${{
contentType
:
'
bullet
_list
'
,
iconName
:
'
list-bulleted
'
,
label
:
'
Add a bullet l
ist
'
}
}
${
'
ordered-list
'
}
|
${{
contentType
:
'
ordered
_list
'
,
iconName
:
'
list-numbered
'
,
label
:
'
Add a numbered l
ist
'
}
}
testId | button
Props
${
'
bold
'
}
|
${{
contentType
:
'
bold
'
,
iconName
:
'
bold
'
,
label
:
'
Bold
text
'
,
editorCommand
:
'
toggleBold
'
}
}
${
'
italic
'
}
|
${{
contentType
:
'
italic
'
,
iconName
:
'
italic
'
,
label
:
'
Italic
text
'
,
editorCommand
:
'
toggleItalic
'
}
}
${
'
code
'
}
|
${{
contentType
:
'
code
'
,
iconName
:
'
code
'
,
label
:
'
Code
'
,
editorCommand
:
'
toggleCode
'
}
}
${
'
blockquote
'
}
|
${{
contentType
:
'
blockquote
'
,
iconName
:
'
quote
'
,
label
:
'
Insert a quote
'
,
editorCommand
:
'
toggleBlockquote
'
}
}
${
'
bullet-list
'
}
|
${{
contentType
:
'
bullet
List
'
,
iconName
:
'
list-bulleted
'
,
label
:
'
Add a bullet list
'
,
editorCommand
:
'
toggleBulletL
ist
'
}
}
${
'
ordered-list
'
}
|
${{
contentType
:
'
ordered
List
'
,
iconName
:
'
list-numbered
'
,
label
:
'
Add a numbered list
'
,
editorCommand
:
'
toggleOrderedL
ist
'
}
}
`
(
'
renders $testId button
'
,
({
testId
,
buttonProps
})
=>
{
buildWrapper
();
expect
(
wrapper
.
findByTestId
(
testId
).
props
()).
to
MatchObject
({
expect
(
wrapper
.
findByTestId
(
testId
).
props
()).
to
Equal
({
...
buttonProps
,
e
ditor
,
tiptapEditor
:
contentEditor
.
tiptapE
ditor
,
});
});
});
spec/frontend/content_editor/markdown_processing_spec.js
View file @
d2ddb757
import
{
createEditor
}
from
'
~/content_editor
'
;
import
{
create
Content
Editor
}
from
'
~/content_editor
'
;
import
{
loadMarkdownApiExamples
,
loadMarkdownApiResult
}
from
'
./markdown_processing_examples
'
;
describe
(
'
markdown processing
'
,
()
=>
{
// Ensure we generate same markdown that was provided to Markdown API.
it
.
each
(
loadMarkdownApiExamples
())(
'
correctly handles %s
'
,
async
(
testName
,
markdown
)
=>
{
const
{
html
}
=
loadMarkdownApiResult
(
testName
);
const
editor
=
await
createEditor
({
content
:
markdown
,
renderMarkdown
:
()
=>
html
});
const
contentEditor
=
createContentEditor
({
renderMarkdown
:
()
=>
html
});
await
contentEditor
.
setSerializedContent
(
markdown
);
expect
(
e
ditor
.
getSerializedContent
()).
toBe
(
markdown
);
expect
(
contentE
ditor
.
getSerializedContent
()).
toBe
(
markdown
);
});
});
spec/frontend/content_editor/services/create_content_editor_spec.js
0 → 100644
View file @
d2ddb757
import
{
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
}
from
'
~/content_editor/constants
'
;
import
{
createContentEditor
}
from
'
~/content_editor/services/create_content_editor
'
;
describe
(
'
content_editor/services/create_editor
'
,
()
=>
{
let
renderMarkdown
;
let
editor
;
beforeEach
(()
=>
{
renderMarkdown
=
jest
.
fn
();
editor
=
createContentEditor
({
renderMarkdown
});
});
it
(
'
sets gl-outline-0! class selector to the tiptapEditor instance
'
,
()
=>
{
expect
(
editor
.
tiptapEditor
.
options
.
editorProps
).
toMatchObject
({
attributes
:
{
class
:
'
gl-outline-0!
'
,
},
});
});
it
(
'
provides the renderMarkdown function to the markdown serializer
'
,
async
()
=>
{
const
serializedContent
=
'
**bold text**
'
;
renderMarkdown
.
mockReturnValueOnce
(
'
<p><b>bold text</b></p>
'
);
await
editor
.
setSerializedContent
(
serializedContent
);
expect
(
renderMarkdown
).
toHaveBeenCalledWith
(
serializedContent
);
});
it
(
'
throws an error when a renderMarkdown fn is not provided
'
,
()
=>
{
expect
(()
=>
createContentEditor
()).
toThrow
(
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
);
});
});
spec/frontend/content_editor/services/create_editor_spec.js
deleted
100644 → 0
View file @
f1a40767
import
{
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
}
from
'
~/content_editor/constants
'
;
import
createEditor
from
'
~/content_editor/services/create_editor
'
;
import
createMarkdownSerializer
from
'
~/content_editor/services/markdown_serializer
'
;
jest
.
mock
(
'
~/content_editor/services/markdown_serializer
'
);
describe
(
'
content_editor/services/create_editor
'
,
()
=>
{
const
renderMarkdown
=
()
=>
true
;
const
buildMockSerializer
=
()
=>
({
serialize
:
jest
.
fn
(),
deserialize
:
jest
.
fn
(),
});
it
(
'
sets gl-outline-0! class selector to editor attributes
'
,
async
()
=>
{
const
editor
=
await
createEditor
({
renderMarkdown
});
expect
(
editor
.
options
.
editorProps
).
toMatchObject
({
attributes
:
{
class
:
'
gl-outline-0!
'
,
},
});
});
describe
(
'
creating an editor
'
,
()
=>
{
it
(
'
uses markdown serializer when a renderMarkdown function is provided
'
,
async
()
=>
{
const
mockSerializer
=
buildMockSerializer
();
createMarkdownSerializer
.
mockReturnValueOnce
(
mockSerializer
);
await
createEditor
({
renderMarkdown
});
expect
(
createMarkdownSerializer
).
toHaveBeenCalledWith
({
render
:
renderMarkdown
});
});
it
(
'
uses custom serializer when it is provided
'
,
async
()
=>
{
const
mockSerializer
=
buildMockSerializer
();
const
serializedContent
=
'
**bold**
'
;
mockSerializer
.
serialize
.
mockReturnValueOnce
(
serializedContent
);
const
editor
=
await
createEditor
({
serializer
:
mockSerializer
});
expect
(
editor
.
getSerializedContent
()).
toBe
(
serializedContent
);
});
it
(
'
throws an error when neither a serializer or renderMarkdown fn are provided
'
,
async
()
=>
{
await
expect
(
createEditor
()).
rejects
.
toThrow
(
PROVIDE_SERIALIZER_OR_RENDERER_ERROR
);
});
});
});
spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
View file @
d2ddb757
...
...
@@ -337,7 +337,10 @@ describe('WikiForm', () => {
// wait for content editor to load
await
waitForPromises
();
wrapper
.
vm
.
editor
.
setContent
(
'
<p>hello __world__ from content editor</p>
'
,
true
);
wrapper
.
vm
.
contentEditor
.
tiptapEditor
.
commands
.
setContent
(
'
<p>hello __world__ from content editor</p>
'
,
true
,
);
await
waitForPromises
();
...
...
@@ -378,7 +381,10 @@ describe('WikiForm', () => {
// wait for content editor to load
await
waitForPromises
();
wrapper
.
vm
.
editor
.
setContent
(
'
<p>hello __world__ from content editor</p>
'
,
true
);
wrapper
.
vm
.
contentEditor
.
tiptapEditor
.
commands
.
setContent
(
'
<p>hello __world__ from content editor</p>
'
,
true
,
);
wrapper
.
findComponent
(
GlAlert
).
findComponent
(
GlButton
).
trigger
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
...
...
yarn.lock
View file @
d2ddb757
This diff is collapsed.
Click to expand it.
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