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
c416d436
Commit
c416d436
authored
Jul 08, 2020
by
Jacques Erasmus
Committed by
Natalia Tepluhina
Jul 08, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add ability to upload images
Add the ability to upload images via the SSE
parent
37d86926
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
161 additions
and
29 deletions
+161
-29
app/assets/javascripts/static_site_editor/components/edit_area.vue
...s/javascripts/static_site_editor/components/edit_area.vue
+18
-1
app/assets/javascripts/static_site_editor/constants.js
app/assets/javascripts/static_site_editor/constants.js
+2
-0
app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
...c_site_editor/graphql/resolvers/submit_content_changes.js
+2
-2
app/assets/javascripts/static_site_editor/image_repository.js
...assets/javascripts/static_site_editor/image_repository.js
+20
-0
app/assets/javascripts/static_site_editor/pages/home.vue
app/assets/javascripts/static_site_editor/pages/home.vue
+4
-3
app/assets/javascripts/static_site_editor/services/image_service.js
.../javascripts/static_site_editor/services/image_service.js
+9
-0
app/assets/javascripts/static_site_editor/services/submit_content_changes.js
...pts/static_site_editor/services/submit_content_changes.js
+29
-3
app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
.../rich_content_editor/modals/add_image/add_image_modal.vue
+11
-1
app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
...ed/components/rich_content_editor/rich_content_editor.vue
+7
-6
app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js
.../components/rich_content_editor/services/image_service.js
+0
-2
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/static_site_editor/mock_data.js
spec/frontend/static_site_editor/mock_data.js
+7
-0
spec/frontend/static_site_editor/services/submit_content_changes_spec.js
...tatic_site_editor/services/submit_content_changes_spec.js
+40
-8
spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
...h_content_editor/modals/add_image/add_image_modal_spec.js
+7
-2
spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
...omponents/rich_content_editor/rich_content_editor_spec.js
+2
-1
No files found.
app/assets/javascripts/static_site_editor/components/edit_area.vue
View file @
c416d436
...
...
@@ -5,6 +5,8 @@ import EditHeader from './edit_header.vue';
import
UnsavedChangesConfirmDialog
from
'
./unsaved_changes_confirm_dialog.vue
'
;
import
parseSourceFile
from
'
~/static_site_editor/services/parse_source_file
'
;
import
{
EDITOR_TYPES
}
from
'
~/vue_shared/components/rich_content_editor/constants
'
;
import
{
DEFAULT_IMAGE_UPLOAD_PATH
}
from
'
../constants
'
;
import
imageRepository
from
'
../image_repository
'
;
export
default
{
components
:
{
...
...
@@ -31,6 +33,12 @@ export default {
required
:
false
,
default
:
''
,
},
imageRoot
:
{
type
:
String
,
required
:
false
,
default
:
DEFAULT_IMAGE_UPLOAD_PATH
,
validator
:
prop
=>
prop
.
endsWith
(
'
/
'
),
},
},
data
()
{
return
{
...
...
@@ -40,6 +48,7 @@ export default {
isModified
:
false
,
};
},
imageRepository
:
imageRepository
(),
computed
:
{
editableContent
()
{
return
this
.
parsedSource
.
content
(
this
.
isWysiwygMode
);
...
...
@@ -57,8 +66,14 @@ export default {
this
.
editorMode
=
mode
;
this
.
$refs
.
editor
.
resetInitialValue
(
this
.
editableContent
);
},
onUploadImage
({
file
,
imageUrl
})
{
this
.
$options
.
imageRepository
.
add
(
file
,
imageUrl
);
},
onSubmit
()
{
this
.
$emit
(
'
submit
'
,
{
content
:
this
.
parsedSource
.
content
()
});
this
.
$emit
(
'
submit
'
,
{
content
:
this
.
parsedSource
.
content
(),
images
:
this
.
$options
.
imageRepository
.
getAll
(),
});
},
},
};
...
...
@@ -70,9 +85,11 @@ export default {
ref=
"editor"
:content=
"editableContent"
:initial-edit-type=
"editorMode"
:image-root=
"imageRoot"
class=
"mb-9 h-100"
@
modeChange=
"onModeChange"
@
input=
"onInputChange"
@
uploadImage=
"onUploadImage"
/>
<unsaved-changes-confirm-dialog
:modified=
"isModified"
/>
<publish-toolbar
...
...
app/assets/javascripts/static_site_editor/constants.js
View file @
c416d436
...
...
@@ -19,3 +19,5 @@ export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
export
const
TRACKING_ACTION_CREATE_COMMIT
=
'
create_commit
'
;
export
const
TRACKING_ACTION_CREATE_MERGE_REQUEST
=
'
create_merge_request
'
;
export
const
TRACKING_ACTION_INITIALIZE_EDITOR
=
'
initialize_editor
'
;
export
const
DEFAULT_IMAGE_UPLOAD_PATH
=
'
source/images/uploads/
'
;
app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
View file @
c416d436
...
...
@@ -3,10 +3,10 @@ import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
const
submitContentChangesResolver
=
(
_
,
{
input
:
{
project
:
projectId
,
username
,
sourcePath
,
content
}
},
{
input
:
{
project
:
projectId
,
username
,
sourcePath
,
content
,
images
}
},
{
cache
},
)
=>
{
return
submitContentChanges
({
projectId
,
username
,
sourcePath
,
content
}).
then
(
return
submitContentChanges
({
projectId
,
username
,
sourcePath
,
content
,
images
}).
then
(
savedContentMeta
=>
{
cache
.
writeQuery
({
query
:
savedContentMetaQuery
,
...
...
app/assets/javascripts/static_site_editor/image_repository.js
0 → 100644
View file @
c416d436
import
{
__
}
from
'
~/locale
'
;
import
Flash
from
'
~/flash
'
;
import
{
getBinary
}
from
'
./services/image_service
'
;
const
imageRepository
=
()
=>
{
const
images
=
new
Map
();
const
flash
=
message
=>
new
Flash
(
message
);
const
add
=
(
file
,
url
)
=>
{
getBinary
(
file
)
.
then
(
content
=>
images
.
set
(
url
,
content
))
.
catch
(()
=>
flash
(
__
(
'
Something went wrong while inserting your image. Please try again.
'
)));
};
const
getAll
=
()
=>
images
;
return
{
add
,
getAll
};
};
export
default
imageRepository
;
app/assets/javascripts/static_site_editor/pages/home.vue
View file @
c416d436
...
...
@@ -67,11 +67,11 @@ export default {
onDismissError
()
{
this
.
submitChangesError
=
null
;
},
onSubmit
({
content
})
{
onSubmit
({
content
,
images
})
{
this
.
content
=
content
;
this
.
submitChanges
();
this
.
submitChanges
(
images
);
},
submitChanges
()
{
submitChanges
(
images
)
{
this
.
isSavingChanges
=
true
;
this
.
$apollo
...
...
@@ -83,6 +83,7 @@ export default {
username
:
this
.
appData
.
username
,
sourcePath
:
this
.
appData
.
sourcePath
,
content
:
this
.
content
,
images
,
},
},
})
...
...
app/assets/javascripts/static_site_editor/services/image_service.js
0 → 100644
View file @
c416d436
// eslint-disable-next-line import/prefer-default-export
export
const
getBinary
=
file
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
reader
=
new
FileReader
();
reader
.
readAsDataURL
(
file
);
reader
.
onload
=
()
=>
resolve
(
reader
.
result
.
split
(
'
,
'
)[
1
]);
reader
.
onerror
=
error
=>
reject
(
error
);
});
};
app/assets/javascripts/static_site_editor/services/submit_content_changes.js
View file @
c416d436
...
...
@@ -21,7 +21,32 @@ const createBranch = (projectId, branch) =>
throw
new
Error
(
SUBMIT_CHANGES_BRANCH_ERROR
);
});
const
commitContent
=
(
projectId
,
message
,
branch
,
sourcePath
,
content
)
=>
{
const
createImageActions
=
(
images
,
markdown
)
=>
{
const
actions
=
[];
if
(
!
markdown
)
{
return
actions
;
}
images
.
forEach
((
imageContent
,
filePath
)
=>
{
const
imageExistsInMarkdown
=
path
=>
new
RegExp
(
`!\\[([^[\\]\\n]*)\\](\\(
${
path
}
)\\)`
);
// matches the image markdown syntax: ![<any-string-except-newline>](<path>)
if
(
imageExistsInMarkdown
(
filePath
).
test
(
markdown
))
{
actions
.
push
(
convertObjectPropsToSnakeCase
({
encoding
:
'
base64
'
,
action
:
'
create
'
,
content
:
imageContent
,
filePath
,
}),
);
}
});
return
actions
;
};
const
commitContent
=
(
projectId
,
message
,
branch
,
sourcePath
,
content
,
images
)
=>
{
Tracking
.
event
(
document
.
body
.
dataset
.
page
,
TRACKING_ACTION_CREATE_COMMIT
);
return
Api
.
commitMultiple
(
...
...
@@ -35,6 +60,7 @@ const commitContent = (projectId, message, branch, sourcePath, content) => {
filePath
:
sourcePath
,
content
,
}),
...
createImageActions
(
images
,
content
),
],
}),
).
catch
(()
=>
{
...
...
@@ -62,7 +88,7 @@ const createMergeRequest = (
});
};
const
submitContentChanges
=
({
username
,
projectId
,
sourcePath
,
content
})
=>
{
const
submitContentChanges
=
({
username
,
projectId
,
sourcePath
,
content
,
images
})
=>
{
const
branch
=
generateBranchName
(
username
);
const
mergeRequestTitle
=
sprintf
(
s__
(
`StaticSiteEditor|Update %{sourcePath} file`
),
{
sourcePath
,
...
...
@@ -73,7 +99,7 @@ const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
.
then
(({
data
:
{
web_url
:
url
}
})
=>
{
Object
.
assign
(
meta
,
{
branch
:
{
label
:
branch
,
url
}
});
return
commitContent
(
projectId
,
mergeRequestTitle
,
branch
,
sourcePath
,
content
);
return
commitContent
(
projectId
,
mergeRequestTitle
,
branch
,
sourcePath
,
content
,
images
);
})
.
then
(({
data
:
{
short_id
:
label
,
web_url
:
url
}
})
=>
{
Object
.
assign
(
meta
,
{
commit
:
{
label
,
url
}
});
...
...
app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
View file @
c416d436
...
...
@@ -16,8 +16,15 @@ export default {
GlTab
,
},
mixins
:
[
glFeatureFlagMixin
()],
props
:
{
imageRoot
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
file
:
null
,
urlError
:
null
,
imageUrl
:
null
,
description
:
null
,
...
...
@@ -38,6 +45,7 @@ export default {
},
methods
:
{
show
()
{
this
.
file
=
null
;
this
.
urlError
=
null
;
this
.
imageUrl
=
null
;
this
.
description
=
null
;
...
...
@@ -66,7 +74,9 @@ export default {
return
;
}
this
.
$emit
(
'
addImage
'
,
{
file
,
altText
:
altText
||
file
.
name
});
const
imageUrl
=
`
${
this
.
imageRoot
}${
file
.
name
}
`
;
this
.
$emit
(
'
addImage
'
,
{
imageUrl
,
file
,
altText
:
altText
||
file
.
name
});
},
submitURL
(
event
)
{
if
(
!
this
.
validateUrl
())
{
...
...
app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
View file @
c416d436
...
...
@@ -19,8 +19,6 @@ import {
getMarkdown
,
}
from
'
./services/editor_service
'
;
import
{
getUrl
}
from
'
./services/image_service
'
;
export
default
{
components
:
{
ToastEditor
:
()
=>
...
...
@@ -54,6 +52,11 @@ export default {
required
:
false
,
default
:
EDITOR_PREVIEW_STYLE
,
},
imageRoot
:
{
type
:
String
,
required
:
true
,
validator
:
prop
=>
prop
.
endsWith
(
'
/
'
),
},
},
data
()
{
return
{
...
...
@@ -104,10 +107,8 @@ export default {
const
image
=
{
imageUrl
,
altText
};
if
(
file
)
{
image
.
imageUrl
=
getUrl
(
file
);
// TODO - persist images locally (local image repository)
this
.
$emit
(
'
uploadImage
'
,
{
file
,
imageUrl
});
// TODO - ensure that the actual repo URL for the image is used in Markdown mode
// TODO - upload images to the project repository (on submit)
}
addImage
(
this
.
editorInstance
,
image
);
...
...
@@ -130,6 +131,6 @@ export default {
@
change=
"onContentChanged"
@
load=
"onLoad"
/>
<add-image-modal
ref=
"addImageModal"
@
addImage=
"onAddImage"
/>
<add-image-modal
ref=
"addImageModal"
:image-root=
"imageRoot"
@
addImage=
"onAddImage"
/>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/rich_content_editor/services/image_service.js
deleted
100644 → 0
View file @
37d86926
// eslint-disable-next-line import/prefer-default-export
export
const
getUrl
=
file
=>
URL
.
createObjectURL
(
file
);
locale/gitlab.pot
View file @
c416d436
...
...
@@ -21508,6 +21508,9 @@ msgstr ""
msgid "Something went wrong while initializing the OpenAPI viewer"
msgstr ""
msgid "Something went wrong while inserting your image. Please try again."
msgstr ""
msgid "Something went wrong while merging this merge request. Please try again."
msgstr ""
...
...
spec/frontend/static_site_editor/mock_data.js
View file @
c416d436
...
...
@@ -10,6 +10,8 @@ export const sourceContentBody = `## On this page
- TOC
{:toc .hidden-md .hidden-lg}
![image](path/to/image1.png)
`
;
export
const
sourceContent
=
`
${
sourceContentHeader
}${
sourceContentSpacing
}${
sourceContentBody
}
`
;
export
const
sourceContentTitle
=
'
Handbook
'
;
...
...
@@ -48,3 +50,8 @@ export const createMergeRequestResponse = {
};
export
const
trackingCategory
=
'
projects:static_site_editor:show
'
;
export
const
images
=
new
Map
([
[
'
path/to/image1.png
'
,
'
image1-content
'
],
[
'
path/to/image2.png
'
,
'
image2-content
'
],
]);
spec/frontend/static_site_editor/services/submit_content_changes_spec.js
View file @
c416d436
...
...
@@ -22,6 +22,7 @@ import {
sourcePath
,
sourceContent
as
content
,
trackingCategory
,
images
,
}
from
'
../mock_data
'
;
jest
.
mock
(
'
~/static_site_editor/services/generate_branch_name
'
);
...
...
@@ -69,7 +70,7 @@ describe('submitContentChanges', () => {
});
it
(
'
commits the content changes to the branch when creating branch succeeds
'
,
()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
}).
then
(()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
,
images
}).
then
(()
=>
{
expect
(
Api
.
commitMultiple
).
toHaveBeenCalledWith
(
projectId
,
{
branch
,
commit_message
:
mergeRequestTitle
,
...
...
@@ -79,6 +80,35 @@ describe('submitContentChanges', () => {
file_path
:
sourcePath
,
content
,
},
{
action
:
'
create
'
,
content
:
'
image1-content
'
,
encoding
:
'
base64
'
,
file_path
:
'
path/to/image1.png
'
,
},
],
});
});
});
it
(
'
does not commit an image if it has been removed from the content
'
,
()
=>
{
const
contentWithoutImages
=
'
## Content without images
'
;
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
:
contentWithoutImages
,
images
,
}).
then
(()
=>
{
expect
(
Api
.
commitMultiple
).
toHaveBeenCalledWith
(
projectId
,
{
branch
,
commit_message
:
mergeRequestTitle
,
actions
:
[
{
action
:
'
update
'
,
file_path
:
sourcePath
,
content
:
contentWithoutImages
,
},
],
});
});
...
...
@@ -87,13 +117,13 @@ describe('submitContentChanges', () => {
it
(
'
notifies error when content could not be committed
'
,
()
=>
{
Api
.
commitMultiple
.
mockRejectedValueOnce
();
return
expect
(
submitContentChanges
({
username
,
projectId
})).
rejects
.
toThrow
(
return
expect
(
submitContentChanges
({
username
,
projectId
,
images
})).
rejects
.
toThrow
(
SUBMIT_CHANGES_COMMIT_ERROR
,
);
});
it
(
'
creates a merge request when commiting changes succeeds
'
,
()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
}).
then
(()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
,
images
}).
then
(()
=>
{
expect
(
Api
.
createProjectMergeRequest
).
toHaveBeenCalledWith
(
projectId
,
convertObjectPropsToSnakeCase
({
...
...
@@ -108,7 +138,7 @@ describe('submitContentChanges', () => {
it
(
'
notifies error when merge request could not be created
'
,
()
=>
{
Api
.
createProjectMergeRequest
.
mockRejectedValueOnce
();
return
expect
(
submitContentChanges
({
username
,
projectId
})).
rejects
.
toThrow
(
return
expect
(
submitContentChanges
({
username
,
projectId
,
images
})).
rejects
.
toThrow
(
SUBMIT_CHANGES_MERGE_REQUEST_ERROR
,
);
});
...
...
@@ -117,9 +147,11 @@ describe('submitContentChanges', () => {
let
result
;
beforeEach
(()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
}).
then
(
_result
=>
{
result
=
_result
;
});
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
,
images
}).
then
(
_result
=>
{
result
=
_result
;
},
);
});
it
(
'
returns the branch name
'
,
()
=>
{
...
...
@@ -147,7 +179,7 @@ describe('submitContentChanges', () => {
describe
(
'
sends the correct tracking event
'
,
()
=>
{
beforeEach
(()
=>
{
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
});
return
submitContentChanges
({
username
,
projectId
,
sourcePath
,
content
,
images
});
});
it
(
'
for committing changes
'
,
()
=>
{
...
...
spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
View file @
c416d436
...
...
@@ -6,6 +6,7 @@ import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constant
describe
(
'
Add Image Modal
'
,
()
=>
{
let
wrapper
;
const
propsData
=
{
imageRoot
:
'
path/to/root/
'
};
const
findModal
=
()
=>
wrapper
.
find
(
GlModal
);
const
findTabs
=
()
=>
wrapper
.
find
(
GlTabs
);
...
...
@@ -14,7 +15,10 @@ describe('Add Image Modal', () => {
const
findDescriptionInput
=
()
=>
wrapper
.
find
({
ref
:
'
descriptionInput
'
});
beforeEach
(()
=>
{
wrapper
=
shallowMount
(
AddImageModal
,
{
provide
:
{
glFeatures
:
{
sseImageUploads
:
true
}
}
});
wrapper
=
shallowMount
(
AddImageModal
,
{
provide
:
{
glFeatures
:
{
sseImageUploads
:
true
}
},
propsData
,
});
});
describe
(
'
when content is loaded
'
,
()
=>
{
...
...
@@ -44,9 +48,10 @@ describe('Add Image Modal', () => {
it
(
'
validates the file
'
,
()
=>
{
const
preventDefault
=
jest
.
fn
();
const
description
=
'
some description
'
;
const
file
=
{
name
:
'
some_file.png
'
};
wrapper
.
vm
.
$refs
.
uploadImageTab
=
{
validateFile
:
jest
.
fn
()
};
wrapper
.
setData
({
description
,
tabIndex
:
IMAGE_TABS
.
UPLOAD_TAB
});
wrapper
.
setData
({
file
,
description
,
tabIndex
:
IMAGE_TABS
.
UPLOAD_TAB
});
findModal
().
vm
.
$emit
(
'
ok
'
,
{
preventDefault
});
...
...
spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
View file @
c416d436
...
...
@@ -28,12 +28,13 @@ describe('Rich Content Editor', () => {
let
wrapper
;
const
content
=
'
## Some Markdown
'
;
const
imageRoot
=
'
path/to/root/
'
;
const
findEditor
=
()
=>
wrapper
.
find
({
ref
:
'
editor
'
});
const
findAddImageModal
=
()
=>
wrapper
.
find
(
AddImageModal
);
beforeEach
(()
=>
{
wrapper
=
shallowMount
(
RichContentEditor
,
{
propsData
:
{
content
},
propsData
:
{
content
,
imageRoot
},
});
});
...
...
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