mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3804 from overleaf/msm-react-publish-button
[ReactNavigationToolbar] Submit button GitOrigin-RevId: 9b40e09f001b44bd2f5035469f0d0c852fea7199
This commit is contained in:
parent
f7166c5c1b
commit
0ecebefb0c
9 changed files with 173 additions and 64 deletions
|
@ -1,86 +1,82 @@
|
|||
/* 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) {}
|
||||
}
|
||||
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(
|
||||
{
|
||||
uri: `/api/v2/brand_variations/${brandVariationId}`
|
||||
},
|
||||
function (error, response, brandVariationDetails) {
|
||||
if (error != null) {
|
||||
OError.tag(error, 'error getting brand variation details', {
|
||||
brandVariationId
|
||||
})
|
||||
return callback(error)
|
||||
}
|
||||
_formatBrandVariationDetails(brandVariationDetails)
|
||||
return callback(null, brandVariationDetails)
|
||||
}
|
||||
)
|
||||
}
|
||||
module.exports = {
|
||||
getBrandVariationById
|
||||
}
|
||||
|
||||
var _formatBrandVariationDetails = function (details) {
|
||||
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')
|
||||
V1Api.request(
|
||||
{
|
||||
uri: `/api/v2/brand_variations/${brandVariationId}`
|
||||
},
|
||||
function (error, response, brandVariationDetails) {
|
||||
if (error != null) {
|
||||
OError.tag(error, 'error getting brand variation details', {
|
||||
brandVariationId
|
||||
})
|
||||
return callback(error)
|
||||
}
|
||||
formatBrandVariationDetails(brandVariationDetails)
|
||||
sanitizeBrandVariationDetails(brandVariationDetails)
|
||||
callback(null, brandVariationDetails)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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} />
|
||||
|
||||
{!isRestrictedTokenMember && (
|
||||
<TrackChangesToggleButton
|
||||
onClick={toggleReviewPanelOpen}
|
||||
disabled={historyIsOpen}
|
||||
trackChangesIsOpen={reviewPanelOpen}
|
||||
/>
|
||||
)}
|
||||
<ShareProjectButton onClick={openShareModal} />
|
||||
{PublishButton && <PublishButton cobranding={cobranding} />}
|
||||
{!isRestrictedTokenMember && (
|
||||
<>
|
||||
<TrackChangesToggleButton
|
||||
onClick={toggleReviewPanelOpen}
|
||||
disabled={historyIsOpen}
|
||||
trackChangesIsOpen={reviewPanelOpen}
|
||||
/>
|
||||
<HistoryToggleButton
|
||||
historyIsOpen={historyIsOpen}
|
||||
onClick={toggleHistoryOpen}
|
||||
|
|
|
@ -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,
|
||||
|
|
47
services/web/frontend/js/shared/context/compile-context.js
Normal file
47
services/web/frontend/js/shared/context/compile-context.js
Normal 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
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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}>
|
||||
<LayoutProvider $scope={ide.$scope}>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</LayoutProvider>
|
||||
<CompileProvider $scope={ide.$scope}>
|
||||
<LayoutProvider $scope={ide.$scope}>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</LayoutProvider>
|
||||
</CompileProvider>
|
||||
</EditorProvider>
|
||||
</ApplicationProvider>
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue