Merge pull request #3804 from overleaf/msm-react-publish-button

[ReactNavigationToolbar] Submit button

GitOrigin-RevId: 9b40e09f001b44bd2f5035469f0d0c852fea7199
This commit is contained in:
Timothée Alby 2021-04-19 14:38:03 +02:00 committed by Copybot
parent f7166c5c1b
commit 0ecebefb0c
9 changed files with 173 additions and 64 deletions

View file

@ -1,34 +1,20 @@
/* eslint-disable
node/handle-callback-err,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let BrandVariationsHandler
const OError = require('@overleaf/o-error')
const url = require('url')
const settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const V1Api = require('../V1/V1Api')
const sanitizeHtml = require('sanitize-html')
module.exports = BrandVariationsHandler = {
getBrandVariationById(brandVariationId, callback) {
if (callback == null) {
callback = function (error, brandVariationDetails) {}
}
module.exports = {
getBrandVariationById
}
function getBrandVariationById(brandVariationId, callback) {
if (brandVariationId == null || brandVariationId === '') {
return callback(new Error('Branding variation id not provided'))
}
logger.log({ brandVariationId }, 'fetching brand variation details from v1')
return V1Api.request(
V1Api.request(
{
uri: `/api/v2/brand_variations/${brandVariationId}`
},
@ -39,48 +25,58 @@ module.exports = BrandVariationsHandler = {
})
return callback(error)
}
_formatBrandVariationDetails(brandVariationDetails)
return callback(null, brandVariationDetails)
formatBrandVariationDetails(brandVariationDetails)
sanitizeBrandVariationDetails(brandVariationDetails)
callback(null, brandVariationDetails)
}
)
}
}
var _formatBrandVariationDetails = function (details) {
function formatBrandVariationDetails(details) {
if (details.export_url != null) {
details.export_url = _setV1AsHostIfRelativeURL(details.export_url)
details.export_url = setV1AsHostIfRelativeURL(details.export_url)
}
if (details.home_url != null) {
details.home_url = _setV1AsHostIfRelativeURL(details.home_url)
details.home_url = setV1AsHostIfRelativeURL(details.home_url)
}
if (details.logo_url != null) {
details.logo_url = _setV1AsHostIfRelativeURL(details.logo_url)
details.logo_url = setV1AsHostIfRelativeURL(details.logo_url)
}
if (details.journal_guidelines_url != null) {
details.journal_guidelines_url = _setV1AsHostIfRelativeURL(
details.journal_guidelines_url = setV1AsHostIfRelativeURL(
details.journal_guidelines_url
)
}
if (details.journal_cover_url != null) {
details.journal_cover_url = _setV1AsHostIfRelativeURL(
details.journal_cover_url = setV1AsHostIfRelativeURL(
details.journal_cover_url
)
}
if (details.submission_confirmation_page_logo_url != null) {
details.submission_confirmation_page_logo_url = _setV1AsHostIfRelativeURL(
details.submission_confirmation_page_logo_url = setV1AsHostIfRelativeURL(
details.submission_confirmation_page_logo_url
)
}
if (details.publish_menu_icon != null) {
return (details.publish_menu_icon = _setV1AsHostIfRelativeURL(
details.publish_menu_icon = setV1AsHostIfRelativeURL(
details.publish_menu_icon
))
)
}
}
var _setV1AsHostIfRelativeURL = urlString =>
function sanitizeBrandVariationDetails(details) {
if (details.submit_button_html) {
details.submit_button_html = sanitizeHtml(
details.submit_button_html,
settings.modules.sanitize.options
)
}
}
function setV1AsHostIfRelativeURL(urlString) {
// The first argument is the base URL to resolve against if the second argument is not absolute.
// As it only applies if the second argument is not absolute, we can use it to transform relative URLs into
// absolute ones using v1 as the host. If the URL is absolute (e.g. a filepicker one), then the base
// argument is just ignored
url.resolve(settings.apis.v1.url, urlString)
return url.resolve(settings.apis.v1.url, urlString)
}

View file

@ -718,6 +718,7 @@ module.exports = settings =
'img': [ 'alt', 'class', 'src', 'style' ]
'source': [ 'src', 'type' ]
'span': [ 'class', 'id', 'style' ]
'strong': [ 'style' ]
'table': [ 'border', 'class', 'id', 'style' ]
'td': [ 'colspan', 'rowspan', 'headers', 'style' ]
'th': [ 'abbr', 'headers', 'colspan', 'rowspan', 'scope', 'sorted', 'style' ]
@ -728,6 +729,7 @@ module.exports = settings =
# modules to import (an empty array for each set of modules)
createFileModes: []
gitBridge: []
publishModal: []
}
csp: {

View file

@ -212,6 +212,7 @@
"stop_compile": "",
"stop_on_validation_error": "",
"store_your_work": "",
"submit": "",
"sure_you_want_to_delete": "",
"sync_project_to_github_explanation": "",
"sync_to_dropbox": "",

View file

@ -10,6 +10,10 @@ import TrackChangesToggleButton from './track-changes-toggle-button'
import HistoryToggleButton from './history-toggle-button'
import ShareProjectButton from './share-project-button'
import PdfToggleButton from './pdf-toggle-button'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
const [publishModalModules] = importOverleafModules('publishModal')
const PublishButton = publishModalModules?.import.default
function ToolbarHeader({
cobranding,
@ -53,14 +57,18 @@ function ToolbarHeader({
<div className="toolbar-right">
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
<ShareProjectButton onClick={openShareModal} />
{!isRestrictedTokenMember && (
<>
<TrackChangesToggleButton
onClick={toggleReviewPanelOpen}
disabled={historyIsOpen}
trackChangesIsOpen={reviewPanelOpen}
/>
)}
<ShareProjectButton onClick={openShareModal} />
{PublishButton && <PublishButton cobranding={cobranding} />}
{!isRestrictedTokenMember && (
<>
<HistoryToggleButton
historyIsOpen={historyIsOpen}
onClick={toggleHistoryOpen}

View file

@ -7,7 +7,9 @@ export const ApplicationContext = createContext()
ApplicationContext.Provider.propTypes = {
value: PropTypes.shape({
user: PropTypes.shape({
id: PropTypes.string.isRequired
id: PropTypes.string.isRequired,
firstName: PropTypes.string,
lastName: PropTypes.string
}),
exposedSettings: PropTypes.shape({
appName: PropTypes.string.isRequired,

View file

@ -0,0 +1,47 @@
import React, { createContext, useContext } from 'react'
import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook'
export const CompileContext = createContext()
CompileContext.Provider.propTypes = {
value: PropTypes.shape({
pdfUrl: PropTypes.string,
pdfDownloadUrl: PropTypes.string,
logEntries: PropTypes.object,
uncompiled: PropTypes.bool
})
}
export function CompileProvider({ children, $scope }) {
const [pdfUrl] = useScopeValue('pdf.url', $scope)
const [pdfDownloadUrl] = useScopeValue('pdf.downloadUrl', $scope)
const [logEntries] = useScopeValue('pdf.logEntries', $scope)
const [uncompiled] = useScopeValue('pdf.uncompiled', $scope)
const value = {
pdfUrl,
pdfDownloadUrl,
logEntries,
uncompiled
}
return (
<>
<CompileContext.Provider value={value}>
{children}
</CompileContext.Provider>
</>
)
}
CompileProvider.propTypes = {
children: PropTypes.any,
$scope: PropTypes.any.isRequired
}
export function useCompileContext(propTypes) {
const data = useContext(CompileContext)
PropTypes.checkPropTypes(propTypes, data, 'data', 'CompileContext.Provider')
return data
}

View file

@ -11,14 +11,22 @@ EditorContext.Provider.propTypes = {
cobranding: PropTypes.shape({
logoImgUrl: PropTypes.string.isRequired,
brandVariationName: PropTypes.string.isRequired,
brandVariationHomeUrl: PropTypes.string.isRequired
brandVariationId: PropTypes.number.isRequired,
brandId: PropTypes.number.isRequired,
brandVariationHomeUrl: PropTypes.string.isRequired,
publishGuideHtml: PropTypes.string,
partner: PropTypes.string,
brandedMenu: PropTypes.string,
submitBtnHtml: PropTypes.string
}),
loading: PropTypes.bool,
projectRootDocId: PropTypes.string,
projectId: PropTypes.string.isRequired,
projectName: PropTypes.string.isRequired,
renameProject: PropTypes.func.isRequired,
isProjectOwner: PropTypes.bool,
isRestrictedTokenMember: PropTypes.bool
isRestrictedTokenMember: PropTypes.bool,
rootFolder: PropTypes.object
})
}
@ -34,7 +42,13 @@ export function EditorProvider({ children, ide, settings }) {
? {
logoImgUrl: window.brandVariation.logo_url,
brandVariationName: window.brandVariation.name,
brandVariationHomeUrl: window.brandVariation.home_url
brandVariationId: window.brandVariation.id,
brandId: window.brandVariation.brand_id,
brandVariationHomeUrl: window.brandVariation.home_url,
publishGuideHtml: window.brandVariation.publish_guide_html,
partner: window.brandVariation.partner,
brandedMenu: window.brandVariation.branded_menu,
submitBtnHtml: window.brandVariation.submit_button_html
}
: undefined
@ -45,11 +59,15 @@ export function EditorProvider({ children, ide, settings }) {
const [loading] = useScopeValue('state.loading', ide.$scope)
const [projectRootDocId] = useScopeValue('project.rootDoc_id', ide.$scope)
const [projectName, setProjectName] = useScopeValue(
'project.name',
ide.$scope
)
const [rootFolder] = useScopeValue('rootFolder', ide.$scope)
const renameProject = useCallback(
newName => {
setProjectName(oldName => {
@ -82,10 +100,12 @@ export function EditorProvider({ children, ide, settings }) {
cobranding,
loading,
projectId: window.project_id,
projectRootDocId,
projectName: projectName || '', // initially might be empty in Angular
renameProject,
isProjectOwner: ownerId === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder
}
return (

View file

@ -5,14 +5,17 @@ import { EditorProvider } from './editor-context'
import createSharedContext from 'react2angular-shared-context'
import { ChatProvider } from '../../features/chat/context/chat-context'
import { LayoutProvider } from './layout-context'
import { CompileProvider } from './compile-context'
export function ContextRoot({ children, ide, settings }) {
return (
<ApplicationProvider>
<EditorProvider ide={ide} settings={settings}>
<CompileProvider $scope={ide.$scope}>
<LayoutProvider $scope={ide.$scope}>
<ChatProvider>{children}</ChatProvider>
</LayoutProvider>
</CompileProvider>
</EditorProvider>
</ApplicationProvider>
)

View file

@ -28,6 +28,16 @@ describe('BrandVariationsHandler', function () {
v1: {
url: 'http://overleaf.example.com'
}
},
modules: {
sanitize: {
options: {
allowedTags: ['br', 'strong'],
allowedAttributes: {
strong: ['style']
}
}
}
}
}
this.V1Api = { request: sinon.stub() }
@ -107,5 +117,25 @@ describe('BrandVariationsHandler', function () {
}
)
})
it("should sanitize 'submit_button_html'", function (done) {
this.mockedBrandVariationDetails.submit_button_html =
'<br class="break"/><strong style="color:#B39500">AGU Journal</strong><iframe>hello</iframe>'
this.V1Api.request.callsArgWith(
1,
null,
{ statusCode: 200 },
this.mockedBrandVariationDetails
)
return this.BrandVariationsHandler.getBrandVariationById(
'12',
(err, brandVariationDetails) => {
expect(brandVariationDetails.submit_button_html).to.equal(
'<br /><strong style="color:#B39500">AGU Journal</strong>hello'
)
return done()
}
)
})
})
})